View Javadoc

1   /*
2      Licensed under the Apache License, Version 2.0 (the "License");
3      you may not use this file except in compliance with the License.
4      You may obtain a copy of the License at
5   
6        http://www.apache.org/licenses/LICENSE-2.0
7   
8      Unless required by applicable law or agreed to in writing, software
9      distributed under the License is distributed on an "AS IS" BASIS,
10     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11     See the License for the specific language governing permissions and
12     limitations under the License.
13  */
14  package uk.nhs.interoperability.consumer.appemulator;
15  
16  import java.io.IOException;
17  import java.io.InputStream;
18  import java.util.HashMap;
19  import java.util.Map;
20  import java.util.Properties;
21  import java.util.Queue;
22  import java.util.Scanner;
23  import java.util.UUID;
24  import java.util.concurrent.ExecutorService;
25  import java.util.concurrent.Executors;
26  import java.util.concurrent.LinkedBlockingQueue;
27  
28  import uk.nhs.interoperability.consumer.ITKMessageConsumer;
29  import uk.nhs.interoperability.infrastructure.ITKAddressImpl;
30  import uk.nhs.interoperability.infrastructure.ITKMessageProperties;
31  import uk.nhs.interoperability.infrastructure.ITKMessagingException;
32  import uk.nhs.interoperability.payload.ITKMessage;
33  import uk.nhs.interoperability.payload.ITKSimpleMessageResponse;
34  import uk.nhs.interoperability.payload.SimpleMessage;
35  import uk.nhs.interoperability.source.ITKMessageSender;
36  import uk.nhs.interoperability.source.ITKMessageSenderImpl;
37  import uk.nhs.interoperability.transform.TransformManager;
38  import uk.nhs.interoperability.util.HL7Utils;
39  import uk.nhs.interoperability.util.Logger;
40  
41  /**
42   * The Class SimpleApplicationEmulator.
43   *
44   * @author Michael Odling-Smee
45   * @author Nicholas Jones
46   * @since 0.1
47   */
48  public class SimpleApplicationEmulator implements ITKMessageConsumer, Runnable {
49  
50  	/** The Constant FROMADDRESS. */
51  	private static final String FROMADDRESS = "urn:nhs-uk:addressing:ods:TESTORGS:ORGA";
52  
53  	/** The executor service. */
54  	private ExecutorService executorService;
55  	
56  	/** The is running. */
57  	private boolean isRunning = true;
58  	
59  	/** The itk message sender. */
60  	private ITKMessageSender itkMessageSender;
61  	
62  	/** The props. */
63  	private Properties props = new Properties();
64  	
65  	/** The audit identity. */
66  	private String auditIdentity;
67  	
68  	/*
69  	 * Construct an in-memory queue to queue ITK messages request for
70  	 * subsequent asynchronous processing - this emulates a common
71  	 * pattern in many application where inbound messages are queued
72  	 * for processing 
73  	 */
74  	/** The async processing queue. */
75  	private Queue<ITKMessage> asyncProcessingQueue = new LinkedBlockingQueue<ITKMessage>();
76  	
77  	/**
78  	 * Instantiates a new simple application emulator.
79  	 */
80  	public SimpleApplicationEmulator() {
81  		this.executorService = Executors.newFixedThreadPool(1);
82  		this.executorService.execute(this);
83  		this.itkMessageSender = new ITKMessageSenderImpl();
84  		this.props = new Properties();
85  		try {
86  			props.load(this.getClass().getResourceAsStream("/consumeremulator.properties"));
87  		} catch (IOException e) {
88  			Logger.error("Could not load consumer emulator properties - emulator not likely to behave correctly", e);
89  		}
90  		this.auditIdentity = this.props.getProperty("audit.identity");
91  	}
92  
93  	/* (non-Javadoc)
94  	 * @see uk.nhs.interoperability.consumer.ITKMessageConsumer#onSyncMessage(uk.nhs.interoperability.payload.ITKMessage)
95  	 */
96  	@Override
97  	public ITKMessage onSyncMessage(ITKMessage request) throws ITKMessagingException {
98  		Logger.debug("Application invoked");
99  		ITKMessage response = this.processMessage(request);
100 		response.getMessageProperties().setFromAddress(new ITKAddressImpl(FROMADDRESS));
101 
102 		return response;
103 	}
104 	
105 	/* (non-Javadoc)
106 	 * @see uk.nhs.interoperability.consumer.ITKMessageConsumer#onMessage(uk.nhs.interoperability.payload.ITKMessage)
107 	 */
108 	@Override
109 	public void onMessage(ITKMessage request) throws ITKMessagingException {
110 		//Do some (really) basic validation of the message
111 		if (request == null) {
112 			throw new ITKMessagingException("The request was null - could not process");
113 		}
114 		if (request.getBusinessPayload() == null && request.getMessageProperties() == null) {
115 			throw new ITKMessagingException(request.getMessageProperties(), ITKMessagingException.INVALID_MESSAGE_CODE, "The request message properties or contents were null - message cannot be processed");
116 		}
117 		//Queue for asynchronous processing
118 		this.asyncProcessingQueue.add(request);
119 	}
120 	
121 	/**
122 	 * Process async message.
123 	 *
124 	 * @param request the request
125 	 */
126 	private void processAsyncMessage(ITKMessage request) {
127 		Logger.debug("Processing queued itk request");
128 		try {
129 			ITKMessage response = this.processMessage(request);
130 			if (response != null) {
131 				response.getMessageProperties().setFromAddress(new ITKAddressImpl(FROMADDRESS));
132 				this.itkMessageSender.send(response);
133 			} else {
134 				Logger.info("No response configured/created for " + request);
135 			}
136 		} catch (ITKMessagingException e) {
137 			/*
138 			 * In a real application some more in-depth error
139 			 * handling may occur such as storing the failed message
140 			 * for attention of an administrator, however for this
141 			 * simple application emulator we just log an error
142 			 */
143 			Logger.error("Could not send aysnchronous response", e);
144 		}
145 	}
146 	
147 	/**
148 	 * Process message.
149 	 *
150 	 * @param request the request
151 	 * @return the iTK message
152 	 * @throws ITKMessagingException the iTK messaging exception
153 	 */
154 	private ITKMessage processMessage(ITKMessage request) throws ITKMessagingException {
155 		
156 		String requestMsgService = request.getMessageProperties().getServiceId();	
157 		/*
158 		 * This simple emulator application's response behaviour is determined
159 		 * via configuration - more sophisticated measures are likely to exist in
160 		 * a real world implementation
161 		 */
162 		String responseType = this.props.getProperty(requestMsgService + ".response.type");
163 		
164 		//Conditional logic depending on type of response
165 		if (responseType == null) {
166 			//Ooops no response configured
167 			throw new ITKMessagingException("Incorrect emulator configuration - no response type configured for " + requestMsgService);
168 			
169 		} else if (responseType.equalsIgnoreCase("simple")) {
170 			//Simple message response - can be generated directly
171 			Logger.trace("Creating a simple message response");
172 			return new ITKSimpleMessageResponse(request.getMessageProperties(), true);
173 			
174 		} else if (responseType.equalsIgnoreCase("fullResponse")) {
175 			//Full business response - construct via XSLT
176 			String responseProfileId = this.props.getProperty(request.getMessageProperties().getServiceId() + "Response.profileId");
177 			Logger.trace("Creating a business response");
178 			return this.turnaroundViaXSLT(responseProfileId, request);
179 			
180 		} else if (responseType.equalsIgnoreCase("fixedResponse")) {
181 			// Fixed response - construct via fixed response for message type
182 			String responseProfileId = this.props.getProperty(request.getMessageProperties().getServiceId() + "Response.profileId");
183 			Logger.trace("Creating a Fixed response");
184 			return this.turnaroundViaFile(responseProfileId, request);
185 
186 		} else if (responseType.equalsIgnoreCase("businessAck")) {
187 			//Create a business ack if required
188 			return this.createBusinessAck(request);
189 		}  else if (responseType.equalsIgnoreCase("none")) {
190 			//No business response is expected
191 			return null;
192 		} else {
193 			//Whilst something was configured the value was unexpected
194 			throw new ITKMessagingException("Incorrect emulator configuration - unknown response type (" + responseType + ") configured for " + requestMsgService);
195 		}
196 	}
197 	
198 	/**
199 	 * Creates the business ack.
200 	 *
201 	 * @param request the request
202 	 * @return the iTK message
203 	 * @throws ITKMessagingException the iTK messaging exception
204 	 */
205 	private ITKMessage createBusinessAck(ITKMessage request) throws ITKMessagingException {
206 		//Create a businessAck if requested
207 		String businessAckHandlingSpec = request.getMessageProperties().getHandlingSpecification(ITKMessageProperties.BUSINESS_ACK_HANDLING_SPECIFICATION_KEY);
208 		if (businessAckHandlingSpec != null && businessAckHandlingSpec.equals("true")) {
209 			String businessAckService = "urn:nhs-itk:services:201005:SendBusinessAck-v1-0";
210 			String responseProfileId = this.props.getProperty(businessAckService + ".profileId");
211 			Logger.trace("Creating a business response");
212 			ITKMessage msg = this.turnaroundViaXSLT(responseProfileId, request);
213 			msg.getMessageProperties().setServiceId(businessAckService);
214 			return msg;
215 		} else {
216 			Logger.trace("No handling specification for business ack - not creating a business Ack");
217 		}
218 		//Otherwise no response is required
219 		return null;
220 	}
221 	
222 	/**
223 	 * Turnaround via xslt.
224 	 *
225 	 * @param responseProfileId the response profile id
226 	 * @param request the request
227 	 * @return the iTK message
228 	 * @throws ITKMessagingException the iTK messaging exception
229 	 */
230 	private ITKMessage turnaroundViaXSLT(String responseProfileId, ITKMessage request) throws ITKMessagingException {
231 		//Use simple XSLTs to create appropriate responses
232 		String xslt = this.props.getProperty(request.getMessageProperties().getServiceId() + ".turnaround.xslt");
233 		if (xslt != null) {
234 			
235 			//Create a response using the message properties from the request
236 			SimpleMessage msg = new SimpleMessage(request.getMessageProperties(), this.auditIdentity, responseProfileId, true);
237 			UUID messageId = UUID.randomUUID();
238 			msg.getMessageProperties().setBusinessPayloadId(messageId.toString().toUpperCase());
239 			
240 			Logger.trace("Using " + xslt + " to turnaround request");
241 			String inputXML = request.getBusinessPayload();
242 			//Logger.trace("XSLT input " + inputXML);
243 			String outputXML = TransformManager.doTransform(xslt, inputXML, this.getTransformParameters(msg));
244 			//Logger.trace("XSLT output " + outputXML);			
245 			msg.setBusinessPayload(outputXML);
246 			
247 			return msg;			
248 		} else {
249 			Logger.warn("Could not use XSLT to turnaround request");
250 		}
251 		return null;
252 	}
253 	
254 	/**
255 	 * Turnaround via file.
256 	 *
257 	 * @param responseProfileId the response profile id
258 	 * @param request the request
259 	 * @return the iTK message
260 	 * @throws ITKMessagingException the iTK messaging exception
261 	 */
262 	private ITKMessage turnaroundViaFile(String responseProfileId, ITKMessage request) throws ITKMessagingException {
263 		
264 		//Use fixed file response
265 		String fileName = this.props.getProperty(request.getMessageProperties().getServiceId() + ".turnaround.txt");
266 		if (fileName != null) {
267 			
268 			
269 			//Create a response using the message properties from the request
270 			SimpleMessage msg = new SimpleMessage(request.getMessageProperties(), this.auditIdentity, responseProfileId, true);
271 			UUID messageId = UUID.randomUUID();
272 			msg.getMessageProperties().setBusinessPayloadId(messageId.toString().toUpperCase());
273 			
274 			Logger.trace("Using " + fileName + " to turnaround request");
275 			String responseString;
276 			try {
277 				responseString = readFile(fileName);
278 			} catch (IOException e) {
279 				e.printStackTrace();
280 				throw new ITKMessagingException("Incorrect emulator configuration - error reading response file (" + fileName);
281 			}
282 			msg.setBusinessPayload(responseString);
283 			return msg;			
284 
285 		} else {
286 			Logger.warn("Could not load turnaround request from file.");
287 		}
288 		return null;
289 	}
290 	
291 	/**
292 	 * Extract some common properties from the newly created
293 	 * message to pass to the XSLT transform as a map of parameters.
294 	 *
295 	 * @param itkMessage The skeleton ItkMessage whose contents will be created
296 	 * as a result of the transform
297 	 * @return A Map containing the appropriate XSLT transform parameters including items
298 	 * such as the new message Id, sender, receiver etc.
299 	 */
300 	private Map<String, String> getTransformParameters(ITKMessage itkMessage) {
301 		if (itkMessage != null && itkMessage.getMessageProperties() != null) {
302 			 ITKMessageProperties msgProps = itkMessage.getMessageProperties();
303 			 Map<String, String> params = new HashMap<String, String>();
304 			 params.put("response-msg-id", msgProps.getBusinessPayloadId());
305 			 params.put("from-address", msgProps.getFromAddress().getURI());
306 			 params.put("to-address", msgProps.getToAddress().getURI());
307 			 params.put("creation-time", HL7Utils.getHL7DateTime());
308 			 return params;
309 		}
310 		return null;
311 	}
312 	
313 	/* (non-Javadoc)
314 	 * @see java.lang.Runnable#run()
315 	 */
316 	@Override
317 	public void run() {
318 		while (this.isRunning) {
319 			try {
320 				if (this.asyncProcessingQueue.isEmpty()) {
321 					Thread.sleep(5000);
322 				} else {
323 					this.processAsyncMessage(this.asyncProcessingQueue.poll());
324 				}
325 			} catch (InterruptedException e) {
326 				this.isRunning = false;
327 			}
328 		}
329 	}
330 	
331 	/**
332 	 * Read file.
333 	 *
334 	 * @param fname the fname
335 	 * @return the string
336 	 * @throws IOException Signals that an I/O exception has occurred.
337 	 */
338 	private String readFile(String fname) throws IOException {
339 
340 	    InputStream tis = this.getClass().getResourceAsStream("/messages/"+fname);
341 	    StringBuilder fileContents = new StringBuilder();
342 	    Scanner scanner = new Scanner(tis);
343 	    String lineSeparator = System.getProperty("line.separator");
344 
345 	    try {
346 	        while(scanner.hasNextLine()) {        
347 	            fileContents.append(scanner.nextLine() + lineSeparator);
348 	        }
349 	        return fileContents.toString();
350 	    } finally {
351 	        scanner.close();
352 	    }
353 	}
354 
355 }