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