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 "HL7Service.java".  Description: 
010"Accepts incoming TCP/IP connections and creates Connection objects" 
011
012The Initial Developer of the Original Code is University Health Network. Copyright (C) 
0132001.  All Rights Reserved. 
014
015Contributor(s): Kyle Buza 
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 */
027
028package ca.uhn.hl7v2.app;
029
030import java.io.BufferedReader;
031import java.io.File;
032import java.io.FileReader;
033import java.io.IOException;
034import java.net.ServerSocket;
035import java.util.ArrayList;
036import java.util.Iterator;
037import java.util.List;
038import java.util.NoSuchElementException;
039import java.util.StringTokenizer;
040import java.util.concurrent.ExecutorService;
041
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import ca.uhn.hl7v2.HL7Exception;
046import ca.uhn.hl7v2.concurrent.DefaultExecutorService;
047import ca.uhn.hl7v2.concurrent.Service;
048import ca.uhn.hl7v2.llp.LowerLayerProtocol;
049import ca.uhn.hl7v2.parser.Parser;
050
051/**
052 * <p>
053 * An HL7 service. Accepts incoming TCP/IP connections and creates Connection
054 * objects. Uses a single MessageTypeRouter object (for all Connections) to
055 * define the Applications to which message are sent. To configure, use
056 * registerApplication() or loadApplicationsFromFile().
057 * </p>
058 * </p>A separate thread looks for Connections that have been closed (locally or
059 * remotely) and discards them. </p>
060 * 
061 * @author Bryan Tripp
062 * @author Christian Ohr
063 */
064public abstract class HL7Service extends Service {
065
066        private static final Logger log = LoggerFactory.getLogger(HL7Service.class);
067
068        private List<Connection> connections;
069        protected Parser parser;
070        protected LowerLayerProtocol llp;
071        private MessageTypeRouter router;
072        private List<ConnectionListener> listeners;
073        private ConnectionCleaner cleaner;
074
075        /** Creates a new instance of Server using a default thread pool */
076        public HL7Service(Parser parser, LowerLayerProtocol llp) {
077                this(parser, llp, DefaultExecutorService.getDefaultService());
078        }
079
080        /** Creates a new instance of Server */
081        public HL7Service(Parser parser, LowerLayerProtocol llp,
082                        ExecutorService executorService) {
083                super("HL7 Server", executorService);
084                connections = new ArrayList<Connection>();
085                listeners = new ArrayList<ConnectionListener>();
086                this.parser = parser;
087                this.llp = llp;
088                this.router = new MessageTypeRouter();
089        }
090
091        /**
092         * Called after startup before the thread enters its main loop. This
093         * implementation launches a cleaner thread that removes stale connections
094         * from the connection list. Override to initialize resources for the
095         * running thread, e.g. opening {@link ServerSocket}s etc.
096         * 
097         * @throws IOException
098         */
099        @Override
100        protected void afterStartup() {
101                // Fix for bug 960101: Don't start the cleaner thread until the
102                // server is started.
103                cleaner = new ConnectionCleaner(this);
104                cleaner.start();
105        }
106
107        /**
108         * Called after the thread has left its main loop. This implementation stops
109         * the connection cleaner thread and closes any open connections. Override
110         * to clean up additional resources from the running thread, e.g. closing
111         * {@link ServerSocket}s.
112         */
113        @Override
114        protected void afterTermination() {
115                super.afterTermination();
116                cleaner.stopAndWait();
117                for (Connection c : connections) {
118                        c.close();
119                }
120        }
121
122        /**
123         * Returns true if the thread should continue to run, false otherwise (ie if
124         * stop() has been called).
125         * 
126         * @deprecated Use {@link #isRunning()}. Deprecated as of version 0.6.
127         */
128        protected boolean keepRunning() {
129                return isRunning();
130        }
131
132        /**
133         * Called by subclasses when a new Connection is made. Registers the
134         * MessageTypeRouter with the given Connection and stores it.
135         */
136        public synchronized void newConnection(Connection c) {
137                c.getResponder().registerApplication(router);
138                c.activate();
139                connections.add(c); // keep track of connections
140                notifyListeners(c);
141        }
142
143        /**
144         * Returns a connection to a remote host that was initiated by the given
145         * remote host. If the connection has not been made, this method blocks
146         * until the remote host connects. TODO currently nobody calls this...
147         */
148        public Connection getRemoteConnection(String IP) {
149                Connection conn = null;
150                while (conn == null) {
151                        // check all connections ...
152                        int c = 0;
153                        synchronized (this) {
154                                while (conn == null && c < connections.size()) {
155                                        Connection nextConn = (Connection) connections.get(c);
156                                        if (nextConn.getRemoteAddress().getHostAddress().equals(IP))
157                                                conn = nextConn;
158                                        c++;
159                                }
160                        }
161
162                        if (conn == null) {
163                                try {
164                                        Thread.sleep(100);
165                                } catch (InterruptedException e) {
166                                }
167                        }
168                }
169                return conn;
170        }
171
172        /** Returns all currently active connections. */
173        public synchronized List<Connection> getRemoteConnections() {
174                return connections;
175        }
176
177        /**
178         * Registers the given ConnectionListener with the HL7Service - when a
179         * remote host makes a new Connection, all registered listeners will be
180         * notified.
181         */
182        public synchronized void registerConnectionListener(
183                        ConnectionListener listener) {
184                listeners.add(listener);
185        }
186
187        /** Notifies all listeners that a Connection is new or discarded. */
188        private void notifyListeners(Connection c) {
189                for (ConnectionListener cl : listeners) {
190                        if (c.isOpen()) {
191                                cl.connectionReceived(c);
192                        } else {
193                                cl.connectionDiscarded(c);
194                        }
195                }
196        }
197
198        /**
199         * Registers the given application to handle messages corresponding to the
200         * given type and trigger event. Only one application can be registered for
201         * a given message type and trigger event combination. A repeated
202         * registration for a particular combination of type and trigger event
203         * over-writes the previous one. Note that the wildcard "*" for messageType
204         * or triggerEvent means any type or event, respectively.
205         */
206        public synchronized void registerApplication(String messageType,
207                        String triggerEvent, Application handler) {
208                router.registerApplication(messageType, triggerEvent, handler);
209        }
210
211        /**
212         * <p>
213         * A convenience method for registering applications (using
214         * <code>registerApplication()
215         * </code>) with this service. Information about which Applications should
216         * handle which messages is read from the given text file. Each line in the
217         * file should have the following format (entries tab delimited):
218         * </p>
219         * <p>
220         * message_type &#009; trigger_event &#009; application_class
221         * </p>
222         * <p>
223         * message_type &#009; trigger_event &#009; application_class
224         * </p>
225         * <p>
226         * Note that message type and event can be the wildcard "*", which means
227         * any.
228         * </p>
229         * <p>
230         * For example, if you write an Application called
231         * org.yourorganiztion.ADTProcessor that processes several types of ADT
232         * messages, and another called org.yourorganization.ResultProcessor that
233         * processes result messages, you might have a file that looks like this:
234         * </p>
235         * <p>
236         * ADT &#009; * &#009; org.yourorganization.ADTProcessor<br>
237         * ORU &#009; R01 &#009; org.yourorganization.ResultProcessor
238         * </p>
239         * <p>
240         * Each class listed in this file must implement Application and must have a
241         * zero-argument constructor.
242         * </p>
243         */
244        public void loadApplicationsFromFile(File f) throws IOException,
245                        HL7Exception, ClassNotFoundException, InstantiationException,
246                        IllegalAccessException {
247                BufferedReader in = new BufferedReader(new FileReader(f));
248                String line = null;
249                while ((line = in.readLine()) != null) {
250                        // parse application registration information
251                        StringTokenizer tok = new StringTokenizer(line, "\t", false);
252                        String type = null, event = null, className = null;
253
254                        if (tok.hasMoreTokens()) { // skip blank lines
255                                try {
256                                        type = tok.nextToken();
257                                        event = tok.nextToken();
258                                        className = tok.nextToken();
259                                } catch (NoSuchElementException ne) {
260                                        throw new HL7Exception(
261                                                        "Can't register applications from file "
262                                                                        + f.getName()
263                                                                        + ". The line '"
264                                                                        + line
265                                                                        + "' is not of the form: message_type [tab] trigger_event [tab] application_class.",
266                                                        HL7Exception.APPLICATION_INTERNAL_ERROR);
267                                }
268
269                                try {
270                                        @SuppressWarnings("unchecked")
271                                        Class<? extends Application> appClass = (Class<? extends Application>) Class
272                                                        .forName(className); // may throw
273                                                                                                        // ClassNotFoundException
274                                        Application app = appClass.newInstance();
275                                        registerApplication(type, event, app);
276                                } catch (ClassCastException cce) {
277                                        throw new HL7Exception("The specified class, " + className
278                                                        + ", doesn't implement Application.",
279                                                        HL7Exception.APPLICATION_INTERNAL_ERROR);
280                                }
281
282                        }
283                }
284        }
285
286        /**
287         * Runnable that looks for closed Connections and discards them. It would be
288         * nice to find a way to externalize this safely so that it could be re-used
289         * by (for example) TestPanel. It could take a Vector of Connections as an
290         * argument, instead of an HL7Service, but some problems might arise if
291         * other threads were iterating through the Vector while this one was
292         * removing elements from it.
293         * 
294         * Note: this could be started as daemon, so we don't need to care about
295         * termination.
296         */
297        private class ConnectionCleaner extends Service {
298
299                HL7Service service;
300
301                public ConnectionCleaner(HL7Service service) {
302                        super("ConnectionCleaner", service.getExecutorService());
303                        this.service = service;
304                }
305
306                @Override
307                public void start() {
308                        log.info("Starting ConnectionCleaner service");
309                        super.start();
310                }
311
312                public void handle() {
313                        try {
314                                Thread.sleep(500);
315                                synchronized (service) {
316                                        Iterator<Connection> it = service.getRemoteConnections()
317                                                        .iterator();
318                                        while (it.hasNext()) {
319                                                Connection conn = it.next();
320                                                if (!conn.isOpen()) {
321                                                        log.debug(
322                                                                        "Removing connection from {} from connection list",
323                                                                        conn.getRemoteAddress().getHostAddress());
324                                                        it.remove();
325                                                        service.notifyListeners(conn);
326                                                }
327                                        }
328                                }
329                        } catch (InterruptedException e) {
330                        }
331                }
332
333        }
334
335}