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