001/*
002 * Created on 21-Apr-2004
003 */
004package ca.uhn.hl7v2.protocol.impl;
005
006import java.io.IOException;
007import java.util.ArrayList;
008import java.util.List;
009import java.util.Map;
010import java.util.regex.Pattern;
011
012import ca.uhn.hl7v2.AcknowledgmentCode;
013import ca.uhn.hl7v2.HL7Exception;
014import ca.uhn.hl7v2.HapiContext;
015import ca.uhn.hl7v2.Version;
016import ca.uhn.hl7v2.app.DefaultApplication;
017import ca.uhn.hl7v2.model.GenericMessage;
018import ca.uhn.hl7v2.model.Message;
019import ca.uhn.hl7v2.model.Segment;
020import ca.uhn.hl7v2.parser.GenericParser;
021import ca.uhn.hl7v2.parser.Parser;
022import ca.uhn.hl7v2.protocol.*;
023import ca.uhn.hl7v2.util.DeepCopy;
024import ca.uhn.hl7v2.util.Terser;
025import org.slf4j.Logger;
026import org.slf4j.LoggerFactory;
027
028/**
029 * <p>A default implementation of <code>ApplicationRouter</code> </p>
030 *
031 * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
032 * @version $Revision: 1.2 $ updated on $Date: 2009-09-01 00:22:23 $ by $Author: jamesagnew $
033 */
034public class ApplicationRouterImpl implements ApplicationRouter {
035
036    /**
037     * The default acknowledgment code used in MSA-1 when generating a NAK (negative ACK) message
038     * as a result of a processing exception.
039     */
040    public static final AcknowledgmentCode DEFAULT_EXCEPTION_ACKNOWLEDGEMENT_CODE = AcknowledgmentCode.AE;
041
042    private static final Logger log = LoggerFactory.getLogger(ApplicationRouterImpl.class);
043
044    /**
045     * Key under which raw message text is stored in metadata Map sent to
046     * <code>ReceivingApplication</code>s.
047     */
048    public static final String RAW_MESSAGE_KEY = MetadataKeys.IN_RAW_MESSAGE;
049
050    private List<Binding> myBindings;
051    private Parser myParser;
052    private ReceivingApplicationExceptionHandler myExceptionHandler;
053    private HapiContext myContext;
054    private AcknowledgmentCode defaultAcknowledgementMode = DEFAULT_EXCEPTION_ACKNOWLEDGEMENT_CODE;
055
056
057    /**
058     * Creates an instance that uses a <code>GenericParser</code>.
059     */
060    @Deprecated
061    public ApplicationRouterImpl() {
062        this(new GenericParser());
063    }
064
065    /**
066     * Creates an instance that uses the specified <code>Parser</code>.
067     *
068     * @param theParser the parser used for converting between Message and
069     *                  Transportable
070     */
071    public ApplicationRouterImpl(Parser theParser) {
072        this(theParser.getHapiContext(), theParser);
073    }
074
075    public ApplicationRouterImpl(HapiContext theContext) {
076        this(theContext, theContext.getGenericParser());
077    }
078
079    /**
080     * Creates an instance that uses the specified <code>Parser</code>.
081     *
082     * @param theContext HAPI context
083     * @param theParser  the parser used for converting between Message and
084     *                   Transportable
085     * @deprecated define parser over context
086     */
087    public ApplicationRouterImpl(HapiContext theContext, Parser theParser) {
088        init(theParser);
089        myContext = theContext;
090    }
091
092    private void init(Parser theParser) {
093        myBindings = new ArrayList<Binding>(20);
094        myParser = theParser;
095    }
096
097    public void setDefaultAcknowledgementMode(AcknowledgmentCode defaultAcknowledgementMode) {
098        this.defaultAcknowledgementMode = defaultAcknowledgementMode;
099    }
100
101    /**
102     * @see ca.uhn.hl7v2.protocol.ApplicationRouter#processMessage(ca.uhn.hl7v2.protocol.Transportable)
103     */
104    public Transportable processMessage(Transportable theMessage) throws HL7Exception {
105        String[] result = processMessage(theMessage.getMessage(), theMessage.getMetadata());
106        Transportable response = new TransportableImpl(result[0]);
107
108        if (result[1] != null) {
109            response.getMetadata().put(METADATA_KEY_MESSAGE_CHARSET, result[1]);
110        }
111
112        return response;
113    }
114
115    /**
116     * Processes an incoming message string and returns the response message string.
117     * Message processing consists of parsing the message, finding an appropriate
118     * Application and processing the message with it, and encoding the response.
119     * Applications are chosen from among those registered using
120     * <code>bindApplication</code>.
121     *
122     * @return {text, charset}
123     */
124    private String[] processMessage(String incomingMessageString, Map<String, Object> theMetadata) throws HL7Exception {
125        Logger rawOutbound = LoggerFactory.getLogger("ca.uhn.hl7v2.raw.outbound");
126        Logger rawInbound = LoggerFactory.getLogger("ca.uhn.hl7v2.raw.inbound");
127
128        // TODO: add a way to register an application handler and
129        // invoke it any time something goes wrong
130
131        log.debug("ApplicationRouterImpl got message: {}", incomingMessageString);
132        rawInbound.debug(incomingMessageString);
133
134        Message incomingMessageObject = null;
135        String outgoingMessageString = null;
136        String outgoingMessageCharset = null;
137        try {
138            incomingMessageObject = myParser.parse(incomingMessageString);
139
140            Terser inTerser = new Terser(incomingMessageObject);
141            theMetadata.put(MetadataKeys.IN_MESSAGE_CONTROL_ID, inTerser.get("/.MSH-10"));
142
143        } catch (HL7Exception e) {
144            log.debug("Exception parsing incoming message", e);
145            try {
146                outgoingMessageString = logAndMakeErrorMessage(e, myParser.getCriticalResponseData(incomingMessageString), myParser, myParser.getEncoding(incomingMessageString));
147            } catch (HL7Exception e2) {
148                log.error("Exception occurred while logging parse failure", e2);
149                outgoingMessageString = null;
150            }
151            if (myExceptionHandler != null) {
152                outgoingMessageString = myExceptionHandler.processException(incomingMessageString, theMetadata, outgoingMessageString, e);
153                if (outgoingMessageString == null) {
154                    throw new HL7Exception("Application exception handler may not return null");
155                }
156            }
157        }
158
159        // At this point, no exception has occurred and the message is processed normally
160        if (outgoingMessageString == null) {
161            try {
162                //optionally check integrity of parse
163                String check = System.getProperty("ca.uhn.hl7v2.protocol.impl.check_parse");
164                if (check != null && check.equals("TRUE")) {
165                    ParseChecker.checkParse(incomingMessageString, incomingMessageObject, myParser);
166                }
167
168                //message validation (in terms of optionality, cardinality) would go here ***
169
170                ReceivingApplication<Message> app = findApplication(incomingMessageObject);
171                theMetadata.put(RAW_MESSAGE_KEY, incomingMessageString);
172
173                log.debug("Sending message to application: {}", app.toString());
174                Message response = app.processMessage(incomingMessageObject, theMetadata);
175
176                //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
177                outgoingMessageString = myParser.encode(response, myParser.getEncoding(incomingMessageString));
178
179                Terser t = new Terser(response);
180                outgoingMessageCharset = t.get(METADATA_KEY_MESSAGE_CHARSET);
181            } catch (Exception e) {
182                outgoingMessageString = handleProcessMessageException(incomingMessageString, theMetadata, incomingMessageObject, e);
183            } catch (Error e) {
184                log.debug("Caught runtime exception of type {}, going to wrap it as HL7Exception and handle it", e.getClass());
185                HL7Exception wrapped = new HL7Exception(e);
186                outgoingMessageString = handleProcessMessageException(incomingMessageString, theMetadata, incomingMessageObject, wrapped);
187            }
188        }
189
190        log.debug("ApplicationRouterImpl sending message: {}", outgoingMessageString);
191        rawOutbound.debug(outgoingMessageString);
192
193        return new String[]{outgoingMessageString, outgoingMessageCharset};
194    }
195
196    private String handleProcessMessageException(String incomingMessageString, Map<String, Object> theMetadata, Message incomingMessageObject, Exception e) throws HL7Exception {
197        String outgoingMessageString;
198        Segment inHeader = incomingMessageObject != null ? (Segment) incomingMessageObject.get("MSH") : null;
199        outgoingMessageString = logAndMakeErrorMessage(e, inHeader, myParser, myParser.getEncoding(incomingMessageString));
200        if (outgoingMessageString != null && myExceptionHandler != null) {
201            outgoingMessageString = myExceptionHandler.processException(incomingMessageString, theMetadata, outgoingMessageString, e);
202        }
203        return outgoingMessageString;
204    }
205
206
207    /**
208     * @see ca.uhn.hl7v2.protocol.ApplicationRouter#hasActiveBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData)
209     */
210    public boolean hasActiveBinding(AppRoutingData theRoutingData) {
211        boolean result = false;
212        ReceivingApplication<? extends Message> app = findDestination(null, theRoutingData);
213        if (app != null) {
214            result = true;
215        }
216        return result;
217    }
218
219    /**
220     * @param theMessage     message for which a destination is looked up
221     * @param theRoutingData routing data
222     * @return the application from the binding with a WILDCARD match, if one exists
223     */
224    private <T extends Message> ReceivingApplication<T> findDestination(T theMessage, AppRoutingData theRoutingData) {
225        ReceivingApplication<? extends Message> result = null;
226        for (int i = 0; i < myBindings.size() && result == null; i++) {
227            Binding binding = myBindings.get(i);
228            if (matches(theRoutingData, binding.routingData) && binding.active) {
229                if (theMessage == null || ((ReceivingApplication<T>)binding.application).canProcess(theMessage)) {
230                    result = binding.application;
231                }
232            }
233        }
234        return (ReceivingApplication<T>)result;
235    }
236
237    /**
238     * @param application receiving application
239     * @return the binding that forwards to the receiving application
240     */
241    private Binding findBinding(ReceivingApplication<? extends Message> application) {
242        Binding result = null;
243        for (int i = 0; i < myBindings.size() && result == null; i++) {
244            Binding binding = myBindings.get(i);
245            if (application == binding.application) {
246                result = binding;
247            }
248        }
249        return result;
250    }
251
252    /**
253     * @param theRoutingData routing data
254     * @return the binding with an EXACT match on routing data if one exists
255     */
256    private Binding findBinding(AppRoutingData theRoutingData) {
257        Binding result = null;
258        for (int i = 0; i < myBindings.size() && result == null; i++) {
259            Binding binding = myBindings.get(i);
260            if (theRoutingData.equals(binding.routingData)) {
261                result = binding;
262            }
263        }
264        return result;
265    }
266
267
268    /**
269     * @see ca.uhn.hl7v2.protocol.ApplicationRouter#bindApplication(
270     *ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData, ca.uhn.hl7v2.protocol.ReceivingApplication)
271     */
272    public void bindApplication(AppRoutingData theRoutingData, ReceivingApplication<? extends Message> theApplication) {
273        Binding binding = new Binding(theRoutingData, true, theApplication);
274        myBindings.add(binding);
275    }
276
277    /**
278     *
279     * @see ca.uhn.hl7v2.protocol.ApplicationRouter#unbindApplication(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData)
280     */
281    public boolean unbindApplication(AppRoutingData theRoutingData) {
282        Binding b = findBinding(theRoutingData);
283        return b != null && myBindings.remove(b);
284    }
285
286    /**
287     * @see ca.uhn.hl7v2.protocol.ApplicationRouter#unbindApplication(ca.uhn.hl7v2.protocol.ReceivingApplication)
288     */
289    public boolean unbindApplication(ReceivingApplication<? extends Message> theApplication) {
290        Binding b = findBinding(theApplication);
291        return b != null && myBindings.remove(b);
292    }
293
294    /**
295     * @see ca.uhn.hl7v2.protocol.ApplicationRouter#disableBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData)
296     */
297    public void disableBinding(AppRoutingData theRoutingData) {
298        Binding b = findBinding(theRoutingData);
299        if (b != null) {
300            b.active = false;
301        }
302    }
303
304    /**
305     * @see ca.uhn.hl7v2.protocol.ApplicationRouter#enableBinding(ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData)
306     */
307    public void enableBinding(AppRoutingData theRoutingData) {
308        Binding b = findBinding(theRoutingData);
309        if (b != null) {
310            b.active = true;
311        }
312    }
313
314    /**
315     * @see ca.uhn.hl7v2.protocol.ApplicationRouter#getParser()
316     */
317    public Parser getParser() {
318        return myParser;
319    }
320
321    /**
322     * {@inheritDoc}
323     */
324    public void setExceptionHandler(ReceivingApplicationExceptionHandler theExceptionHandler) {
325        this.myExceptionHandler = theExceptionHandler;
326    }
327
328    /**
329     * @param theMessageData   routing data related to a particular message
330     * @param theReferenceData routing data related to a binding, which may include
331     *                         wildcards
332     * @return true if the message data is consist with the reference data, ie all
333     * values either match or are wildcards in the reference
334     */
335    public static boolean matches(AppRoutingData theMessageData,
336                                  AppRoutingData theReferenceData) {
337
338        boolean result = false;
339
340        if (matches(theMessageData.getMessageType(), theReferenceData.getMessageType())
341                && matches(theMessageData.getTriggerEvent(), theReferenceData.getTriggerEvent())
342                && matches(theMessageData.getProcessingId(), theReferenceData.getProcessingId())
343                && matches(theMessageData.getVersion(), theReferenceData.getVersion())) {
344
345            result = true;
346        }
347
348        return result;
349    }
350
351    //support method for matches(AppRoutingData theMessageData, AppRoutingData theReferenceData)
352    private static boolean matches(String theMessageData, String theReferenceData) {
353        boolean result = false;
354
355        String messageData = theMessageData;
356        if (messageData == null) {
357            messageData = "";
358        }
359
360        if (messageData.equals(theReferenceData) ||
361                theReferenceData.equals("*") ||
362                Pattern.matches(theReferenceData, messageData)) {
363            result = true;
364        }
365        return result;
366    }
367
368    /**
369     * Returns the first Application that has been bound to messages of this type.
370     */
371    private <T extends Message> ReceivingApplication<T> findApplication(T theMessage) throws HL7Exception {
372        Terser t = new Terser(theMessage);
373        AppRoutingData msgData =
374                new AppRoutingDataImpl(t.get("/MSH-9-1"), t.get("/MSH-9-2"), t.get("/MSH-11-1"), t.get("/MSH-12"));
375
376        ReceivingApplication<T> app = findDestination(theMessage, msgData);
377
378        //have to send back an application reject if no apps available to process
379        if (app == null) {
380            app = (ReceivingApplication<T>)new DefaultApplication();
381        }
382
383        return app;
384    }
385
386    /**
387     * A structure for bindings between routing data and applications.
388     */
389    private static class Binding {
390        public AppRoutingData routingData;
391        public boolean active;
392        public ReceivingApplication<? extends Message> application;
393
394        public Binding(AppRoutingData theRoutingData, boolean isActive, ReceivingApplication<? extends Message> theApplication) {
395            routingData = theRoutingData;
396            active = isActive;
397            application = theApplication;
398        }
399    }
400
401    /**
402     * Logs the given exception and creates an error message to send to the
403     * remote system.
404     *
405     * @param e        exception
406     * @param inHeader MSH segment of incoming message
407     * @param p        parser to be used
408     * @param encoding The encoding for the error message. If <code>null</code>, uses
409     *                 default encoding
410     * @return error message as string
411     * @throws ca.uhn.hl7v2.HL7Exception if an error occured during generation of the error message
412     */
413    public String logAndMakeErrorMessage(Exception e, Segment inHeader,
414                                         Parser p, String encoding) throws HL7Exception {
415
416        switch (myContext.getServerConfiguration().getApplicationExceptionPolicy()) {
417            case DO_NOT_RESPOND:
418                log.error("Application exception detected, not going to send a response back to the client", e);
419                return null;
420            case DEFAULT:
421            default:
422                log.error("Attempting to send error message to remote system.", e);
423                break;
424        }
425
426        HL7Exception hl7e = e instanceof HL7Exception ?
427                (HL7Exception) e :
428                new HL7Exception(e.getMessage(), e);
429
430        try {
431            Message out = hl7e.getResponseMessage();
432            if (out == null) {
433                Message in = getInMessage(inHeader);
434                out = in.generateACK(defaultAcknowledgementMode, hl7e);
435            }
436            return encoding != null ? p.encode(out, encoding) : p.encode(out);
437
438        } catch (IOException ioe) {
439            throw new HL7Exception(
440                    "IOException creating error response message: "
441                            + ioe.getMessage());
442        }
443
444    }
445
446    private Message getInMessage(Segment inHeader) throws HL7Exception, IOException {
447        Message in;
448        if (inHeader != null) {
449            in = inHeader.getMessage();
450            // the message may be a dummy message, whose MSH segment is incomplete
451            DeepCopy.copy(inHeader, (Segment) in.get("MSH"));
452        } else {
453            in = Version.highestAvailableVersionOrDefault().newGenericMessage(myParser.getFactory());
454            ((GenericMessage) in).initQuickstart("ACK", "", "");
455        }
456        return in;
457    }
458
459
460}