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