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