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