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