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