001/**
002 The 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.
004 You may obtain a copy of the License at http://www.mozilla.org/MPL/
005 Software distributed under the License is distributed on an "AS IS" basis,
006 WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for the
007 specific language governing rights and limitations under the License.
008
009 The Original Code is "HL7Service.java".  Description:
010 "Accepts incoming TCP/IP connections and creates Connection objects"
011
012 The Initial Developer of the Original Code is University Health Network. Copyright (C)
013 2001.  All Rights Reserved.
014
015 Contributor(s): Kyle Buza
016
017 Alternatively, the contents of this file may be used under the terms of the
018 GNU General Public License (the  �GPL�), in which case the provisions of the GPL are
019 applicable instead of those above.  If you wish to allow use of your version of this
020 file only under the terms of the GPL and not to allow others to use your version
021 of this file under the MPL, indicate your decision by deleting  the provisions above
022 and replace  them with the notice and other provisions required by the GPL License.
023 If you do not delete the provisions above, a recipient may use your version of
024 this file under either the MPL or the GPL.
025
026 */
027
028package ca.uhn.hl7v2.app;
029
030import ca.uhn.hl7v2.HL7Exception;
031import ca.uhn.hl7v2.HapiContext;
032import ca.uhn.hl7v2.app.Receiver.ReceiverParserExceptionHandler;
033import ca.uhn.hl7v2.concurrent.DefaultExecutorService;
034import ca.uhn.hl7v2.concurrent.Service;
035import ca.uhn.hl7v2.llp.LowerLayerProtocol;
036import ca.uhn.hl7v2.model.Message;
037import ca.uhn.hl7v2.parser.Parser;
038import ca.uhn.hl7v2.protocol.ApplicationRouter.AppRoutingData;
039import ca.uhn.hl7v2.protocol.ReceivingApplication;
040import ca.uhn.hl7v2.protocol.ReceivingApplicationExceptionHandler;
041import ca.uhn.hl7v2.protocol.impl.AppRoutingDataImpl;
042import ca.uhn.hl7v2.protocol.impl.AppWrapper;
043import ca.uhn.hl7v2.protocol.impl.ApplicationRouterImpl;
044import org.slf4j.Logger;
045import org.slf4j.LoggerFactory;
046
047import java.io.BufferedReader;
048import java.io.File;
049import java.io.FileReader;
050import java.io.IOException;
051import java.util.*;
052import java.util.concurrent.ExecutorService;
053
054/**
055 * <p>
056 * An HL7 service. Accepts incoming TCP/IP connections and creates Connection
057 * objects. Uses a single ApplicationRouter object (for all Connections) to
058 * define the Applications to which message are sent. To configure, use
059 * registerApplication() or loadApplicationsFromFile().
060 * </p>
061 * </p>A separate thread looks for Connections that have been closed (locally or
062 * remotely) and discards them. </p>
063 *
064 * @author Bryan Tripp
065 * @author Christian Ohr
066 */
067public abstract class HL7Service extends Service {
068
069    private static final Logger log = LoggerFactory.getLogger(HL7Service.class);
070
071    private final List<Connection> connections;
072    private final Parser parser;
073    private final LowerLayerProtocol llp;
074    private final List<ConnectionListener> listeners;
075    private final ConnectionCleaner cleaner;
076    private final ApplicationRouterImpl applicationRouter;
077    private ReceiverParserExceptionHandler parserExeptionHandler;
078
079    /**
080     * @param theHapiContext HapiContext
081     */
082    public HL7Service(HapiContext theHapiContext) {
083        this(theHapiContext.getGenericParser(), theHapiContext.getLowerLayerProtocol(), theHapiContext.getExecutorService());
084    }
085
086    /**
087     * @param parser parser to be used
088     * @param llp    LowerLayerProtocol
089     */
090    public HL7Service(final Parser parser, final LowerLayerProtocol llp) {
091        this(parser, llp, DefaultExecutorService.getDefaultService());
092    }
093
094    /**
095     * Creates a new instance of Server
096     *
097     * @param parser          parser to be used
098     * @param llp             LowerLayerProtocol
099     * @param executorService executor used for starting threads
100     */
101    public HL7Service(Parser parser, LowerLayerProtocol llp,
102                      ExecutorService executorService) {
103        super("HL7 Server", executorService);
104        this.connections = new ArrayList<>();
105        this.listeners = new ArrayList<>();
106        this.parser = parser;
107        this.llp = llp;
108        this.applicationRouter = new ApplicationRouterImpl(parser);
109        this.cleaner = new ConnectionCleaner(this);
110
111        // 960101
112        assert !this.cleaner.isRunning();
113    }
114
115    /**
116     * Called after startup before the thread enters its main loop. This
117     * implementation launches a cleaner thread that removes stale connections
118     * from the connection list. Override to initialize resources for the
119     * running thread, e.g. opening {@link java.net.ServerSocket}s etc.
120     */
121    @Override
122    protected void afterStartup() {
123        // Fix for bug 960101: Don't start the cleaner thread until the
124        // server is started.
125        cleaner.start();
126    }
127
128    /**
129     * Called after the thread has left its main loop. This implementation stops
130     * the connection cleaner thread and closes any open connections. Override
131     * to clean up additional resources from the running thread, e.g. closing
132     * {@link java.net.ServerSocket}s.
133     */
134    @Override
135    protected void afterTermination() {
136        super.afterTermination();
137        cleaner.stopAndWait();
138        for (Connection c : connections) {
139            try {
140                c.close();
141            } catch (IOException e) {
142                throw new RuntimeException(e);
143            }
144        }
145    }
146
147    /**
148     * Returns true if the thread should continue to run, false otherwise (ie if
149     * stop() has been called).
150     *
151     * @deprecated Use {@link #isRunning()}. Deprecated as of version 0.6.
152     */
153    protected boolean keepRunning() {
154        return isRunning();
155    }
156
157    LowerLayerProtocol getLlp() {
158        return llp;
159    }
160
161    Parser getParser() {
162        return parser;
163    }
164
165    /**
166     * Called by subclasses when a new Connection is made. Registers the
167     * ApplicationRouter with the given Connection and stores it.
168     *
169     * @param c existing connection
170     */
171    public synchronized void newConnection(ActiveConnection c) {
172        c.getResponder().setApplicationRouter(applicationRouter);
173        c.setReceiverParserExeptionHandler(parserExeptionHandler);
174        c.activate();
175        connections.add(c); // keep track of connections
176        notifyListeners(c);
177    }
178
179    /**
180     * Returns a connection to a remote host that was initiated by the given
181     * remote host. If the connection has not been made, this method blocks
182     * until the remote host connects.
183     *
184     * @param ipAddress IP Address
185     * @return connection that was initiated by the given address
186     */
187    public Connection getRemoteConnection(String ipAddress) {
188        Connection conn = null;
189        while (conn == null) {
190            // check all connections ...
191            int c = 0;
192            synchronized (this) {
193                while (conn == null && c < connections.size()) {
194                    Connection nextConn = connections.get(c);
195                    if (nextConn.getRemoteAddress().getHostAddress().equals(ipAddress))
196                        conn = nextConn;
197                    c++;
198                }
199            }
200
201            if (conn == null) {
202                try {
203                    Thread.sleep(100);
204                } catch (InterruptedException e) {
205                    // don't care
206                }
207            }
208        }
209        return conn;
210    }
211
212    /**
213     * Returns all currently active connections.
214     *
215     * @return list of active remote connections
216     */
217    public synchronized List<Connection> getRemoteConnections() {
218        return connections;
219    }
220
221    /**
222     * Registers the given ConnectionListener with the HL7Service - when a
223     * remote host makes a new Connection, all registered listeners will be
224     * notified.
225     *
226     * @param listener connection listener to be called
227     */
228    public synchronized void registerConnectionListener(
229            ConnectionListener listener) {
230        listeners.add(listener);
231    }
232
233    /**
234     * Notifies all listeners that a Connection is new or discarded.
235     */
236    private void notifyListeners(Connection c) {
237        for (ConnectionListener cl : listeners) {
238            if (c.isOpen()) {
239                cl.connectionReceived(c);
240            } else {
241                cl.connectionDiscarded(c);
242            }
243        }
244    }
245
246    /**
247     * Registers the given application to handle messages corresponding to the
248     * given type and trigger event. Only one application can be registered for
249     * a given message type and trigger event combination. A repeated
250     * registration for a particular combination of type and trigger event
251     * over-writes the previous one. Note that the wildcard "*" for messageType
252     * or triggerEvent means any type or event, respectively.
253     *
254     * @deprecated use {@link #registerApplication(String, String, ca.uhn.hl7v2.protocol.ReceivingApplication)} and
255     * {@link ca.uhn.hl7v2.protocol.impl.AppWrapper}
256     */
257    public synchronized void registerApplication(String messageType,
258                                                 String triggerEvent, Application handler) {
259        ReceivingApplication<Message> handlerWrapper = new AppWrapper(handler);
260        applicationRouter.bindApplication(new AppRoutingDataImpl(messageType, triggerEvent, "*", "*"), handlerWrapper);
261    }
262
263    /**
264     * Registers the given application to handle messages corresponding to the
265     * given type and trigger event. Only one application can be registered for
266     * a given message type and trigger event combination. A repeated
267     * registration for a particular combination of type and trigger event
268     * over-writes the previous one. Note that the wildcard "*" for messageType
269     * or triggerEvent means any type or event, respectively.
270     */
271    public void registerApplication(String messageType, String triggerEvent, ReceivingApplication handler) {
272        applicationRouter.bindApplication(new AppRoutingDataImpl(messageType, triggerEvent, "*", "*"), handler);
273    }
274
275    /**
276     * Registers the given application to handle messages corresponding to ALL
277     * message types and trigger events.
278     */
279    public synchronized void registerApplication(AppRoutingData appRouting, ReceivingApplication<? extends Message> application) {
280        if (appRouting == null) {
281            throw new NullPointerException("appRouting must not be null");
282        }
283        applicationRouter.bindApplication(appRouting, application);
284    }
285
286    /**
287     * Registers the given application to handle messages corresponding to ALL
288     * message types and trigger events.
289     */
290    public synchronized void registerApplication(ReceivingApplication<? extends Message> application) {
291
292        registerApplication(new AppRoutingDataImpl("*", "*", "*", "*"), application);
293    }
294
295    /**
296     * Unregisteres the first application that matches the routing data
297     *
298     * @param appRouting
299     * @return true if an application was unregistered, false otherwise
300     */
301    public synchronized boolean unregisterApplication(AppRoutingData appRouting) {
302        if (appRouting == null) {
303            throw new NullPointerException("appRouting must not be null");
304        }
305        return applicationRouter.unbindApplication(appRouting);
306    }
307
308    /**
309     * Unregisteres the passed application
310     *
311     * @param application receiving application
312     * @return true if an application was unregistered, false otherwise
313     */
314    public synchronized boolean unregisterApplication(ReceivingApplication<? extends Message> application) {
315        if (application == null) {
316            throw new NullPointerException("application must not be null");
317        }
318        return applicationRouter.unbindApplication(application);
319    }
320
321    /**
322     * Sets an exception handler which will be invoked in the event of a
323     * failure during parsing, processing, or encoding of an
324     * incoming message or its response.
325     */
326    public synchronized void setExceptionHandler(ReceivingApplicationExceptionHandler exHandler) {
327        applicationRouter.setExceptionHandler(exHandler);
328    }
329
330    /**
331     * Register a receiver level parser exception handler so that an application can be notified
332     * of protocol level parsing errors if any
333     */
334    public void setParserExeptionHandler(ReceiverParserExceptionHandler parserExeptionHandler) {
335        this.parserExeptionHandler = parserExeptionHandler;
336    }
337
338    /**
339     * <p>
340     * A convenience method for registering applications (using
341     * <code>registerApplication()
342     * </code>) with this service. Information about which Applications should
343     * handle which messages is read from the given text file. Each line in the
344     * file should have the following format (entries tab delimited):
345     * </p>
346     * <p>
347     * message_type &#009; trigger_event &#009; application_class
348     * </p>
349     * <p>
350     * message_type &#009; trigger_event &#009; application_class
351     * </p>
352     * <p>
353     * Note that message type and event can be the wildcard "*", which means
354     * any.
355     * </p>
356     * <p>
357     * For example, if you write an Application called
358     * org.yourorganiztion.ADTProcessor that processes several types of ADT
359     * messages, and another called org.yourorganization.ResultProcessor that
360     * processes result messages, you might have a file that looks like this:
361     * </p>
362     * <p>
363     * ADT &#009; * &#009; org.yourorganization.ADTProcessor<br>
364     * ORU &#009; R01 &#009; org.yourorganization.ResultProcessor
365     * </p>
366     * <p>
367     * Each class listed in this file must implement Application and must have a
368     * zero-argument constructor.
369     * </p>
370     */
371    public void loadApplicationsFromFile(File f) throws IOException,
372            HL7Exception, ClassNotFoundException, InstantiationException,
373            IllegalAccessException {
374        try (BufferedReader in = new BufferedReader(new FileReader(f))) {
375            String line;
376            while ((line = in.readLine()) != null) {
377                // parse application registration information
378                StringTokenizer tok = new StringTokenizer(line, "\t", false);
379                String type, event, className;
380
381                if (tok.hasMoreTokens()) { // skip blank lines
382                    try {
383                        type = tok.nextToken();
384                        event = tok.nextToken();
385                        className = tok.nextToken();
386                    } catch (NoSuchElementException ne) {
387                        throw new HL7Exception(
388                                "Can't register applications from file "
389                                        + f.getName()
390                                        + ". The line '"
391                                        + line
392                                        + "' is not of the form: message_type [tab] trigger_event [tab] application_class.");
393                    }
394
395                    try {
396                        @SuppressWarnings("unchecked")
397                        Class<? extends Application> appClass = (Class<? extends Application>) Class
398                                .forName(className); // may throw
399                        // ClassNotFoundException
400                        Application app = appClass.newInstance();
401                        registerApplication(type, event, new AppWrapper(app));
402                    } catch (ClassCastException cce) {
403                        throw new HL7Exception("The specified class, " + className
404                                + ", doesn't implement Application.");
405                    }
406
407                }
408            }
409        }
410        // don't care
411    }
412
413    /**
414     * Runnable that looks for closed Connections and discards them. It would be
415     * nice to find a way to externalize this safely so that it could be re-used
416     * by (for example) TestPanel. It could take a Vector of Connections as an
417     * argument, instead of an HL7Service, but some problems might arise if
418     * other threads were iterating through the Vector while this one was
419     * removing elements from it.
420     * <p/>
421     * Note: this could be started as daemon, so we don't need to care about
422     * termination.
423     */
424    private static class ConnectionCleaner extends Service {
425
426        private final HL7Service service;
427
428        public ConnectionCleaner(HL7Service service) {
429            super("ConnectionCleaner", service.getExecutorService());
430            this.service = service;
431        }
432
433        @Override
434        public void start() {
435            log.info("Starting ConnectionCleaner service");
436            super.start();
437        }
438
439        public void handle() {
440            try {
441                Thread.sleep(500);
442                synchronized (service) {
443                    Iterator<Connection> it = service.getRemoteConnections()
444                            .iterator();
445                    while (it.hasNext()) {
446                        Connection conn = it.next();
447                        if (!conn.isOpen()) {
448                            log.debug(
449                                    "Removing connection from {} from connection list",
450                                    conn.getRemoteAddress().getHostAddress());
451                            it.remove();
452                            service.notifyListeners(conn);
453                        }
454                    }
455                }
456            } catch (InterruptedException e) {
457                // don't care
458            }
459        }
460
461    }
462
463}