001/* 002 * Created on 21-Apr-2004 003 */ 004package ca.uhn.hl7v2.protocol.impl; 005 006import java.io.IOException; 007import java.util.ArrayList; 008import java.util.List; 009import java.util.Map; 010import java.util.regex.Pattern; 011 012import ca.uhn.hl7v2.AcknowledgmentCode; 013import ca.uhn.hl7v2.HL7Exception; 014import ca.uhn.hl7v2.HapiContext; 015import ca.uhn.hl7v2.Version; 016import ca.uhn.hl7v2.app.DefaultApplication; 017import ca.uhn.hl7v2.model.GenericMessage; 018import ca.uhn.hl7v2.model.Message; 019import ca.uhn.hl7v2.model.Segment; 020import ca.uhn.hl7v2.parser.GenericParser; 021import ca.uhn.hl7v2.parser.Parser; 022import ca.uhn.hl7v2.protocol.*; 023import ca.uhn.hl7v2.util.DeepCopy; 024import ca.uhn.hl7v2.util.Terser; 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028/** 029 * <p>A default implementation of <code>ApplicationRouter</code> </p> 030 * 031 * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a> 032 * @version $Revision: 1.2 $ updated on $Date: 2009-09-01 00:22:23 $ by $Author: jamesagnew $ 033 */ 034public class ApplicationRouterImpl implements ApplicationRouter { 035 036 /** 037 * The default acknowledgment code used in MSA-1 when generating a NAK (negative ACK) message 038 * as a result of a processing exception. 039 */ 040 public static final AcknowledgmentCode DEFAULT_EXCEPTION_ACKNOWLEDGEMENT_CODE = AcknowledgmentCode.AE; 041 042 private static final Logger log = LoggerFactory.getLogger(ApplicationRouterImpl.class); 043 044 /** 045 * Key under which raw message text is stored in metadata Map sent to 046 * <code>ReceivingApplication</code>s. 047 */ 048 public static String RAW_MESSAGE_KEY = MetadataKeys.IN_RAW_MESSAGE; 049 050 private List<Binding> myBindings; 051 private Parser myParser; 052 private ReceivingApplicationExceptionHandler myExceptionHandler; 053 private HapiContext myContext; 054 055 056 /** 057 * Creates an instance that uses a <code>GenericParser</code>. 058 */ 059 @Deprecated 060 public ApplicationRouterImpl() { 061 this(new GenericParser()); 062 } 063 064 /** 065 * Creates an instance that uses the specified <code>Parser</code>. 066 * 067 * @param theParser the parser used for converting between Message and 068 * Transportable 069 */ 070 public ApplicationRouterImpl(Parser theParser) { 071 this(theParser.getHapiContext(), theParser); 072 } 073 074 public ApplicationRouterImpl(HapiContext theContext) { 075 this(theContext, theContext.getGenericParser()); 076 } 077 078 /** 079 * Creates an instance that uses the specified <code>Parser</code>. 080 * 081 * @param theContext HAPI context 082 * @param theParser the parser used for converting between Message and 083 * Transportable 084 */ 085 public ApplicationRouterImpl(HapiContext theContext, Parser theParser) { 086 init(theParser); 087 myContext = theContext; 088 } 089 090 private void init(Parser theParser) { 091 myBindings = new ArrayList<Binding>(20); 092 myParser = theParser; 093 } 094 095 /** 096 * @see ca.uhn.hl7v2.protocol.ApplicationRouter#processMessage(ca.uhn.hl7v2.protocol.Transportable) 097 */ 098 public Transportable processMessage(Transportable theMessage) throws HL7Exception { 099 String[] result = processMessage(theMessage.getMessage(), theMessage.getMetadata()); 100 Transportable response = new TransportableImpl(result[0]); 101 102 if (result[1] != null) { 103 response.getMetadata().put(METADATA_KEY_MESSAGE_CHARSET, result[1]); 104 } 105 106 return response; 107 } 108 109 /** 110 * Processes an incoming message string and returns the response message string. 111 * Message processing consists of parsing the message, finding an appropriate 112 * Application and processing the message with it, and encoding the response. 113 * Applications are chosen from among those registered using 114 * <code>bindApplication</code>. 115 * 116 * @return {text, charset} 117 */ 118 private String[] processMessage(String incomingMessageString, Map<String, Object> theMetadata) throws HL7Exception { 119 Logger rawOutbound = LoggerFactory.getLogger("ca.uhn.hl7v2.raw.outbound"); 120 Logger rawInbound = LoggerFactory.getLogger("ca.uhn.hl7v2.raw.inbound"); 121 122 // TODO: add a way to register an application handler and 123 // invoke it any time something goes wrong 124 125 log.debug("ApplicationRouterImpl got message: {}", incomingMessageString); 126 rawInbound.debug(incomingMessageString); 127 128 Message incomingMessageObject = null; 129 String outgoingMessageString = null; 130 String outgoingMessageCharset = null; 131 try { 132 incomingMessageObject = myParser.parse(incomingMessageString); 133 134 Terser inTerser = new Terser(incomingMessageObject); 135 theMetadata.put(MetadataKeys.IN_MESSAGE_CONTROL_ID, inTerser.get("/.MSH-10")); 136 137 } catch (HL7Exception e) { 138 try { 139 outgoingMessageString = logAndMakeErrorMessage(e, myParser.getCriticalResponseData(incomingMessageString), myParser, myParser.getEncoding(incomingMessageString)); 140 } catch (HL7Exception e2) { 141 outgoingMessageString = null; 142 } 143 if (myExceptionHandler != null) { 144 outgoingMessageString = myExceptionHandler.processException(incomingMessageString, theMetadata, outgoingMessageString, e); 145 if (outgoingMessageString == null) { 146 throw new HL7Exception("Application exception handler may not return null"); 147 } 148 } 149 } 150 151 // At this point, no exception has occurred and the message is processed normally 152 if (outgoingMessageString == null) { 153 try { 154 //optionally check integrity of parse 155 String check = System.getProperty("ca.uhn.hl7v2.protocol.impl.check_parse"); 156 if (check != null && check.equals("TRUE")) { 157 ParseChecker.checkParse(incomingMessageString, incomingMessageObject, myParser); 158 } 159 160 //message validation (in terms of optionality, cardinality) would go here *** 161 162 ReceivingApplication app = findApplication(incomingMessageObject); 163 theMetadata.put(RAW_MESSAGE_KEY, incomingMessageString); 164 165 log.debug("Sending message to application: {}", app.toString()); 166 Message response = app.processMessage(incomingMessageObject, theMetadata); 167 168 //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 169 outgoingMessageString = myParser.encode(response, myParser.getEncoding(incomingMessageString)); 170 171 Terser t = new Terser(response); 172 outgoingMessageCharset = t.get(METADATA_KEY_MESSAGE_CHARSET); 173 } catch (Exception e) { 174 outgoingMessageString = handleProcessMessageException(incomingMessageString, theMetadata, incomingMessageObject, e); 175 } catch (Error e) { 176 log.debug("Caught runtime exception of type {}, going to wrap it as HL7Exception and handle it", e.getClass()); 177 HL7Exception wrapped = new HL7Exception(e); 178 outgoingMessageString = handleProcessMessageException(incomingMessageString, theMetadata, incomingMessageObject, wrapped); 179 } 180 } 181 182 log.debug("ApplicationRouterImpl sending message: {}", outgoingMessageString); 183 rawOutbound.debug(outgoingMessageString); 184 185 return new String[]{outgoingMessageString, outgoingMessageCharset}; 186 } 187 188 private String handleProcessMessageException(String incomingMessageString, Map<String, Object> theMetadata, Message incomingMessageObject, Exception e) throws HL7Exception { 189 String outgoingMessageString; 190 Segment inHeader = incomingMessageObject != null ? (Segment) incomingMessageObject.get("MSH") : null; 191 outgoingMessageString = logAndMakeErrorMessage(e, inHeader, myParser, myParser.getEncoding(incomingMessageString)); 192 if (outgoingMessageString != null && myExceptionHandler != null) { 193 outgoingMessageString = myExceptionHandler.processException(incomingMessageString, theMetadata, outgoingMessageString, e); 194 } 195 return outgoingMessageString; 196 } 197 198 199 /** 200 * @see ca.uhn.hl7v2.protocol.ApplicationRouter#hasActiveBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData) 201 */ 202 public boolean hasActiveBinding(AppRoutingData theRoutingData) { 203 boolean result = false; 204 ReceivingApplication app = findDestination(null, theRoutingData); 205 if (app != null) { 206 result = true; 207 } 208 return result; 209 } 210 211 /** 212 * @param theMessage message for which a destination is looked up 213 * @param theRoutingData routing data 214 * @return the application from the binding with a WILDCARD match, if one exists 215 */ 216 private ReceivingApplication findDestination(Message theMessage, AppRoutingData theRoutingData) { 217 ReceivingApplication result = null; 218 for (int i = 0; i < myBindings.size() && result == null; i++) { 219 Binding binding = myBindings.get(i); 220 if (matches(theRoutingData, binding.routingData) && binding.active) { 221 if (theMessage == null || binding.application.canProcess(theMessage)) { 222 result = binding.application; 223 } 224 } 225 } 226 return result; 227 } 228 229 /** 230 * @param theRoutingData routing data 231 * @return the binding with an EXACT match on routing data if one exists 232 */ 233 private Binding findBinding(AppRoutingData theRoutingData) { 234 Binding result = null; 235 for (int i = 0; i < myBindings.size() && result == null; i++) { 236 Binding binding = myBindings.get(i); 237 if (theRoutingData.equals(binding.routingData)) { 238 result = binding; 239 } 240 } 241 return result; 242 243 } 244 245 /** 246 * @see ca.uhn.hl7v2.protocol.ApplicationRouter#bindApplication( 247 *ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData, ca.uhn.hl7v2.protocol.ReceivingApplication) 248 */ 249 public void bindApplication(AppRoutingData theRoutingData, ReceivingApplication theApplication) { 250 Binding binding = new Binding(theRoutingData, true, theApplication); 251 myBindings.add(binding); 252 } 253 254 /** 255 * @see ca.uhn.hl7v2.protocol.ApplicationRouter#disableBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData) 256 */ 257 public void disableBinding(AppRoutingData theRoutingData) { 258 Binding b = findBinding(theRoutingData); 259 if (b != null) { 260 b.active = false; 261 } 262 } 263 264 /** 265 * @see ca.uhn.hl7v2.protocol.ApplicationRouter#enableBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData) 266 */ 267 public void enableBinding(AppRoutingData theRoutingData) { 268 Binding b = findBinding(theRoutingData); 269 if (b != null) { 270 b.active = true; 271 } 272 } 273 274 /** 275 * @see ca.uhn.hl7v2.protocol.ApplicationRouter#getParser() 276 */ 277 public Parser getParser() { 278 return myParser; 279 } 280 281 /** 282 * {@inheritDoc} 283 */ 284 public void setExceptionHandler(ReceivingApplicationExceptionHandler theExceptionHandler) { 285 this.myExceptionHandler = theExceptionHandler; 286 } 287 288 /** 289 * @param theMessageData routing data related to a particular message 290 * @param theReferenceData routing data related to a binding, which may include 291 * wildcards 292 * @return true if the message data is consist with the reference data, ie all 293 * values either match or are wildcards in the reference 294 */ 295 public static boolean matches(AppRoutingData theMessageData, 296 AppRoutingData theReferenceData) { 297 298 boolean result = false; 299 300 if (matches(theMessageData.getMessageType(), theReferenceData.getMessageType()) 301 && matches(theMessageData.getTriggerEvent(), theReferenceData.getTriggerEvent()) 302 && matches(theMessageData.getProcessingId(), theReferenceData.getProcessingId()) 303 && matches(theMessageData.getVersion(), theReferenceData.getVersion())) { 304 305 result = true; 306 } 307 308 return result; 309 } 310 311 //support method for matches(AppRoutingData theMessageData, AppRoutingData theReferenceData) 312 private static boolean matches(String theMessageData, String theReferenceData) { 313 boolean result = false; 314 315 String messageData = theMessageData; 316 if (messageData == null) { 317 messageData = ""; 318 } 319 320 if (messageData.equals(theReferenceData) || 321 theReferenceData.equals("*") || 322 Pattern.matches(theReferenceData, messageData)) { 323 result = true; 324 } 325 return result; 326 } 327 328 /** 329 * Returns the first Application that has been bound to messages of this type. 330 */ 331 private ReceivingApplication findApplication(Message theMessage) throws HL7Exception { 332 Terser t = new Terser(theMessage); 333 AppRoutingData msgData = 334 new AppRoutingDataImpl(t.get("/MSH-9-1"), t.get("/MSH-9-2"), t.get("/MSH-11-1"), t.get("/MSH-12")); 335 336 ReceivingApplication app = findDestination(theMessage, msgData); 337 338 //have to send back an application reject if no apps available to process 339 if (app == null) { 340 app = new DefaultApplication(); 341 } 342 343 return app; 344 } 345 346 /** 347 * A structure for bindings between routing data and applications. 348 */ 349 private static class Binding { 350 public AppRoutingData routingData; 351 public boolean active; 352 public ReceivingApplication application; 353 354 public Binding(AppRoutingData theRoutingData, boolean isActive, ReceivingApplication theApplication) { 355 routingData = theRoutingData; 356 active = isActive; 357 application = theApplication; 358 } 359 } 360 361 /** 362 * Logs the given exception and creates an error message to send to the 363 * remote system. 364 * 365 * @param e exception 366 * @param inHeader MSH segment of incoming message 367 * @param p parser to be used 368 * @param encoding The encoding for the error message. If <code>null</code>, uses 369 * default encoding 370 * @return error message as string 371 * @throws ca.uhn.hl7v2.HL7Exception if an error occured during generation of the error message 372 */ 373 public String logAndMakeErrorMessage(Exception e, Segment inHeader, 374 Parser p, String encoding) throws HL7Exception { 375 376 switch (myContext.getServerConfiguration().getApplicationExceptionPolicy()) { 377 case DO_NOT_RESPOND: 378 log.error("Application exception detected, not going to send a response back to the client", e); 379 return null; 380 case DEFAULT: 381 default: 382 log.error("Attempting to send error message to remote system.", e); 383 break; 384 } 385 386 HL7Exception hl7e = e instanceof HL7Exception ? 387 (HL7Exception) e : 388 new HL7Exception(e.getMessage(), e); 389 390 try { 391 Message out = hl7e.getResponseMessage(); 392 if (out == null) { 393 Message in = getInMessage(inHeader); 394 out = in.generateACK(DEFAULT_EXCEPTION_ACKNOWLEDGEMENT_CODE, hl7e); 395 } 396 return encoding != null ? p.encode(out, encoding) : p.encode(out); 397 398 } catch (IOException ioe) { 399 throw new HL7Exception( 400 "IOException creating error response message: " 401 + ioe.getMessage()); 402 } 403 404 } 405 406 private Message getInMessage(Segment inHeader) throws HL7Exception, IOException { 407 Message in; 408 if (inHeader != null) { 409 in = inHeader.getMessage(); 410 // the message may be a dummy message, whose MSH segment is incomplete 411 DeepCopy.copy(inHeader, (Segment) in.get("MSH")); 412 } else { 413 in = Version.highestAvailableVersionOrDefault().newGenericMessage(myParser.getFactory()); 414 ((GenericMessage) in).initQuickstart("ACK", "", ""); 415 } 416 return in; 417 } 418 419 420}