View Javadoc

1   /*
2    * Created on 21-Apr-2004
3    */
4   package ca.uhn.hl7v2.protocol.impl;
5   
6   import java.io.IOException;
7   import java.util.ArrayList;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.regex.Pattern;
11  
12  import org.slf4j.Logger;
13  import org.slf4j.LoggerFactory;
14  
15  import ca.uhn.hl7v2.AcknowledgmentCode;
16  import ca.uhn.hl7v2.HL7Exception;
17  import ca.uhn.hl7v2.Version;
18  import ca.uhn.hl7v2.app.DefaultApplication;
19  import ca.uhn.hl7v2.model.GenericMessage;
20  import ca.uhn.hl7v2.model.Message;
21  import ca.uhn.hl7v2.model.Segment;
22  import ca.uhn.hl7v2.parser.GenericParser;
23  import ca.uhn.hl7v2.parser.Parser;
24  import ca.uhn.hl7v2.protocol.ApplicationRouter;
25  import ca.uhn.hl7v2.protocol.MetadataKeys;
26  import ca.uhn.hl7v2.protocol.ReceivingApplication;
27  import ca.uhn.hl7v2.protocol.ReceivingApplicationExceptionHandler;
28  import ca.uhn.hl7v2.protocol.Transportable;
29  import ca.uhn.hl7v2.util.DeepCopy;
30  import ca.uhn.hl7v2.util.Terser;
31  
32  /**
33   * <p>A default implementation of <code>ApplicationRouter</code> </p>  
34   * 
35   * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
36   * @version $Revision: 1.2 $ updated on $Date: 2009-09-01 00:22:23 $ by $Author: jamesagnew $
37   */
38  public class ApplicationRouterImpl implements ApplicationRouter {
39  
40  	private static final Logger log = LoggerFactory.getLogger(ApplicationRouterImpl.class);
41      
42      /**
43       * Key under which raw message text is stored in metadata Map sent to 
44       * <code>ReceivingApplication</code>s. 
45       */
46      public static String RAW_MESSAGE_KEY = MetadataKeys.IN_RAW_MESSAGE; 
47  
48      private List<Binding> myBindings;
49      private Parser myParser;
50  
51  	private ReceivingApplicationExceptionHandler myExceptionHandler;
52      
53  
54      /**
55       * Creates an instance that uses a <code>GenericParser</code>. 
56       */
57      public ApplicationRouterImpl() {
58          init(new GenericParser());
59      }
60      
61      /**
62       * Creates an instance that uses the specified <code>Parser</code>. 
63       * @param theParser the parser used for converting between Message and 
64       *      Transportable
65       */
66      public ApplicationRouterImpl(Parser theParser) {
67          init(theParser);
68      }
69      
70      private void init(Parser theParser) {
71          myBindings = new ArrayList<Binding>(20);
72          myParser = theParser;
73      }
74  
75      /** 
76       * @see ca.uhn.hl7v2.protocol.ApplicationRouter#processMessage(ca.uhn.hl7v2.protocol.Transportable)
77       */
78      public Transportable processMessage(Transportable theMessage) throws HL7Exception {
79          String[] result = processMessage(theMessage.getMessage(), theMessage.getMetadata());
80          Transportable response = new TransportableImpl(result[0]);
81          
82          if (result[1] != null) {
83              response.getMetadata().put(METADATA_KEY_MESSAGE_CHARSET, result[1]);
84          }
85          
86          return response;
87      }
88      
89      /**
90       * Processes an incoming message string and returns the response message string.
91       * Message processing consists of parsing the message, finding an appropriate
92       * Application and processing the message with it, and encoding the response.
93       * Applications are chosen from among those registered using
94       * <code>bindApplication</code>.  
95       * 
96       * @return {text, charset}
97       */
98      private String[] processMessage(String incomingMessageString, Map<String, Object> theMetadata) throws HL7Exception {
99          Logger rawOutbound = LoggerFactory.getLogger("ca.uhn.hl7v2.raw.outbound");
100         Logger rawInbound = LoggerFactory.getLogger("ca.uhn.hl7v2.raw.inbound");
101         
102         // TODO: add a way to register an application handler and
103         // invoke it any time something goes wrong
104         
105         log.debug( "ApplicationRouterImpl got message: {}", incomingMessageString );
106         rawInbound.debug(incomingMessageString);
107         
108         Message incomingMessageObject = null;
109         String outgoingMessageString = null;
110         String outgoingMessageCharset = null;
111         try {
112             incomingMessageObject = myParser.parse(incomingMessageString);
113             
114             Terser inTerser = new Terser(incomingMessageObject);
115             theMetadata.put(MetadataKeys.IN_MESSAGE_CONTROL_ID, inTerser.get("/.MSH-10"));
116             
117         }
118         catch (HL7Exception e) {
119 			try {
120 				 outgoingMessageString = logAndMakeErrorMessage(e, myParser.getCriticalResponseData(incomingMessageString), myParser, myParser.getEncoding(incomingMessageString));               
121 			} catch (HL7Exception e2) {
122 				 outgoingMessageString = null;
123 			}
124         	if (myExceptionHandler != null) {
125         		outgoingMessageString = myExceptionHandler.processException(incomingMessageString, theMetadata, outgoingMessageString, e);
126         		if (outgoingMessageString == null) {
127         			throw new HL7Exception("Application exception handler may not return null");
128         		}
129         	}
130         }
131         
132         if (outgoingMessageString == null) {
133             try {
134                 //optionally check integrity of parse
135                 String check = System.getProperty("ca.uhn.hl7v2.protocol.impl.check_parse");
136                 if (check != null && check.equals("TRUE")) {
137                     ParseChecker.checkParse(incomingMessageString, incomingMessageObject, myParser);
138                 }
139                 
140                 //message validation (in terms of optionality, cardinality) would go here ***
141                 
142                 ReceivingApplication app = findApplication(incomingMessageObject);
143                 theMetadata.put(RAW_MESSAGE_KEY, incomingMessageString);
144                 
145                 log.debug("Sending message to application: {}", app.toString());
146                 Message response = app.processMessage(incomingMessageObject, theMetadata);
147                 
148                 //Here we explicitly use the same encoding as that of the inbound message - this is important with GenericParser, which might use a different encoding by default
149                 outgoingMessageString = myParser.encode(response, myParser.getEncoding(incomingMessageString));
150                 
151                 Terser t = new Terser(response);
152                 outgoingMessageCharset = t.get(METADATA_KEY_MESSAGE_CHARSET); 
153             } catch (Exception e) {
154                 outgoingMessageString = handleProcessMessageException(incomingMessageString, theMetadata, incomingMessageObject, e);
155             } catch (Error e) {
156             	log.debug("Caught runtime exception of type {}, going to wrap it as HL7Exception and handle it", e.getClass());
157             	HL7Exception wrapped = new HL7Exception(e);
158             	outgoingMessageString = handleProcessMessageException(incomingMessageString, theMetadata, incomingMessageObject, wrapped);
159             }
160         }
161         
162         log.debug( "ApplicationRouterImpl sending message: {}", outgoingMessageString );
163         rawOutbound.debug(outgoingMessageString);
164         
165         return new String[] {outgoingMessageString, outgoingMessageCharset};
166     }
167 
168 	private String handleProcessMessageException(String incomingMessageString, Map<String, Object> theMetadata, Message incomingMessageObject, Exception e) throws HL7Exception {
169 		String outgoingMessageString;
170 		Segment inHeader = incomingMessageObject != null ? (Segment) incomingMessageObject.get("MSH") : null;
171 		outgoingMessageString = logAndMakeErrorMessage(e, inHeader, myParser, myParser.getEncoding(incomingMessageString));
172 		if (myExceptionHandler != null) {
173 			outgoingMessageString = myExceptionHandler.processException(incomingMessageString, theMetadata, outgoingMessageString, e);
174 		}
175 		return outgoingMessageString;
176 	}
177     
178 
179     /**
180      * @see ca.uhn.hl7v2.protocol.ApplicationRouter#hasActiveBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData)
181      */
182     public boolean hasActiveBinding(AppRoutingData theRoutingData) {
183         boolean result = false;
184         ReceivingApplication app = findDestination(null, theRoutingData);
185         if (app != null) {
186             result = true;
187         }
188         return result;
189     }
190     
191     /**
192      * @param theRoutingData
193      * @return the application from the binding with a WILDCARD match, if one exists
194      */
195     private ReceivingApplication findDestination(Message theMessage, AppRoutingData theRoutingData) {
196         ReceivingApplication result = null;
197         for (int i = 0; i < myBindings.size() && result == null; i++) {
198             Binding binding = (Binding) myBindings.get(i);
199             if (matches(theRoutingData, binding.routingData) && binding.active) {
200             	if (theMessage == null || binding.application.canProcess(theMessage)) {
201             		result = binding.application;
202             	}
203             }
204         }
205         return result;        
206     }
207     
208     /**
209      * @param theRoutingData
210      * @return the binding with an EXACT match on routing data if one exists 
211      */
212     private Binding findBinding(AppRoutingData theRoutingData) {
213         Binding result = null;
214         for (int i = 0; i < myBindings.size() && result == null; i++) {
215             Binding binding = (Binding) myBindings.get(i);
216             if ( theRoutingData.equals(binding.routingData) ) {
217                 result = binding;
218             }
219         }
220         return result;        
221         
222     }
223 
224     /** 
225      * @see ca.uhn.hl7v2.protocol.ApplicationRouter#bindApplication(
226      *      ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData, ca.uhn.hl7v2.protocol.ReceivingApplication)
227      */
228     public void bindApplication(AppRoutingData theRoutingData, ReceivingApplication theApplication) {
229         Binding binding = new Binding(theRoutingData, true, theApplication);
230         myBindings.add(binding);
231     }
232 
233     /** 
234      * @see ca.uhn.hl7v2.protocol.ApplicationRouter#disableBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData)
235      */
236     public void disableBinding(AppRoutingData theRoutingData) {
237         Binding b = findBinding(theRoutingData);
238         if (b != null) {
239             b.active = false;
240         }
241     }
242 
243     /** 
244      * @see ca.uhn.hl7v2.protocol.ApplicationRouter#enableBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData)
245      */
246     public void enableBinding(AppRoutingData theRoutingData) {
247         Binding b = findBinding(theRoutingData);
248         if (b != null) {
249             b.active = true;
250         }
251     }
252 
253     /**
254      * @see ca.uhn.hl7v2.protocol.ApplicationRouter#getParser()
255      */
256     public Parser getParser() {
257         return myParser;
258     }
259     
260     /**
261      * {@inheritDoc}
262      */
263     public void setExceptionHandler(ReceivingApplicationExceptionHandler theExceptionHandler) {
264     	this.myExceptionHandler = theExceptionHandler;
265     }
266 
267     /**
268      * @param theMessageData routing data related to a particular message
269      * @param theReferenceData routing data related to a binding, which may include 
270      *      wildcards 
271      * @param exact if true, each field must match exactly
272      * @return true if the message data is consist with the reference data, ie all 
273      *      values either match or are wildcards in the reference
274      */
275     public static boolean matches(AppRoutingData theMessageData, 
276                 AppRoutingData theReferenceData) {
277                     
278         boolean result = false;
279         
280         ApplicationRouter.AppRoutingData ref = theReferenceData;
281         ApplicationRouter.AppRoutingData msg = theMessageData;
282         
283         if (matches(msg.getMessageType(), ref.getMessageType()) 
284             && matches(msg.getTriggerEvent(), ref.getTriggerEvent()) 
285             && matches(msg.getProcessingId(), ref.getProcessingId()) 
286             && matches(msg.getVersion(), ref.getVersion())) {
287         
288             result = true;
289         }
290         
291         return result;        
292     }
293 
294     //support method for matches(AppRoutingData theMessageData, AppRoutingData theReferenceData)
295     private static boolean matches(String theMessageData, String theReferenceData) {
296         boolean result = false;
297 
298         String messageData = theMessageData;
299         if (messageData == null) {
300         	messageData = "";
301         }
302         
303 		if (messageData.equals(theReferenceData) || 
304                 theReferenceData.equals("*") || 
305                 Pattern.matches(theReferenceData, messageData)) {
306             result = true;
307         }
308         return result;
309     }
310     
311     /**
312      * Returns the first Application that has been bound to messages of this type.  
313      */
314     private ReceivingApplication findApplication(Message theMessage) throws HL7Exception {
315         Terser t = new Terser(theMessage);
316         AppRoutingData msgData = 
317             new AppRoutingDataImpl(t.get("/MSH-9-1"), t.get("/MSH-9-2"), t.get("/MSH-11-1"), t.get("/MSH-12"));
318             
319         ReceivingApplication app = findDestination(theMessage, msgData);
320         
321         //have to send back an application reject if no apps available to process
322         if (app == null) {
323             app = new DefaultApplication();
324         }
325         
326         return app;
327     }
328     
329     /**
330      * A structure for bindings between routing data and applications.  
331      */
332     private static class Binding {
333         public AppRoutingData routingData;
334         public boolean active;
335         public ReceivingApplication application;
336         
337         public Binding(AppRoutingData theRoutingData, boolean isActive, ReceivingApplication theApplication) {
338             routingData = theRoutingData;
339             active = isActive;
340             application = theApplication;
341         }
342     }
343     
344 	/**
345 	 * Logs the given exception and creates an error message to send to the
346 	 * remote system.
347 	 * 
348 	 * @param encoding
349 	 *            The encoding for the error message. If <code>null</code>, uses
350 	 *            default encoding
351 	 */
352 	public String logAndMakeErrorMessage(Exception e, Segment inHeader,
353 			Parser p, String encoding) throws HL7Exception {
354 
355 		log.error("Attempting to send error message to remote system.", e);
356 		
357 		HL7Exception hl7e = e instanceof HL7Exception ? 
358 				(HL7Exception) e :
359 				new HL7Exception(e.getMessage(), e);
360 
361 		// create error message ...
362 		String errorMessage = null;
363 		try {
364 			
365 			Message out;
366 			Message in;
367 			if (inHeader != null) {
368 				in = inHeader.getMessage();
369 				// the message may be a dummy message, whose MSH segment is incomplete
370 				DeepCopy.copy(inHeader, (Segment)in.get("MSH"));				
371 			} else {
372 				in = Version.highestAvailableVersionOrDefault().newGenericMessage(myParser.getFactory());
373 				((GenericMessage)in).initQuickstart("ACK", "", "");
374 			}
375 			
376 			out = in.generateACK(AcknowledgmentCode.AE, hl7e);
377 			
378 			if (encoding != null) {
379 				errorMessage = p.encode(out, encoding);
380 			} else {
381 				errorMessage = p.encode(out);
382 			}
383 
384 		} catch (IOException ioe) {
385 			throw new HL7Exception(
386 					"IOException creating error response message: "
387 							+ ioe.getMessage());
388 		}
389 		return errorMessage;
390 	}
391 
392 
393     
394 }