001/** 002 * The contents of this file are subject to the Mozilla Public License Version 1.1 003 * (the "License"); you may not use this file except in compliance with the License. 004 * You may obtain a copy of the License at http://www.mozilla.org/MPL/ 005 * Software distributed under the License is distributed on an "AS IS" basis, 006 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 007 * specific language governing rights and limitations under the License. 008 * 009 * The Original Code is "Responder.java". Description: 010 * "Performs the responding role in a message exchange according to HL7's original mode 011 * processing rules." 012 * 013 * The Initial Developer of the Original Code is University Health Network. Copyright (C) 014 * 2002. All Rights Reserved. 015 * 016 * Contributor(s): ______________________________________. 017 * 018 * Alternatively, the contents of this file may be used under the terms of the 019 * GNU General Public License (the �GPL�), in which case the provisions of the GPL are 020 * applicable instead of those above. If you wish to allow use of your version of this 021 * file only under the terms of the GPL and not to allow others to use your version 022 * of this file under the MPL, indicate your decision by deleting the provisions above 023 * and replace them with the notice and other provisions required by the GPL License. 024 * If you do not delete the provisions above, a recipient may use your version of 025 * this file under either the MPL or the GPL. 026 * 027 */ 028package ca.uhn.hl7v2.app; 029 030import java.io.BufferedReader; 031import java.io.BufferedWriter; 032import java.io.File; 033import java.io.FileReader; 034import java.io.FileWriter; 035import java.io.IOException; 036import java.io.PipedInputStream; 037import java.io.PipedOutputStream; 038import java.io.Reader; 039import java.util.ArrayList; 040import java.util.List; 041import java.util.StringTokenizer; 042 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045 046import ca.uhn.hl7v2.HL7Exception; 047import ca.uhn.hl7v2.llp.LLPException; 048import ca.uhn.hl7v2.model.Message; 049import ca.uhn.hl7v2.model.Segment; 050import ca.uhn.hl7v2.parser.Parser; 051import ca.uhn.hl7v2.parser.PipeParser; 052import ca.uhn.hl7v2.util.MessageIDGenerator; 053import ca.uhn.hl7v2.util.Terser; 054 055/** 056 * <p> 057 * Performs the responding role in a message exchange (i.e receiver of the first 058 * message, sender of the response; analagous to the server in a client-server 059 * interaction), according to HL7's original mode processing rules. 060 * </p> 061 * <p> 062 * At the time of writing, enhanced mode, two-phase reply, continuation 063 * messages, and batch processing are unsupported. 064 * </p> 065 * 066 * @author Bryan Tripp 067 */ 068public class Responder { 069 070 private static final Logger log = LoggerFactory.getLogger(Responder.class); 071 072 // private LowerLayerProtocol llp; 073 private Parser parser; 074 private List<Application> apps; 075 private BufferedWriter checkWriter = null; 076 077 /** 078 * Creates a new instance of Responder that optionally validates parsing of 079 * incoming messages using a system property. If the system property 080 * <code>ca.uhn.hl7v2.app.checkparse</code> equals "true", parse integrity 081 * is checked, i.e. each message is re-encoded and differences between the 082 * received message text and the re-encoded text are written to the file 083 * <hapi.home>/parse_check.txt. 084 */ 085 public Responder(Parser parser) throws LLPException { 086 String checkParse = System.getProperty("ca.uhn.hl7v2.app.checkparse"); 087 if (checkParse != null && checkParse.equals("true")) { 088 init(parser, true); 089 } else { 090 init(parser, false); 091 } 092 } 093 094 /** 095 * Creates a new instance of Responder that optionally validates parsing of 096 * incoming messages. 097 * 098 * @param validate 099 * if true, encodes each incoming message after parsing it, 100 * compares the result to the original message string, and prints 101 * differences to the file "<hapi.home>/parse_check.txt" in the 102 * working directory. This process is slow and should only be 103 * used during testing. 104 */ 105 public Responder(Parser parser, boolean checkParse) { 106 init(parser, checkParse); 107 } 108 109 /** 110 * Performs common constructor tasks. 111 */ 112 private void init(Parser parser, boolean checkParse) { 113 this.parser = parser; 114 apps = new ArrayList<Application>(10); 115 try { 116 if (checkParse) 117 checkWriter = new BufferedWriter(new FileWriter( 118 ca.uhn.hl7v2.util.Home.getHomeDirectory() 119 .getAbsolutePath() + "/parse_check.txt", true)); 120 } catch (IOException e) { 121 log.error( 122 "Unable to open file to write parse check results. Parse integrity checks will not proceed", 123 e); 124 } 125 } 126 127 /** 128 * Processes an incoming message string and returns the response message 129 * string. Message processing consists of parsing the message, finding an 130 * appropriate Application and processing the message with it, and encoding 131 * the response. Applications are chosen from among those registered using 132 * <code>registerApplication</code>. The Parser is obtained from the 133 * Connection associated with this Responder. 134 */ 135 protected String processMessage(String incomingMessageString) 136 throws HL7Exception { 137 Logger rawOutbound = LoggerFactory 138 .getLogger("ca.uhn.hl7v2.raw.outbound"); 139 Logger rawInbound = LoggerFactory.getLogger("ca.uhn.hl7v2.raw.inbound"); 140 141 log.debug("Responder got message: {}", incomingMessageString); 142 rawInbound.debug(incomingMessageString); 143 144 Message incomingMessageObject = null; 145 String outgoingMessageString = null; 146 try { 147 incomingMessageObject = parser.parse(incomingMessageString); 148 } catch (HL7Exception e) { 149 // TODO this may also throw an Exception, which hides the 150 // previous one. 151 outgoingMessageString = logAndMakeErrorMessage(e, 152 parser.getCriticalResponseData(incomingMessageString), 153 parser, parser.getEncoding(incomingMessageString)); 154 for (Object app : apps) { 155 if (app instanceof ApplicationExceptionHandler) { 156 ApplicationExceptionHandler aeh = (ApplicationExceptionHandler) app; 157 outgoingMessageString = aeh.processException( 158 incomingMessageString, outgoingMessageString, e); 159 } 160 } 161 } 162 163 if (outgoingMessageString == null) { 164 try { 165 // optionally check integrity of parse 166 try { 167 if (checkWriter != null) 168 checkParse(incomingMessageString, 169 incomingMessageObject, parser); 170 } catch (IOException e) { 171 log.error("Unable to write parse check results to file", e); 172 } 173 174 // message validation (in terms of optionality, cardinality) 175 // would go here *** 176 177 Application app = findApplication(incomingMessageObject); 178 Message response = app.processMessage(incomingMessageObject); 179 180 if (response == null) { 181 throw new HL7Exception("Application of type " + app.getClass().getName() + " failed to return a response message from 'processMessage'"); 182 } 183 184 // Here we explicitly use the same encoding as that of the 185 // inbound message - this is important with GenericParser, which 186 // might use a different encoding by default 187 outgoingMessageString = parser.encode(response, 188 parser.getEncoding(incomingMessageString)); 189 } catch (Exception e) { 190 outgoingMessageString = logAndMakeErrorMessage(e, 191 (Segment) incomingMessageObject.get("MSH"), parser, 192 parser.getEncoding(incomingMessageString)); 193 } 194 } 195 196 log.debug("Responder sending message: {}", outgoingMessageString); 197 rawOutbound.debug(outgoingMessageString); 198 199 return outgoingMessageString; 200 } 201 202 /** 203 * Returns the first Application that has been registered, which can process 204 * the given Message (according to its canProcess() method). If none is 205 * found, returns the DefaultApplication that always NAKs. 206 */ 207 private Application findApplication(Message message) { 208 Application app = new DefaultApplication(); 209 for (Application a : apps) { 210 if (a.canProcess(message)) { 211 app = a; 212 break; 213 } 214 } 215 return app; 216 } 217 218 /** 219 * Encodes the given message and compares it to the given string. Any 220 * differences are noted in the file [hapi.home]/parse_check.txt. Ignores 221 * extra field delimiters. 222 */ 223 private void checkParse(String originalMessageText, Message parsedMessage, 224 Parser parser) throws HL7Exception, IOException { 225 log.info("ca.uhn.hl7v2.app.Responder is checking parse integrity (turn this off if you are not testing)"); 226 String newMessageText = parser.encode(parsedMessage); 227 228 checkWriter 229 .write("******************* Comparing Messages ****************\r\n"); 230 checkWriter 231 .write("Original: " + originalMessageText + "\r\n"); 232 checkWriter.write("Parsed and Encoded: " + newMessageText + "\r\n"); 233 234 if (!originalMessageText.equals(newMessageText)) { 235 // check each segment 236 StringTokenizer tok = new StringTokenizer(originalMessageText, "\r"); 237 List<String> one = new ArrayList<String>(); 238 while (tok.hasMoreTokens()) { 239 String seg = tok.nextToken(); 240 if (seg.length() > 4) 241 one.add(seg); 242 } 243 tok = new StringTokenizer(newMessageText, "\r"); 244 List<String> two = new ArrayList<String>(); 245 while (tok.hasMoreTokens()) { 246 String seg = tok.nextToken(); 247 if (seg.length() > 4) 248 two.add(stripExtraDelimiters(seg, seg.charAt(3))); 249 } 250 251 if (one.size() != two.size()) { 252 checkWriter 253 .write("Warning: inbound and parsed messages have different numbers of segments: \r\n"); 254 checkWriter.write("Original: " + originalMessageText + "\r\n"); 255 checkWriter.write("Parsed: " + newMessageText + "\r\n"); 256 } else { 257 // check each segment 258 for (int i = 0; i < one.size(); i++) { 259 String origSeg = one.get(i); 260 String newSeg = two.get(i); 261 if (!origSeg.equals(newSeg)) { 262 checkWriter 263 .write("Warning: inbound and parsed message segment differs: \r\n"); 264 checkWriter.write("Original: " + origSeg + "\r\n"); 265 checkWriter.write("Parsed: " + newSeg + "\r\n"); 266 } 267 } 268 } 269 } else { 270 checkWriter.write("No differences found\r\n"); 271 } 272 273 checkWriter 274 .write("******************** End Comparison ******************\r\n"); 275 checkWriter.flush(); 276 277 } 278 279 /** 280 * Removes unecessary delimiters from the end of a field or segment. This is 281 * cut-and-pasted from PipeParser (just making it public in PipeParser would 282 * kind of cloud the purpose of PipeParser). 283 */ 284 private static String stripExtraDelimiters(String in, char delim) { 285 char[] chars = in.toCharArray(); 286 287 // search from back end for first occurance of non-delimiter ... 288 int c = chars.length - 1; 289 boolean found = false; 290 while (c >= 0 && !found) { 291 if (chars[c--] != delim) 292 found = true; 293 } 294 295 String ret = ""; 296 if (found) 297 ret = String.valueOf(chars, 0, c + 2); 298 return ret; 299 } 300 301 /** 302 * Logs the given exception and creates an error message to send to the 303 * remote system. 304 * 305 * @param encoding 306 * The encoding for the error message. If <code>null</code>, uses 307 * default encoding 308 */ 309 public static String logAndMakeErrorMessage(Exception e, Segment inHeader, 310 Parser p, String encoding) throws HL7Exception { 311 312 log.error("Attempting to send error message to remote system.", e); 313 314 // create error message ... 315 String errorMessage = null; 316 try { 317 Message out = DefaultApplication.makeACK(inHeader); 318 Terser t = new Terser(out); 319 320 // copy required data from incoming message ... 321 try { 322 t.set("/MSH-10", MessageIDGenerator.getInstance().getNewID()); 323 } catch (IOException ioe) { 324 throw new HL7Exception("Problem creating error message ID: " 325 + ioe.getMessage()); 326 } 327 328 // populate MSA ... 329 t.set("/MSA-1", "AE"); // should this come from HL7Exception 330 // constructor? 331 t.set("/MSA-2", Terser.get(inHeader, 10, 0, 1, 1)); 332 String excepMessage = e.getMessage(); 333 if (excepMessage != null) 334 t.set("/MSA-3", 335 excepMessage.substring(0, 336 Math.min(80, excepMessage.length()))); 337 338 /* 339 * Some earlier ACKs don't have ERRs, but I think we'll change this 340 * within HAPI so that there is a single ACK for each version (with 341 * an ERR). 342 */ 343 // see if it's an HL7Exception (so we can get specific information) 344 // ... 345 if (e.getClass().equals(HL7Exception.class)) { 346// Segment err = (Segment) out.get("ERR"); 347 // ((HL7Exception) e).populate(err); // FIXME: this is broken, 348 // it relies on the database in a place where it's not available 349 } else { 350 t.set("/ERR-1-4-1", "207"); 351 t.set("/ERR-1-4-2", "Application Internal Error"); 352 t.set("/ERR-1-4-3", "HL70357"); 353 } 354 355 if (encoding != null) { 356 errorMessage = p.encode(out, encoding); 357 } else { 358 errorMessage = p.encode(out); 359 } 360 361 } catch (IOException ioe) { 362 throw new HL7Exception( 363 "IOException creating error response message: " 364 + ioe.getMessage(), 365 HL7Exception.APPLICATION_INTERNAL_ERROR); 366 } 367 return errorMessage; 368 } 369 370 /** 371 * Registers a message parser/encoder with this responder. If multiple 372 * parsers are registered, each message is inspected by each parser in the 373 * order in which they are registered, until one parser recognizes the 374 * format and parses the message. 375 */ 376 /* 377 * public void registerParser(Parser p) { this.parsers.add(p); } 378 */ 379 380 /** 381 * Registers an Application with this Responder. The "Application", in this 382 * context, is the software that uses the information in the message. If 383 * multiple applications are registered, incoming Message objects will be 384 * passed to each one in turn (calling <code>canProcess()</code>) until one 385 * of them accepts responsibility for the message. If none of the registered 386 * applications can process the message, a DefaultApplication is used, which 387 * simply returns an Application Reject message. 388 */ 389 public void registerApplication(Application a) { 390 this.apps.add(a); 391 } 392 393 /** 394 * Test code. 395 */ 396 @SuppressWarnings("unused") 397 public static void main(String args[]) { 398 if (args.length != 1) { 399 System.err.println("Usage: DefaultApplication message_file"); 400 System.exit(1); 401 } 402 403 // read test message file ... 404 try { 405 File messageFile = new File(args[0]); 406 Reader in = new BufferedReader(new FileReader(messageFile)); 407 int fileLength = (int) messageFile.length(); 408 char[] cbuf = new char[fileLength]; 409 in.read(cbuf, 0, fileLength); 410 String messageString = new String(cbuf); 411 412 // parse inbound message ... 413 final Parser parser = new PipeParser(); 414 Message inMessage = null; 415 try { 416 inMessage = parser.parse(messageString); 417 } catch (HL7Exception e) { 418 e.printStackTrace(); 419 } 420 421 // process with responder ... 422 PipedInputStream initInbound = new PipedInputStream(); 423 PipedOutputStream initOutbound = new PipedOutputStream(); 424 PipedInputStream respInbound = new PipedInputStream(initOutbound); 425 PipedOutputStream respOutbound = new PipedOutputStream(initInbound); 426 427 /* 428 * This code won't work with new changes: final Initiator init = new 429 * Initiator(parser, new MinLowerLayerProtocol(), initInbound, 430 * initOutbound); Responder resp = new Responder(respInbound, 431 * respOutbound); 432 * 433 * //run the initiator in a separate thread ... final Message 434 * inMessCopy = inMessage; Thread initThd = new Thread(new 435 * Runnable() { public void run() { try { Message response = 436 * init.sendAndReceive(inMessCopy); 437 * System.out.println("This is initiator writing response ..."); 438 * System.out.println(parser.encode(response)); } catch (Exception 439 * ie) { if (HL7Exception.class.isAssignableFrom(ie.getClass())) { 440 * System.out.println("Error in segment " + 441 * ((HL7Exception)ie).getSegmentName() + " field " + 442 * ((HL7Exception)ie).getFieldPosition()); } ie.printStackTrace(); } 443 * } }); initThd.start(); 444 * 445 * //process the message we expect from the initiator thread ... 446 * System.out.println("Responder is going to respond now ..."); 447 * resp.processOneMessage(); 448 */ 449 } catch (Exception e) { 450 e.printStackTrace(); 451 } 452 453 } 454 455}