Coverage Report - ca.uhn.hl7v2.protocol.impl.ApplicationRouterImpl
 
Classes in this File Line Coverage Branch Coverage Complexity
ApplicationRouterImpl
89%
140/156
76%
65/85
2.923
ApplicationRouterImpl$1
100%
1/1
N/A
2.923
ApplicationRouterImpl$Binding
100%
5/5
N/A
2.923
 
 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  5
     public static final AcknowledgmentCode DEFAULT_EXCEPTION_ACKNOWLEDGEMENT_CODE = AcknowledgmentCode.AE;
 41  
 
 42  5
     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 HapiContext myContext;
 54  225
     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  75
         this(new GenericParser());
 63  75
     }
 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  220
         this(theParser.getHapiContext(), theParser);
 73  220
     }
 74  
 
 75  
     public ApplicationRouterImpl(HapiContext theContext) {
 76  5
         this(theContext, theContext.getGenericParser());
 77  5
     }
 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  225
     public ApplicationRouterImpl(HapiContext theContext, Parser theParser) {
 88  225
         init(theParser);
 89  225
         myContext = theContext;
 90  225
     }
 91  
 
 92  
     private void init(Parser theParser) {
 93  225
         myBindings = new ArrayList<Binding>(20);
 94  225
         myParser = theParser;
 95  225
     }
 96  
 
 97  
     public void setDefaultAcknowledgementMode(AcknowledgmentCode defaultAcknowledgementMode) {
 98  0
         this.defaultAcknowledgementMode = defaultAcknowledgementMode;
 99  0
     }
 100  
 
 101  
     /**
 102  
      * @see ca.uhn.hl7v2.protocol.ApplicationRouter#processMessage(ca.uhn.hl7v2.protocol.Transportable)
 103  
      */
 104  
     public Transportable processMessage(Transportable theMessage) throws HL7Exception {
 105  683
         String[] result = processMessage(theMessage.getMessage(), theMessage.getMetadata());
 106  683
         Transportable response = new TransportableImpl(result[0]);
 107  
 
 108  683
         if (result[1] != null) {
 109  5
             response.getMetadata().put(METADATA_KEY_MESSAGE_CHARSET, result[1]);
 110  
         }
 111  
 
 112  683
         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  683
         Logger rawOutbound = LoggerFactory.getLogger("ca.uhn.hl7v2.raw.outbound");
 126  683
         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  683
         log.debug("ApplicationRouterImpl got message: {}", incomingMessageString);
 132  683
         rawInbound.debug(incomingMessageString);
 133  
 
 134  683
         Message incomingMessageObject = null;
 135  683
         String outgoingMessageString = null;
 136  683
         String outgoingMessageCharset = null;
 137  
         try {
 138  683
             incomingMessageObject = myParser.parse(incomingMessageString);
 139  
 
 140  668
             Terser inTerser = new Terser(incomingMessageObject);
 141  668
             theMetadata.put(MetadataKeys.IN_MESSAGE_CONTROL_ID, inTerser.get("/.MSH-10"));
 142  
 
 143  15
         } catch (HL7Exception e) {
 144  15
             log.debug("Exception parsing incoming message", e);
 145  
             try {
 146  15
                 outgoingMessageString = logAndMakeErrorMessage(e, myParser.getCriticalResponseData(incomingMessageString), myParser, myParser.getEncoding(incomingMessageString));
 147  5
             } catch (HL7Exception e2) {
 148  5
                 log.error("Exception occurred while logging parse failure", e2);
 149  5
                 outgoingMessageString = null;
 150  10
             }
 151  15
             if (myExceptionHandler != null) {
 152  5
                 outgoingMessageString = myExceptionHandler.processException(incomingMessageString, theMetadata, outgoingMessageString, e);
 153  5
                 if (outgoingMessageString == null) {
 154  0
                     throw new HL7Exception("Application exception handler may not return null");
 155  
                 }
 156  
             }
 157  668
         }
 158  
 
 159  
         // At this point, no exception has occurred and the message is processed normally
 160  683
         if (outgoingMessageString == null) {
 161  
             try {
 162  
                 //optionally check integrity of parse
 163  668
                 String check = System.getProperty("ca.uhn.hl7v2.protocol.impl.check_parse");
 164  668
                 if (check != null && check.equals("TRUE")) {
 165  0
                     ParseChecker.checkParse(incomingMessageString, incomingMessageObject, myParser);
 166  
                 }
 167  
 
 168  
                 //message validation (in terms of optionality, cardinality) would go here ***
 169  
 
 170  668
                 ReceivingApplication<Message> app = findApplication(incomingMessageObject);
 171  668
                 theMetadata.put(RAW_MESSAGE_KEY, incomingMessageString);
 172  
 
 173  668
                 log.debug("Sending message to application: {}", app.toString());
 174  668
                 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  643
                 outgoingMessageString = myParser.encode(response, myParser.getEncoding(incomingMessageString));
 178  
 
 179  643
                 Terser t = new Terser(response);
 180  643
                 outgoingMessageCharset = t.get(METADATA_KEY_MESSAGE_CHARSET);
 181  15
             } catch (Exception e) {
 182  15
                 outgoingMessageString = handleProcessMessageException(incomingMessageString, theMetadata, incomingMessageObject, e);
 183  10
             } catch (Error e) {
 184  10
                 log.debug("Caught runtime exception of type {}, going to wrap it as HL7Exception and handle it", e.getClass());
 185  10
                 HL7Exception wrapped = new HL7Exception(e);
 186  10
                 outgoingMessageString = handleProcessMessageException(incomingMessageString, theMetadata, incomingMessageObject, wrapped);
 187  658
             }
 188  
         }
 189  
 
 190  683
         log.debug("ApplicationRouterImpl sending message: {}", outgoingMessageString);
 191  683
         rawOutbound.debug(outgoingMessageString);
 192  
 
 193  683
         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  25
         Segment inHeader = incomingMessageObject != null ? (Segment) incomingMessageObject.get("MSH") : null;
 199  25
         outgoingMessageString = logAndMakeErrorMessage(e, inHeader, myParser, myParser.getEncoding(incomingMessageString));
 200  25
         if (outgoingMessageString != null && myExceptionHandler != null) {
 201  0
             outgoingMessageString = myExceptionHandler.processException(incomingMessageString, theMetadata, outgoingMessageString, e);
 202  
         }
 203  25
         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  105
         boolean result = false;
 212  105
         ReceivingApplication<? extends Message> app = findDestination(null, theRoutingData);
 213  105
         if (app != null) {
 214  50
             result = true;
 215  
         }
 216  105
         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  773
         ReceivingApplication<? extends Message> result = null;
 226  1291
         for (int i = 0; i < myBindings.size() && result == null; i++) {
 227  518
             Binding binding = myBindings.get(i);
 228  518
             if (matches(theRoutingData, binding.routingData) && binding.active) {
 229  463
                 if (theMessage == null || ((ReceivingApplication<T>)binding.application).canProcess(theMessage)) {
 230  458
                     result = binding.application;
 231  
                 }
 232  
             }
 233  
         }
 234  773
         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  5
         Binding result = null;
 243  10
         for (int i = 0; i < myBindings.size() && result == null; i++) {
 244  5
             Binding binding = myBindings.get(i);
 245  5
             if (application == binding.application) {
 246  5
                 result = binding;
 247  
             }
 248  
         }
 249  5
         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  20
         Binding result = null;
 258  30
         for (int i = 0; i < myBindings.size() && result == null; i++) {
 259  10
             Binding binding = myBindings.get(i);
 260  10
             if (theRoutingData.equals(binding.routingData)) {
 261  10
                 result = binding;
 262  
             }
 263  
         }
 264  20
         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  180
         Binding binding = new Binding(theRoutingData, true, theApplication);
 274  180
         myBindings.add(binding);
 275  180
     }
 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  15
         Binding b = findBinding(theRoutingData);
 283  15
         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  5
         Binding b = findBinding(theApplication);
 291  5
         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  5
         Binding b = findBinding(theRoutingData);
 299  5
         if (b != null) {
 300  5
             b.active = false;
 301  
         }
 302  5
     }
 303  
 
 304  
     /**
 305  
      * @see ca.uhn.hl7v2.protocol.ApplicationRouter#enableBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData)
 306  
      */
 307  
     public void enableBinding(AppRoutingData theRoutingData) {
 308  0
         Binding b = findBinding(theRoutingData);
 309  0
         if (b != null) {
 310  0
             b.active = true;
 311  
         }
 312  0
     }
 313  
 
 314  
     /**
 315  
      * @see ca.uhn.hl7v2.protocol.ApplicationRouter#getParser()
 316  
      */
 317  
     public Parser getParser() {
 318  0
         return myParser;
 319  
     }
 320  
 
 321  
     /**
 322  
      * {@inheritDoc}
 323  
      */
 324  
     public void setExceptionHandler(ReceivingApplicationExceptionHandler theExceptionHandler) {
 325  5
         this.myExceptionHandler = theExceptionHandler;
 326  5
     }
 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  628
         boolean result = false;
 339  
 
 340  628
         if (matches(theMessageData.getMessageType(), theReferenceData.getMessageType())
 341  603
                 && matches(theMessageData.getTriggerEvent(), theReferenceData.getTriggerEvent())
 342  563
                 && matches(theMessageData.getProcessingId(), theReferenceData.getProcessingId())
 343  548
                 && matches(theMessageData.getVersion(), theReferenceData.getVersion())) {
 344  
 
 345  533
             result = true;
 346  
         }
 347  
 
 348  628
         return result;
 349  
     }
 350  
 
 351  
     //support method for matches(AppRoutingData theMessageData, AppRoutingData theReferenceData)
 352  
     private static boolean matches(String theMessageData, String theReferenceData) {
 353  2342
         boolean result = false;
 354  
 
 355  2342
         String messageData = theMessageData;
 356  2342
         if (messageData == null) {
 357  10
             messageData = "";
 358  
         }
 359  
 
 360  2342
         if (messageData.equals(theReferenceData) ||
 361  1817
                 theReferenceData.equals("*") ||
 362  145
                 Pattern.matches(theReferenceData, messageData)) {
 363  2247
             result = true;
 364  
         }
 365  2342
         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  668
         Terser t = new Terser(theMessage);
 373  668
         AppRoutingData msgData =
 374  668
                 new AppRoutingDataImpl(t.get("/MSH-9-1"), t.get("/MSH-9-2"), t.get("/MSH-11-1"), t.get("/MSH-12"));
 375  
 
 376  668
         ReceivingApplication<T> app = findDestination(theMessage, msgData);
 377  
 
 378  
         //have to send back an application reject if no apps available to process
 379  668
         if (app == null) {
 380  260
             app = (ReceivingApplication<T>)new DefaultApplication();
 381  
         }
 382  
 
 383  668
         return app;
 384  
     }
 385  
 
 386  
     /**
 387  
      * A structure for bindings between routing data and applications.
 388  
      */
 389  
     private static class Binding {
 390  
         public AppRoutingData routingData;
 391  
         public boolean active;
 392  
         public ReceivingApplication<? extends Message> application;
 393  
 
 394  180
         public Binding(AppRoutingData theRoutingData, boolean isActive, ReceivingApplication<? extends Message> theApplication) {
 395  180
             routingData = theRoutingData;
 396  180
             active = isActive;
 397  180
             application = theApplication;
 398  180
         }
 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  35
         switch (myContext.getServerConfiguration().getApplicationExceptionPolicy()) {
 417  
             case DO_NOT_RESPOND:
 418  10
                 log.error("Application exception detected, not going to send a response back to the client", e);
 419  10
                 return null;
 420  
             case DEFAULT:
 421  
             default:
 422  25
                 log.error("Attempting to send error message to remote system.", e);
 423  
                 break;
 424  
         }
 425  
 
 426  25
         HL7Exception hl7e = e instanceof HL7Exception ?
 427  
                 (HL7Exception) e :
 428  0
                 new HL7Exception(e.getMessage(), e);
 429  
 
 430  
         try {
 431  25
             Message out = hl7e.getResponseMessage();
 432  25
             if (out == null) {
 433  20
                 Message in = getInMessage(inHeader);
 434  20
                 out = in.generateACK(defaultAcknowledgementMode, hl7e);
 435  
             }
 436  25
             return encoding != null ? p.encode(out, encoding) : p.encode(out);
 437  
 
 438  0
         } catch (IOException ioe) {
 439  0
             throw new HL7Exception(
 440  
                     "IOException creating error response message: "
 441  0
                             + ioe.getMessage());
 442  
         }
 443  
 
 444  
     }
 445  
 
 446  
     private Message getInMessage(Segment inHeader) throws HL7Exception, IOException {
 447  
         Message in;
 448  20
         if (inHeader != null) {
 449  20
             in = inHeader.getMessage();
 450  
             // the message may be a dummy message, whose MSH segment is incomplete
 451  20
             DeepCopy.copy(inHeader, (Segment) in.get("MSH"));
 452  
         } else {
 453  0
             in = Version.highestAvailableVersionOrDefault().newGenericMessage(myParser.getFactory());
 454  0
             ((GenericMessage) in).initQuickstart("ACK", "", "");
 455  
         }
 456  20
         return in;
 457  
     }
 458  
 
 459  
 
 460  
 }