001/*
002The 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. 
004You may obtain a copy of the License at http://www.mozilla.org/MPL/ 
005Software distributed under the License is distributed on an "AS IS" basis, 
006WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the 
007specific language governing rights and limitations under the License. 
008
009The Original Code is "ProcessorImpl.java".  Description: 
010"A default implementation of Processor." 
011
012The Initial Developer of the Original Code is University Health Network. Copyright (C) 
0132004.  All Rights Reserved. 
014
015Contributor(s): ______________________________________. 
016
017Alternatively, the contents of this file may be used under the terms of the 
018GNU General Public License (the  �GPL�), in which case the provisions of the GPL are 
019applicable instead of those above.  If you wish to allow use of your version of this 
020file only under the terms of the GPL and not to allow others to use your version 
021of this file under the MPL, indicate your decision by deleting  the provisions above 
022and replace  them with the notice and other provisions required by the GPL License.  
023If you do not delete the provisions above, a recipient may use your version of 
024this file under either the MPL or the GPL. 
025*/
026
027package ca.uhn.hl7v2.protocol.impl;
028
029import java.util.HashMap;
030import java.util.Iterator;
031import java.util.Map;
032import java.util.concurrent.ExecutorService;
033import java.util.concurrent.Executors;
034
035import org.slf4j.Logger;
036import org.slf4j.LoggerFactory;
037
038import ca.uhn.hl7v2.HL7Exception;
039import ca.uhn.hl7v2.preparser.PreParser;
040import ca.uhn.hl7v2.protocol.Processor;
041import ca.uhn.hl7v2.protocol.ProcessorContext;
042import ca.uhn.hl7v2.protocol.TransportException;
043import ca.uhn.hl7v2.protocol.TransportLayer;
044import ca.uhn.hl7v2.protocol.Transportable;
045
046/**
047 * A default implementation of <code>Processor</code>.  
048 *  
049 * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
050 * @version $Revision: 1.4 $ updated on $Date: 2009-12-16 19:36:34 $ by $Author: jamesagnew $
051 */
052public class ProcessorImpl implements Processor {
053
054    private static final Logger log = LoggerFactory.getLogger(ProcessorImpl.class);
055
056    private ProcessorContext myContext;
057    private final Map<String, ExpiringTransportable> myAcceptAcks;
058    private final Map<String, Long> myReservations;
059    private final Map<String, ExpiringTransportable> myAvailableMessages;
060    private boolean myThreaded; //true if separate threads are calling cycle()  
061    private Cycler ackCycler;
062    private Cycler nonAckCycler;
063    private ExecutorService myResponseExecutorService;
064    
065    /**
066     * @param theContext source of supporting services 
067     * @param isThreaded true if this class should create threads in which to call cycle(), and 
068     *  in which to send responses from Applications.  This is the preferred mode.  Use false 
069     *  if threading is not allowed, eg you are running the code in an EJB container.  In this case, 
070     *  the send() and receive() methods will call cycle() themselves as needed.  However, cycle() 
071     *  makes potentially blocking calls, so these methods may not return until the next message 
072     *  is received from the remote server, regardless of timeout.  Probably the worst example of this
073     *  would be if receive() was called to wait for an application ACK that was specified as "RE" (ie
074     *  required on error).  No response will be returned if the message is processed without error, 
075     *  and in a non-threaded environment, receive() will block forever.  Use true if you can, otherwise
076     *  study this class carefully.
077     *   
078     * TODO: write a MLLPTransport with non-blocking IO  
079     * TODO: reconnect transport layers on error and retry 
080     */
081    public ProcessorImpl(ProcessorContext theContext, boolean isThreaded) {
082        myContext = theContext;
083        myThreaded = isThreaded;
084        myAcceptAcks = new HashMap<String, ExpiringTransportable>();
085        myReservations = new HashMap<String, Long>();
086        myAvailableMessages = new HashMap<String, ExpiringTransportable>();
087        
088        if (isThreaded) {
089            myResponseExecutorService = Executors.newSingleThreadExecutor(); 
090
091                ackCycler = new Cycler(this, true);
092            Thread ackThd = new Thread(ackCycler);
093            ackThd.start();
094            nonAckCycler = new Cycler(this, false);
095            Thread nonAckThd = new Thread(nonAckCycler);
096            nonAckThd.start();            
097        }
098    }
099    
100    /**
101     * If self-threaded, stops threads that have been created.  
102     */
103    public void stop() {
104        if (myThreaded) {
105            ackCycler.stop();
106            nonAckCycler.stop();
107
108            myResponseExecutorService.shutdownNow();
109        }
110    }
111
112    /**
113     * @see ca.uhn.hl7v2.protocol.Processor#send(ca.uhn.hl7v2.protocol.Transportable, int, long)
114     */
115    public void send(Transportable theMessage, int maxRetries, long retryIntervalMillis) throws HL7Exception {
116        String[] fieldPaths = {"MSH-10", "MSH-15", "MSH-16"};
117        String[] fields = PreParser.getFields(theMessage.getMessage(), fieldPaths);
118        String controlId = fields[0];
119        String needAcceptAck = fields[1];
120        String needAppAck = fields[2];
121        
122        checkValidAckNeededCode(needAcceptAck);
123        
124        trySend(myContext.getLocallyDrivenTransportLayer(), theMessage);
125        
126        boolean originalMode = (needAcceptAck == null && needAppAck == null); 
127        if (originalMode || !NE.equals(needAcceptAck)) {
128        
129            Transportable response = null;
130            int retries = 0;
131            do {
132                long until = System.currentTimeMillis() + retryIntervalMillis;
133                while (response == null && System.currentTimeMillis() < until) {
134                    synchronized (this) {
135                        ExpiringTransportable et = myAcceptAcks.remove(controlId);
136                        if (et == null) {
137                            cycleIfNeeded(true);
138                        } else {
139                            response = et.transportable;
140                        }
141                    }
142                    sleepIfNeeded();
143                }
144                
145                if ((response == null && needAcceptAck != null && needAcceptAck.equals(AL))
146                        || (response != null && isReject(response))) {
147                    log.info("Resending message {}", controlId);
148                    trySend(myContext.getLocallyDrivenTransportLayer(), theMessage);
149                    response = null;                    
150                }
151                
152                if (response != null && isError(response)) {
153                    String[] errMsgPath = {"MSA-3"};
154                    String[] errMsg = PreParser.getFields(response.getMessage(), errMsgPath);                    
155                    throw new HL7Exception("Error message received: " + errMsg[0]);
156                }
157                
158            } while (response == null && ++retries <= maxRetries);
159        }
160    }
161    
162    private void checkValidAckNeededCode(String theCode) throws HL7Exception {
163        //must be one of the below ... 
164        if ( !(theCode == null || theCode.equals("") 
165                ||theCode.equals(AL) || theCode.equals(ER) 
166                || theCode.equals(NE) || theCode.equals(SU)) ) {
167            throw new HL7Exception("MSH-15 must be AL, ER, NE, or SU in the outgoing message");
168        }            
169    }
170    
171    /**
172     * Calls cycle() if we do not expect another thread to be doing so
173     * @param expectingAck as in cycle
174     */
175    private void cycleIfNeeded(boolean expectingAck) throws HL7Exception {
176        if (!myThreaded) {
177            cycle(expectingAck);
178        }        
179    }
180    
181    /**
182     * Sleeps for 1 ms if externally threaded (this is to let the CPU idle).   
183     */
184    private void sleepIfNeeded() {
185        if (myThreaded) {
186            try {
187                Thread.sleep(1);
188            } catch (InterruptedException e) { /* no problem */ }
189        }                
190    }
191    
192    /** Returns true if a CR or AR ACK */ 
193    private static boolean isReject(Transportable theMessage) throws HL7Exception {
194        boolean reject = false;
195        String[] fieldPaths = {"MSA-1"};
196        String[] fields = PreParser.getFields(theMessage.getMessage(), fieldPaths);
197        if (fields[0] != null && (fields[0].equals(CR) || fields[0].equals(AR))) {
198            reject = true;
199        }        
200        return reject;
201    }
202
203    /** Returns true if a CE or AE ACK */ 
204    private static boolean isError(Transportable theMessage) throws HL7Exception {
205        boolean error = false;
206        String[] fieldPaths = {"MSA-1"};
207        String[] fields = PreParser.getFields(theMessage.getMessage(), fieldPaths);
208        if (fields[0] != null && (fields[0].equals(CE) || fields[0].equals(AE))) {
209            error = true;
210        }
211        return error;
212    }
213
214    /**
215     * @see ca.uhn.hl7v2.protocol.Processor#reserve(java.lang.String, long)
216     */
217    public synchronized void reserve(String theAckId, long thePeriodMillis) {
218        Long expiry = new Long(System.currentTimeMillis() + thePeriodMillis);
219        myReservations.put(theAckId, expiry);
220    }
221    
222    /**
223     * Tries to send the message, and if there is an error reconnects and tries again. 
224     */
225    private void trySend(TransportLayer theTransport, Transportable theTransportable) throws TransportException {
226        try {
227            theTransport.send(theTransportable);
228        } catch (TransportException e) {
229            theTransport.disconnect();
230            theTransport.connect();
231            theTransport.send(theTransportable);
232        }
233    }
234    
235    
236    /**
237     * Tries to receive a message, and if there is an error reconnects and tries again. 
238     */
239    private Transportable tryReceive(TransportLayer theTransport) throws TransportException {
240        Transportable message = null;
241        try {
242            message = theTransport.receive();            
243        } catch (TransportException e) {
244            theTransport.disconnect();
245            theTransport.connect();
246            message = theTransport.receive();
247        }
248        return message;
249    }
250
251    /** 
252     * @see ca.uhn.hl7v2.protocol.Processor#cycle(boolean)
253     */
254    public void cycle(boolean expectingAck) throws HL7Exception {
255        log.debug("In cycle()");
256        
257        cleanReservations();
258        cleanAcceptAcks();
259        cleanReservedMessages();
260
261        Transportable in = null;
262        try {
263            if (expectingAck) {
264                in = tryReceive(myContext.getLocallyDrivenTransportLayer());
265            } else {
266                in = tryReceive(myContext.getRemotelyDrivenTransportLayer());
267            }
268        } catch (TransportException e) {
269            try {
270                Thread.sleep(1000);
271            } catch (InterruptedException e1) {}
272            throw e;
273        }
274        
275        // log
276        if (in != null) {
277               log.debug("Received message: {}", in.getMessage());
278        } else {
279                log.debug("Received no message");
280        }
281        
282        // If we have a message, handle it
283        if (in != null) { 
284            String acceptAckNeeded = null;
285//            String appAckNeeded = null;
286            String ackCode = null;
287            String ackId = null;
288            
289            try {
290                    String[] fieldPaths = {"MSH-15", "MSH-16", "MSA-1", "MSA-2"};
291                    String[] fields = PreParser.getFields(in.getMessage(), fieldPaths);         
292                                acceptAckNeeded = fields[0];
293//                              appAckNeeded = fields[1];
294                                ackCode = fields[2];
295                                ackId = fields[3];
296            } catch (HL7Exception e) {
297                log.warn("Failed to parse accept ack fields in incoming message", e);
298            }
299            
300            if (ackId != null && ackCode != null && ackCode.startsWith("C")) {
301                long expiryTime = System.currentTimeMillis() + 1000 * 60;
302                myAcceptAcks.put(ackId, new ExpiringTransportable(in, expiryTime));
303            } else {
304                AcceptAcknowledger.AcceptACK ack = AcceptAcknowledger.validate(getContext(), in);
305            
306                if ((acceptAckNeeded != null && acceptAckNeeded.equals(AL)) 
307                    || (acceptAckNeeded != null && acceptAckNeeded.equals(ER) && !ack.isAcceptable()) 
308                    || (acceptAckNeeded != null && acceptAckNeeded.equals(SU) && ack.isAcceptable())) {
309                    trySend(myContext.getRemotelyDrivenTransportLayer(), ack.getMessage());    
310                }
311  
312                if (ack.isAcceptable()) {
313                    if (isReserved(ackId)) {
314                        
315                        log.debug("Received expected ACK message with ACK ID: {}", ackId);
316                        
317                        removeReservation(ackId);
318                        long expiryTime = System.currentTimeMillis() + 1000 * 60 * 5;                
319                        myAvailableMessages.put(ackId, new ExpiringTransportable(in, expiryTime));
320                        
321                    } else {
322
323                        log.debug("Sending message to router");
324                        Transportable out = myContext.getRouter().processMessage(in);
325                        sendAppResponse(out);
326                        
327                    }
328                } else {
329                        // TODO: should we do something more here? Might be nice to 
330                        // allow a configurable handler for this situation
331                        log.warn("Incoming message was not acceptable");
332                }
333                
334            }
335        } else {
336            String transport = expectingAck ? " Locally driven " : "Remotely driven";
337            log.debug("{} TransportLayer.receive() returned null.", transport);
338        }
339        
340        sleepIfNeeded();
341
342        log.debug("Exiting cycle()");
343    }
344    
345    /** Sends in a new thread if isThreaded, otherwise in current thread */
346    private void sendAppResponse(final Transportable theResponse) {
347        final ProcessorImpl processor = this;
348        Runnable sender = new Runnable() {
349            public void run() {
350                try {
351                        log.debug("Sending response: {}", theResponse);
352                        
353                    //TODO: make configurable 
354                        processor.send(theResponse, 2, 3000);
355                        
356                } catch (HL7Exception e) {
357                    log.error("Error trying to send response from Application", e);
358                }
359            }
360        };
361        
362        if (myThreaded) {
363            myResponseExecutorService.execute(sender);
364        } else {
365            sender.run();
366        }
367    }
368    
369    /**
370     * Removes expired message reservations from the reservation list.  
371     */
372    private synchronized void cleanReservations() {
373        Iterator<String> it = myReservations.keySet().iterator();
374        while (it.hasNext()) {
375            String ackId = it.next();
376            Long expiry = myReservations.get(ackId);
377            if (System.currentTimeMillis() > expiry.longValue()) {
378                it.remove();
379            }
380        }
381    }
382    
383    /**
384     * Discards expired accept acknowledgements (these are used in retry protocol; see send()).   
385     */
386    private synchronized void cleanAcceptAcks() {
387        Iterator<String> it = myAcceptAcks.keySet().iterator();
388        while (it.hasNext()) {
389            String ackId = it.next();
390            ExpiringTransportable et = myAcceptAcks.get(ackId);
391            if (System.currentTimeMillis() > et.expiryTime) {
392                it.remove();
393            }
394        }        
395    }
396    
397    private synchronized void cleanReservedMessages() throws HL7Exception {
398        Iterator<String> it = myAvailableMessages.keySet().iterator();
399        while (it.hasNext()) {
400            String ackId = it.next();            
401            ExpiringTransportable et = myAvailableMessages.get(ackId);
402            if (System.currentTimeMillis() > et.expiryTime) {
403                it.remove();
404                
405                //send to an Application 
406                Transportable out = myContext.getRouter().processMessage(et.transportable);
407                sendAppResponse(out);                
408            }
409        }  
410    }
411    
412    private synchronized boolean isReserved(String ackId) {
413        boolean reserved = false;
414        if (myReservations.containsKey(ackId)) {
415            reserved = true;
416        }
417        return reserved;
418    }
419    
420    private synchronized void removeReservation(String ackId) {
421        myReservations.remove(ackId);
422    }
423    
424
425    /**
426     * @see ca.uhn.hl7v2.protocol.Processor#isAvailable(java.lang.String)
427     */
428    public boolean isAvailable(String theAckId) {
429        boolean available = false;
430        if (myAvailableMessages.containsKey(theAckId)) {
431            available = true;
432        }
433        return available;
434    }
435
436    /** 
437     * @see ca.uhn.hl7v2.protocol.Processor#receive(java.lang.String, long)
438     */
439    public Transportable receive(String theAckId, long theTimeoutMillis) throws HL7Exception {
440        if (!isReserved(theAckId)) {
441            reserve(theAckId, theTimeoutMillis);
442        }
443        
444        Transportable in = null;
445        long until = System.currentTimeMillis() + theTimeoutMillis;
446        do {
447            synchronized (this) {
448                ExpiringTransportable et = myAvailableMessages.get(theAckId);                
449                if (et == null) {
450                    cycleIfNeeded(false);
451                } else {
452                    in = et.transportable;
453                }
454            }
455            sleepIfNeeded();
456        } while (in == null && System.currentTimeMillis() < until);
457        return in;
458    }
459
460    /** 
461     * @see ca.uhn.hl7v2.protocol.Processor#getContext()
462     */
463    public ProcessorContext getContext() {
464        return myContext;
465    }
466    
467    /**
468     * A struct for Transportable collection entries that time out.  
469     *  
470     * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
471     * @version $Revision: 1.4 $ updated on $Date: 2009-12-16 19:36:34 $ by $Author: jamesagnew $
472     */
473    class ExpiringTransportable {
474        public Transportable transportable;
475        public long expiryTime;
476        
477        public ExpiringTransportable(Transportable theTransportable, long theExpiryTime) {
478            transportable = theTransportable;
479            expiryTime = theExpiryTime;
480        }
481    }
482    
483    /**
484     * A Runnable that repeatedly calls the cycle() method of this class.  
485     * 
486     * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
487     * @version $Revision: 1.4 $ updated on $Date: 2009-12-16 19:36:34 $ by $Author: jamesagnew $
488     */
489    private static class Cycler implements Runnable {
490
491        private Processor myProcessor;
492        private boolean myExpectingAck;
493        private boolean isRunning;
494        
495        /**
496         * @param theProcessor the processor on which to call cycle()
497         * @param isExpectingAck passed to cycle()
498         */
499        public Cycler(Processor theProcessor, boolean isExpectingAck) {
500            myProcessor = theProcessor;
501            myExpectingAck = isExpectingAck;
502            isRunning = true;
503        }
504        
505        /**
506         * Execution will stop at the end of the next cycle.  
507         */
508        public void stop() {
509            isRunning = false;
510        }
511        
512        /** 
513         * Calls cycle() repeatedly on the Processor given in the 
514         * constructor, until stop() is called.  
515         * 
516         * @see java.lang.Runnable#run()
517         */
518        public void run() {
519            while (isRunning) {
520                try {
521                    myProcessor.cycle(myExpectingAck);
522                } catch (HL7Exception e) {
523                    log.error("Error processing message", e);
524                }
525            }
526        }        
527    }
528
529}