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    }