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 "ConnectionHub.java". Description: 010"Provides access to shared HL7 Connections" 011 012The Initial Developer of the Original Code is University Health Network. Copyright (C) 0132001. All Rights Reserved. 014 015Contributor(s): ______________________________________. 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 027package ca.uhn.hl7v2.app; 028 029import java.util.Collections; 030import java.util.Map; 031import java.util.Set; 032import java.util.concurrent.ConcurrentHashMap; 033import java.util.concurrent.ConcurrentMap; 034import java.util.concurrent.ExecutorService; 035 036import org.slf4j.Logger; 037import org.slf4j.LoggerFactory; 038 039import ca.uhn.hl7v2.HL7Exception; 040import ca.uhn.hl7v2.concurrent.DefaultExecutorService; 041import ca.uhn.hl7v2.llp.LowerLayerProtocol; 042import ca.uhn.hl7v2.parser.Parser; 043 044/** 045 * <p> 046 * Provides access to shared HL7 Connections. The ConnectionHub has at most one 047 * connection to any given address at any time. 048 * </p> 049 * <p> 050 * <b>Synchronization Note:</b> This class should be safe to use in a 051 * multithreaded environment. A synchronization mutex is maintained for any 052 * given target host and port, so that if two threads are trying to connect to 053 * two separate destinations neither will block, but if two threads are trying 054 * to connect to the same destination, one will block until the other has 055 * finished trying. Use caution if this class is to be used in an environment 056 * where a very large (over 1000) number of target host/port destinations will 057 * be accessed at the same time. 058 * </p> 059 * 060 * @author Bryan Tripp 061 */ 062public class ConnectionHub { 063 064 /** 065 * Set a system property with this key to a string containing an integer 066 * larger than the default ("1000") if you need to connect to a very large 067 * number of targets at the same time in a multithreaded environment. 068 */ 069 public static final String MAX_CONCURRENT_TARGETS = ConnectionHub.class 070 .getName() + ".maxSize"; 071 private static final Logger log = LoggerFactory 072 .getLogger(ConnectionHub.class); 073 private static ConnectionHub instance = null; 074 private final CountingMap<ConnectionData, Connection> connections; 075 private final ConcurrentMap<String, String> connectionMutexes = new ConcurrentHashMap<String, String>(); 076 private final ExecutorService executorService; 077 078 /** Creates a new instance of ConnectionHub */ 079 private ConnectionHub(ExecutorService executorService) { 080 this.executorService = executorService; 081 connections = new CountingMap<ConnectionData, Connection>() { 082 083 @Override 084 protected Connection open(ConnectionData connectionData) 085 throws Exception { 086 return ConnectionFactory.open(connectionData, 087 ConnectionHub.this.executorService); 088 } 089 090 @Override 091 protected void dispose(Connection connection) { 092 connection.close(); 093 } 094 095 }; 096 } 097 098 /** Returns the singleton instance of ConnectionHub */ 099 public static ConnectionHub getInstance() { 100 return getInstance(DefaultExecutorService.getDefaultService()); 101 } 102 103 public static void shutdown() { 104 ConnectionHub hub = getInstance(); 105 if (DefaultExecutorService.isDefaultService(hub.executorService)) { 106 hub.executorService.shutdown(); 107 instance = null; 108 } 109 } 110 111 /** Returns the singleton instance of ConnectionHub. If called */ 112 public synchronized static ConnectionHub getInstance(ExecutorService service) { 113 if (instance == null || service.isShutdown()) { 114 instance = new ConnectionHub(service); 115 } 116 return instance; 117 } 118 119 /** 120 * Returns a Connection to the given address, opening this Connection if 121 * necessary. The given Parser will only be used if a new Connection is 122 * opened, so there is no guarantee that the Connection returned will be 123 * using the Parser you provide. If you need explicit access to the Parser 124 * the Connection is using, call <code>Connection.getParser()</code>. 125 */ 126 public Connection attach(String host, int port, Parser parser, 127 Class<? extends LowerLayerProtocol> llpClass) throws HL7Exception { 128 return attach(host, port, parser, llpClass, false); 129 } 130 131 public Connection attach(String host, int port, Parser parser, 132 Class<? extends LowerLayerProtocol> llpClass, boolean tls) 133 throws HL7Exception { 134 return attach(host, port, 0, parser, llpClass, tls); 135 } 136 137 public Connection attach(String host, int port, Parser parser, 138 LowerLayerProtocol llp, boolean tls) 139 throws HL7Exception { 140 return attach(host, port, 0, parser, llp, tls); 141 } 142 143 public Connection attach(String host, int outboundPort, int inboundPort, 144 Parser parser, Class<? extends LowerLayerProtocol> llpClass) 145 throws HL7Exception { 146 return attach(host, outboundPort, inboundPort, parser, llpClass, false); 147 } 148 149 public Connection attach(String host, int outboundPort, int inboundPort, 150 Parser parser, Class<? extends LowerLayerProtocol> llpClass, 151 boolean tls) throws HL7Exception { 152 try { 153 LowerLayerProtocol llp = llpClass.newInstance(); 154 return attach(host, outboundPort, inboundPort, parser, llp, tls); 155 } catch (InstantiationException e) { 156 throw new HL7Exception("Cannot open connection to " + host + ":" 157 + outboundPort, e); 158 } catch (IllegalAccessException e) { 159 throw new HL7Exception("Cannot open connection to " + host + ":" 160 + outboundPort, e); 161 } 162 } 163 164 public Connection attach(String host, int outboundPort, int inboundPort, Parser parser, LowerLayerProtocol llp, boolean tls) throws HL7Exception { 165 return attach(new ConnectionData(host, outboundPort, inboundPort, 166 parser, llp, tls)); 167 } 168 169 public Connection attach(ConnectionData data) throws HL7Exception { 170 try { 171 Connection conn = null; 172 // Disallow establishing same connection targets concurrently 173 connectionMutexes.putIfAbsent(data.toString(), data.toString()); 174 String mutex = connectionMutexes.get(data.toString()); 175 synchronized (mutex) { 176 discardConnectionIfStale(connections.get(data)); 177 // Create connection or increase counter 178 conn = connections.put(data); 179 } 180 return conn; 181 } catch (Exception e) { 182 throw new HL7Exception("Cannot open connection to " 183 + data.getHost() + ":" + data.getPort() + "/" 184 + data.getPort2(), e); 185 } 186 } 187 188 private void discardConnectionIfStale(Connection conn) { 189 if (conn != null && !conn.isOpen()) { 190 log.info( 191 "Discarding connection which appears to be closed. Remote addr: {}", 192 conn.getRemoteAddress()); 193 discard(conn); 194 conn = null; 195 } 196 } 197 198 /** 199 * Informs the ConnectionHub that you are done with the given Connection - 200 * if no other code is using it, it will be closed, so you should not 201 * attempt to use a Connection after detaching from it. If the connection is 202 * not enlisted, this method does nothing. 203 */ 204 public void detach(Connection c) { 205 ConnectionData cd = connections.find(c); 206 if (cd != null) 207 connections.remove(cd); 208 } 209 210 /** 211 * Closes and discards the given Connection so that it can not be returned 212 * in subsequent calls to attach(). This method is to be used when there is 213 * a problem with a Connection, e.g. socket connection closed by remote 214 * host. 215 */ 216 public void discard(Connection c) { 217 ConnectionData cd = connections.find(c); 218 if (cd != null) 219 connections.removeAllOf(cd); 220 } 221 222 public void discardAll() { 223 for (ConnectionData cd : allConnections()) { 224 connections.removeAllOf(cd); 225 } 226 } 227 228 public Set<? extends ConnectionData> allConnections() { 229 return connections.keySet(); 230 } 231 232 public Connection getKnownConnection(ConnectionData key) { 233 return connections.get(key); 234 } 235 236 public boolean isOpen(ConnectionData key) { 237 return getKnownConnection(key).isOpen(); 238 } 239 240 241 /** 242 * Helper class that implements a map that increases/decreases a counter 243 * when an entry is added/removed. It is furthermore intended that an 244 * entry's value is derived from its key. 245 * 246 * @param <K> 247 * key class 248 * @param <D> 249 * managed value class 250 */ 251 private abstract class CountingMap<K, D> { 252 private Map<K, Count> content; 253 254 protected abstract D open(K key) throws Exception; 255 256 protected abstract void dispose(D value); 257 258 public CountingMap() { 259 super(); 260 content = new ConcurrentHashMap<K, Count>(); 261 } 262 263 /** 264 * If the key exists, the counter is increased. Otherwise, a value is 265 * created, and the key/value pair is added to the map. 266 */ 267 public D put(K key) throws Exception { 268 if (content.containsKey(key)) { 269 return content.put(key, content.get(key).increase()).getValue(); 270 } else { 271 Count c = new Count(open(key)); 272 content.put(key, c); 273 return c.getValue(); 274 } 275 } 276 277 public Set<K> keySet() { 278 return Collections.unmodifiableSet(content.keySet()); 279 } 280 281 public D get(K key) { 282 return content.containsKey(key) ? content.get(key).getValue() 283 : null; 284 } 285 286 public K find(D value) { 287 for (Map.Entry<K, Count> entry : content.entrySet()) { 288 if (entry.getValue().getValue().equals(value)) { 289 return entry.getKey(); 290 } 291 } 292 return null; 293 } 294 295 /** 296 * If the counter of the key/value is greater than one, the counter is 297 * decreased. Otherwise, the entry is removed and the value is cleaned 298 * up. 299 */ 300 public D remove(K key) { 301 Count pair = content.get(key); 302 if (pair == null) 303 return null; 304 if (pair.isLast()) { 305 return removeAllOf(key); 306 } 307 return content.put(key, content.get(key).decrease()).getValue(); 308 } 309 310 /** 311 * The key/value entry is removed and the value is cleaned up. 312 */ 313 public D removeAllOf(K key) { 314 D removed = content.remove(key).value; 315 dispose(removed); 316 return removed; 317 } 318 319 private class Count { 320 private D value; 321 private int count; 322 323 public Count(D value) { 324 this(value, 1); 325 } 326 327 private Count(D value, int number) { 328 this.value = value; 329 this.count = number; 330 } 331 332 public D getValue() { 333 return value; 334 } 335 336 Count increase() { 337 return new Count(value, count + 1); 338 } 339 340 boolean isLast() { 341 return count == 1; 342 } 343 344 Count decrease() { 345 return !isLast() ? new Count(value, count - 1) : null; 346 } 347 348 } 349 350 } 351 352}