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 final 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 final 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<>();
085        myReservations = new HashMap<>();
086        myAvailableMessages = new HashMap<>();
087        
088        if (isThreaded) {
089            myResponseExecutorService = Executors.newSingleThreadExecutor(); 
090
091            TransportLayer local = theContext.getLocallyDrivenTransportLayer();
092            TransportLayer remote = theContext.getRemotelyDrivenTransportLayer();
093            
094                ackCycler = new Cycler(this, true);
095            Thread ackThd = new Thread(ackCycler);
096            ackThd.start();
097            
098            if (local != remote) {
099                    nonAckCycler = new Cycler(this, false);
100                    Thread nonAckThd = new Thread(nonAckCycler);
101                    nonAckThd.start();
102            }
103            
104        }
105    }
106    
107    /**
108     * If self-threaded, stops threads that have been created.  
109     */
110    public void stop() {
111        if (myThreaded) {
112            ackCycler.stop();
113            if (nonAckCycler != null) {
114                nonAckCycler.stop();
115            }
116            
117            myResponseExecutorService.shutdownNow();
118        }
119    }
120
121    /**
122     * @see ca.uhn.hl7v2.protocol.Processor#send(ca.uhn.hl7v2.protocol.Transportable, int, long)
123     */
124    public void send(Transportable theMessage, int maxRetries, long retryIntervalMillis) throws HL7Exception {
125        String[] fieldPaths = {"MSH-10", "MSH-15", "MSH-16"};
126        String[] fields = PreParser.getFields(theMessage.getMessage(), fieldPaths);
127        String controlId = fields[0];
128        String needAcceptAck = fields[1];
129        String needAppAck = fields[2];
130        
131        checkValidAckNeededCode(needAcceptAck);
132        
133        trySend(myContext.getLocallyDrivenTransportLayer(), theMessage);
134        
135        boolean originalMode = (needAcceptAck == null && needAppAck == null); 
136        if (originalMode || !NE.equals(needAcceptAck)) {
137        
138            Transportable response = null;
139            int retries = 0;
140            do {
141                long until = System.currentTimeMillis() + retryIntervalMillis;
142                while (response == null && System.currentTimeMillis() < until) {
143                    synchronized (this) {
144                        ExpiringTransportable et = myAcceptAcks.remove(controlId);
145                        if (et == null) {
146                            cycleIfNeeded(true);
147                        } else {
148                            response = et.transportable;
149                        }
150                    }
151                    sleepIfNeeded();
152                }
153                
154                if ((response == null && needAcceptAck != null && needAcceptAck.equals(AL))
155                        || (response != null && isReject(response))) {
156                    log.info("Resending message {}", controlId);
157                    trySend(myContext.getLocallyDrivenTransportLayer(), theMessage);
158                    response = null;                    
159                }
160                
161                if (response != null && isError(response)) {
162                    String[] errMsgPath = {"MSA-3"};
163                    String[] errMsg = PreParser.getFields(response.getMessage(), errMsgPath);                    
164                    throw new HL7Exception("Error message received: " + errMsg[0]);
165                }
166                
167            } while (response == null && ++retries <= maxRetries);
168        }
169    }
170    
171    private void checkValidAckNeededCode(String theCode) throws HL7Exception {
172        //must be one of the below ... 
173        if ( !(theCode == null || theCode.equals("") 
174                ||theCode.equals(AL) || theCode.equals(ER) 
175                || theCode.equals(NE) || theCode.equals(SU)) ) {
176            throw new HL7Exception("MSH-15 must be AL, ER, NE, or SU in the outgoing message");
177        }            
178    }
179    
180    /**
181     * Calls cycle() if we do not expect another thread to be doing so
182     * @param expectingAck as in cycle
183     */
184    private void cycleIfNeeded(boolean expectingAck) throws HL7Exception {
185        if (!myThreaded) {
186            cycle(expectingAck);
187        }        
188    }
189    
190    /**
191     * Sleeps for 1 ms if externally threaded (this is to let the CPU idle).   
192     */
193    private void sleepIfNeeded() {
194        if (myThreaded) {
195            try {
196                Thread.sleep(1);
197            } catch (InterruptedException e) { /* no problem */ }
198        }                
199    }
200    
201    /** Returns true if a CR or AR ACK */ 
202    private static boolean isReject(Transportable theMessage) throws HL7Exception {
203        boolean reject = false;
204        String[] fieldPaths = {"MSA-1"};
205        String[] fields = PreParser.getFields(theMessage.getMessage(), fieldPaths);
206        if (fields[0] != null && (fields[0].equals(CR) || fields[0].equals(AR))) {
207            reject = true;
208        }        
209        return reject;
210    }
211
212    /** Returns true if a CE or AE ACK */ 
213    private static boolean isError(Transportable theMessage) throws HL7Exception {
214        boolean error = false;
215        String[] fieldPaths = {"MSA-1"};
216        String[] fields = PreParser.getFields(theMessage.getMessage(), fieldPaths);
217        if (fields[0] != null && (fields[0].equals(CE) || fields[0].equals(AE))) {
218            error = true;
219        }
220        return error;
221    }
222
223    /**
224     * @see ca.uhn.hl7v2.protocol.Processor#reserve(java.lang.String, long)
225     */
226    public synchronized void reserve(String theAckId, long thePeriodMillis) {
227        Long expiry = System.currentTimeMillis() + thePeriodMillis;
228        myReservations.put(theAckId, expiry);
229    }
230    
231    /**
232     * Tries to send the message, and if there is an error reconnects and tries again. 
233     */
234    private void trySend(TransportLayer theTransport, Transportable theTransportable) throws TransportException {
235        try {
236            theTransport.send(theTransportable);
237        } catch (TransportException e) {
238            theTransport.disconnect();
239            theTransport.connect();
240            theTransport.send(theTransportable);
241        }
242    }
243    
244    
245    /**
246     * Tries to receive a message, and if there is an error reconnects and tries again. 
247     */
248    private Transportable tryReceive(TransportLayer theTransport) throws TransportException {
249        Transportable message;
250        try {
251            message = theTransport.receive();            
252        } catch (TransportException e) {
253            theTransport.disconnect();
254            theTransport.connect();
255            message = theTransport.receive();
256        }
257        return message;
258    }
259
260    /** 
261     * @see ca.uhn.hl7v2.protocol.Processor#cycle(boolean)
262     */
263    public void cycle(boolean expectingAck) throws HL7Exception {
264        log.debug("In cycle({})", expectingAck);
265        
266        cleanReservations();
267        cleanAcceptAcks();
268        cleanReservedMessages();
269
270        Transportable in;
271        try {
272            if (expectingAck) {
273                in = tryReceive(myContext.getLocallyDrivenTransportLayer());
274            } else {
275                in = tryReceive(myContext.getRemotelyDrivenTransportLayer());
276            }
277        } catch (TransportException e) {
278            try {
279                Thread.sleep(1000);
280            } catch (InterruptedException ignored) {}
281            throw e;
282        }
283        
284        // log
285        if (in != null) {
286               log.debug("Received message: {}", in.getMessage());
287        } else {
288                log.debug("Received no message");
289        }
290        
291        // If we have a message, handle it
292        if (in != null) { 
293            String acceptAckNeeded = null;
294//            String appAckNeeded = null;
295            String ackCode = null;
296            String ackId = null;
297            
298            try {
299                    String[] fieldPaths = {"MSH-15", "MSH-16", "MSA-1", "MSA-2"};
300                    String[] fields = PreParser.getFields(in.getMessage(), fieldPaths);         
301                                acceptAckNeeded = fields[0];
302//                              appAckNeeded = fields[1];
303                                ackCode = fields[2];
304                                ackId = fields[3];
305            } catch (HL7Exception e) {
306                log.warn("Failed to parse accept ack fields in incoming message", e);
307            }
308            
309            if (ackId != null && ackCode != null && ackCode.startsWith("C")) {
310                long expiryTime = System.currentTimeMillis() + 1000 * 60;
311                myAcceptAcks.put(ackId, new ExpiringTransportable(in, expiryTime));
312            } else {
313                AcceptAcknowledger.AcceptACK ack = AcceptAcknowledger.validate(getContext(), in);
314            
315                if ((acceptAckNeeded != null && acceptAckNeeded.equals(AL)) 
316                    || (acceptAckNeeded != null && acceptAckNeeded.equals(ER) && !ack.isAcceptable()) 
317                    || (acceptAckNeeded != null && acceptAckNeeded.equals(SU) && ack.isAcceptable())) {
318                    trySend(myContext.getRemotelyDrivenTransportLayer(), ack.getMessage());    
319                }
320  
321                if (ack.isAcceptable()) {
322                    if (isReserved(ackId)) {
323                        
324                        log.debug("Received expected ACK message with ACK ID: {}", ackId);
325                        
326                        removeReservation(ackId);
327                        long expiryTime = System.currentTimeMillis() + 1000 * 60 * 5;                
328                        myAvailableMessages.put(ackId, new ExpiringTransportable(in, expiryTime));
329                        
330                    } else {
331
332                        log.debug("Sending message to router");
333                        Transportable out = myContext.getRouter().processMessage(in);
334                        sendAppResponse(out);
335                        
336                    }
337                } else {
338                        // TODO: should we do something more here? Might be nice to 
339                        // allow a configurable handler for this situation
340                        log.warn("Incoming message was not acceptable");
341                }
342                
343            }
344        } else {
345            String transport = expectingAck ? " Locally driven " : "Remotely driven";
346            log.debug("{} TransportLayer.receive() returned null.", transport);
347        }
348        
349        sleepIfNeeded();
350
351        log.debug("Exiting cycle()");
352    }
353    
354    /** Sends in a new thread if isThreaded, otherwise in current thread */
355    private void sendAppResponse(final Transportable theResponse) {
356        final ProcessorImpl processor = this;
357        Runnable sender = () -> {
358            try {
359                log.debug("Sending response: {}", theResponse);
360
361                //TODO: make configurable
362                processor.send(theResponse, 2, 3000);
363
364            } catch (HL7Exception e) {
365                log.error("Error trying to send response from Application", e);
366            }
367        };
368        
369        if (myThreaded) {
370            myResponseExecutorService.execute(sender);
371        } else {
372            sender.run();
373        }
374    }
375    
376    /**
377     * Removes expired message reservations from the reservation list.  
378     */
379    private synchronized void cleanReservations() {
380        Iterator<String> it = myReservations.keySet().iterator();
381        while (it.hasNext()) {
382            String ackId = it.next();
383            Long expiry = myReservations.get(ackId);
384            if (System.currentTimeMillis() > expiry) {
385                it.remove();
386            }
387        }
388    }
389    
390    /**
391     * Discards expired accept acknowledgements (these are used in retry protocol; see send()).   
392     */
393    private synchronized void cleanAcceptAcks() {
394        Iterator<String> it = myAcceptAcks.keySet().iterator();
395        while (it.hasNext()) {
396            String ackId = it.next();
397            ExpiringTransportable et = myAcceptAcks.get(ackId);
398            if (System.currentTimeMillis() > et.expiryTime) {
399                it.remove();
400            }
401        }        
402    }
403    
404    private synchronized void cleanReservedMessages() throws HL7Exception {
405        Iterator<String> it = myAvailableMessages.keySet().iterator();
406        while (it.hasNext()) {
407            String ackId = it.next();            
408            ExpiringTransportable et = myAvailableMessages.get(ackId);
409            if (System.currentTimeMillis() > et.expiryTime) {
410                it.remove();
411                
412                //send to an Application 
413                Transportable out = myContext.getRouter().processMessage(et.transportable);
414                sendAppResponse(out);                
415            }
416        }  
417    }
418    
419    private synchronized boolean isReserved(String ackId) {
420        boolean reserved = false;
421        if (myReservations.containsKey(ackId)) {
422            reserved = true;
423        }
424        return reserved;
425    }
426    
427    private synchronized void removeReservation(String ackId) {
428        myReservations.remove(ackId);
429    }
430    
431
432    /**
433     * @see ca.uhn.hl7v2.protocol.Processor#isAvailable(java.lang.String)
434     */
435    public boolean isAvailable(String theAckId) {
436        boolean available = false;
437        if (myAvailableMessages.containsKey(theAckId)) {
438            available = true;
439        }
440        return available;
441    }
442
443    /** 
444     * @see ca.uhn.hl7v2.protocol.Processor#receive(java.lang.String, long)
445     */
446    public Transportable receive(String theAckId, long theTimeoutMillis) throws HL7Exception {
447        if (!isReserved(theAckId)) {
448            reserve(theAckId, theTimeoutMillis);
449        }
450        
451        Transportable in = null;
452        long until = System.currentTimeMillis() + theTimeoutMillis;
453        do {
454            synchronized (this) {
455                ExpiringTransportable et = myAvailableMessages.get(theAckId);                
456                if (et == null) {
457                    cycleIfNeeded(false);
458                } else {
459                    in = et.transportable;
460                }
461            }
462            sleepIfNeeded();
463        } while (in == null && System.currentTimeMillis() < until);
464        return in;
465    }
466
467    /** 
468     * @see ca.uhn.hl7v2.protocol.Processor#getContext()
469     */
470    public ProcessorContext getContext() {
471        return myContext;
472    }
473    
474    /**
475     * A struct for Transportable collection entries that time out.  
476     *  
477     * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
478     * @version $Revision: 1.4 $ updated on $Date: 2009-12-16 19:36:34 $ by $Author: jamesagnew $
479     */
480    static class ExpiringTransportable {
481        public final Transportable transportable;
482        public final long expiryTime;
483        
484        public ExpiringTransportable(Transportable theTransportable, long theExpiryTime) {
485            transportable = theTransportable;
486            expiryTime = theExpiryTime;
487        }
488    }
489    
490    /**
491     * A Runnable that repeatedly calls the cycle() method of this class.  
492     * 
493     * @author <a href="mailto:bryan.tripp@uhn.on.ca">Bryan Tripp</a>
494     * @version $Revision: 1.4 $ updated on $Date: 2009-12-16 19:36:34 $ by $Author: jamesagnew $
495     */
496    private static class Cycler implements Runnable {
497
498        private final Processor myProcessor;
499        private final boolean myExpectingAck;
500        private boolean isRunning;
501        
502        /**
503         * @param theProcessor the processor on which to call cycle()
504         * @param isExpectingAck passed to cycle()
505         */
506        public Cycler(Processor theProcessor, boolean isExpectingAck) {
507            myProcessor = theProcessor;
508            myExpectingAck = isExpectingAck;
509            isRunning = true;
510        }
511        
512        /**
513         * Execution will stop at the end of the next cycle.  
514         */
515        public void stop() {
516            isRunning = false;
517        }
518        
519        /** 
520         * Calls cycle() repeatedly on the Processor given in the 
521         * constructor, until stop() is called.  
522         * 
523         * @see java.lang.Runnable#run()
524         */
525        public void run() {
526            while (isRunning) {
527                try {
528                    myProcessor.cycle(myExpectingAck);
529                } catch (HL7Exception e) {
530                    log.error("Error processing message", e);
531                }
532            }
533        }        
534    }
535
536}