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    }