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    
028    package ca.uhn.hl7v2.app;
029    
030    import java.io.BufferedReader;
031    import java.io.File;
032    import java.io.FileReader;
033    import java.io.IOException;
034    import java.net.ServerSocket;
035    import java.util.ArrayList;
036    import java.util.Iterator;
037    import java.util.List;
038    import java.util.NoSuchElementException;
039    import java.util.StringTokenizer;
040    import java.util.concurrent.ExecutorService;
041    
042    import org.slf4j.Logger;
043    import org.slf4j.LoggerFactory;
044    
045    import ca.uhn.hl7v2.HL7Exception;
046    import ca.uhn.hl7v2.concurrent.DefaultExecutorService;
047    import ca.uhn.hl7v2.concurrent.Service;
048    import ca.uhn.hl7v2.llp.LowerLayerProtocol;
049    import 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     */
064    public 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    }