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}