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 	 trigger_event 	 application_class 283 * </p> 284 * <p> 285 * message_type 	 trigger_event 	 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 	 * 	 org.yourorganization.ADTProcessor<br> 299 * ORU 	 R01 	 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}