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 package ca.uhn.hl7v2.app;
029
030 import java.io.BufferedReader;
031 import java.io.BufferedWriter;
032 import java.io.File;
033 import java.io.FileReader;
034 import java.io.FileWriter;
035 import java.io.IOException;
036 import java.io.PipedInputStream;
037 import java.io.PipedOutputStream;
038 import java.io.Reader;
039 import java.util.ArrayList;
040 import java.util.List;
041 import java.util.StringTokenizer;
042
043 import org.slf4j.Logger;
044 import org.slf4j.LoggerFactory;
045
046 import ca.uhn.hl7v2.HL7Exception;
047 import ca.uhn.hl7v2.llp.LLPException;
048 import ca.uhn.hl7v2.model.Message;
049 import ca.uhn.hl7v2.model.Segment;
050 import ca.uhn.hl7v2.parser.Parser;
051 import ca.uhn.hl7v2.parser.PipeParser;
052 import ca.uhn.hl7v2.util.MessageIDGenerator;
053 import 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 */
068 public 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 }