001package ca.uhn.hl7v2.hoh.raw.client; 002 003import static ca.uhn.hl7v2.hoh.util.StringUtils.isBlank; 004 005import java.io.BufferedInputStream; 006import java.io.BufferedOutputStream; 007import java.io.IOException; 008import java.io.OutputStream; 009import java.net.InetSocketAddress; 010import java.net.MalformedURLException; 011import java.net.Socket; 012import java.net.URI; 013import java.net.URISyntaxException; 014import java.net.URL; 015import java.nio.charset.Charset; 016 017import ca.uhn.hl7v2.hoh.api.DecodeException; 018import ca.uhn.hl7v2.hoh.api.EncodeException; 019import ca.uhn.hl7v2.hoh.api.IAuthorizationClientCallback; 020import ca.uhn.hl7v2.hoh.api.IClient; 021import ca.uhn.hl7v2.hoh.api.IReceivable; 022import ca.uhn.hl7v2.hoh.api.ISendable; 023import ca.uhn.hl7v2.hoh.api.MessageMetadataKeys; 024import ca.uhn.hl7v2.hoh.encoder.Hl7OverHttpRequestEncoder; 025import ca.uhn.hl7v2.hoh.encoder.Hl7OverHttpResponseDecoder; 026import ca.uhn.hl7v2.hoh.encoder.NoMessageReceivedException; 027import ca.uhn.hl7v2.hoh.raw.api.RawReceivable; 028import ca.uhn.hl7v2.hoh.sign.ISigner; 029import ca.uhn.hl7v2.hoh.sign.SignatureVerificationException; 030import ca.uhn.hl7v2.hoh.sockets.ISocketFactory; 031import ca.uhn.hl7v2.hoh.sockets.StandardSocketFactory; 032import ca.uhn.hl7v2.hoh.sockets.TlsSocketFactory; 033 034public abstract class AbstractRawClient implements IClient { 035 036 /** 037 * The default charset encoding (UTF-8) 038 */ 039 public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); 040 041 /** 042 * The default connection timeout in milliseconds: 10000 043 */ 044 public static final int DEFAULT_CONNECTION_TIMEOUT = 10000; 045 046 /** 047 * The default number of milliseconds to wait before timing out waiting for 048 * a response: 60000 049 */ 050 public static final int DEFAULT_RESPONSE_TIMEOUT = 60000; 051 052 private static final StandardSocketFactory DEFAULT_SOCKET_FACTORY = new StandardSocketFactory(); 053 054 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HohRawClientSimple.class); 055 056 private IAuthorizationClientCallback myAuthorizationCallback; 057 private Charset myCharset = DEFAULT_CHARSET; 058 private int myConnectionTimeout = DEFAULT_CONNECTION_TIMEOUT; 059 private String myHost; 060 private BufferedInputStream myInputStream; 061 private boolean myKeepAlive = true; 062 private OutputStream myOutputStream; 063 private String myPath; 064 private int myPort; 065 private long myResponseTimeout = DEFAULT_RESPONSE_TIMEOUT; 066 private ISigner mySigner; 067 private ISocketFactory mySocketFactory = DEFAULT_SOCKET_FACTORY; 068 /** 069 * Socket so_timeout value for newly created sockets 070 */ 071 private int mySoTimeout = 5000; 072 private URL myUrl; 073 /** 074 * Constructor 075 */ 076 public AbstractRawClient() { 077 // nothing 078 } 079 /** 080 * Constructor 081 * 082 * @param theHost 083 * The HOST (name/address). E.g. "192.168.1.1" 084 * @param thePort 085 * The PORT. E.g. "8080" 086 * @param thePath 087 * The path being requested (must either be blank or start with 088 * '/' and contain a path). E.g. "/Apps/Receiver.jsp" 089 */ 090 public AbstractRawClient(String theHost, int thePort, String thePath) { 091 setHost(theHost); 092 setPort(thePort); 093 setUriPath(thePath); 094 } 095 /** 096 * Constructor 097 * 098 * @param theUrl 099 * The URL to connect to. Note that if the URL refers to the 100 * "https" protocol, a {@link #setSocketFactory(ISocketFactory) 101 * SocketFactory} which uses TLS will be set. If custom 102 * certificates are used, a different factory may need to be 103 * provided manually. 104 */ 105 public AbstractRawClient(URL theUrl) { 106 setUrl(theUrl); 107 } 108 109 protected void closeSocket(Socket theSocket) { 110 ourLog.debug("Closing socket"); 111 try { 112 theSocket.close(); 113 } catch (IOException e) { 114 ourLog.warn("Problem closing socket", e); 115 } 116 } 117 118 protected Socket connect() throws IOException { 119 ourLog.debug("Creating new connection to {}:{} for URI {}", new Object[] { myHost, myPort, myPath }); 120 121 Socket socket = mySocketFactory.createClientSocket(); 122 socket.connect(new InetSocketAddress(myHost, myPort), myConnectionTimeout); 123 socket.setSoTimeout(mySoTimeout); 124 socket.setKeepAlive(myKeepAlive); 125 ourLog.trace("Connection established to {}:{}", myHost, myPort); 126 myOutputStream = new BufferedOutputStream(socket.getOutputStream()); 127 myInputStream = new BufferedInputStream(socket.getInputStream()); 128 return socket; 129 } 130 131 private IReceivable<String> doSendAndReceiveInternal(ISendable<?> theMessageToSend, Socket socket) throws IOException, DecodeException, SignatureVerificationException, EncodeException { 132 ourLog.trace("Entering doSendAndReceiveInternal()"); 133 134 Hl7OverHttpRequestEncoder enc = new Hl7OverHttpRequestEncoder(); 135 enc.setPath(myPath); 136 enc.setHost(myHost); 137 enc.setPort(myPort); 138 enc.setCharset(myCharset); 139 if (myAuthorizationCallback != null) { 140 enc.setUsername(myAuthorizationCallback.provideUsername(myPath)); 141 enc.setPassword(myAuthorizationCallback.providePassword(myPath)); 142 } 143 enc.setSigner(mySigner); 144 enc.setDataProvider(theMessageToSend); 145 146 ourLog.debug("Writing message to OutputStream"); 147 enc.encodeToOutputStream(myOutputStream); 148 myOutputStream.flush(); 149 150 ourLog.debug("Reading response from OutputStream"); 151 152 RawReceivable response = null; 153 long endTime = System.currentTimeMillis() + myResponseTimeout; 154 do { 155 try { 156 Hl7OverHttpResponseDecoder d = new Hl7OverHttpResponseDecoder(); 157 d.setSigner(mySigner); 158 d.setReadTimeout(myResponseTimeout); 159 d.readHeadersAndContentsFromInputStreamAndDecode(myInputStream); 160 161 response = new RawReceivable(d.getMessage()); 162 InetSocketAddress remoteSocketAddress = (InetSocketAddress) socket.getRemoteSocketAddress(); 163 String hostAddress = remoteSocketAddress.getAddress() != null ? remoteSocketAddress.getAddress().getHostAddress() : null; 164 response.addMetadata(MessageMetadataKeys.REMOTE_HOST_ADDRESS.name(), hostAddress); 165 166 if (d.isConnectionCloseHeaderPresent()) { 167 ourLog.debug("Found Connection=close header, closing socket"); 168 closeSocket(socket); 169 } 170 171 } catch (NoMessageReceivedException ex) { 172 ourLog.debug("No message received yet"); 173 } catch (IOException e) { 174 throw new DecodeException("Failed to read response from remote host", e); 175 } 176 } while (response == null && System.currentTimeMillis() < endTime); 177 178 ourLog.trace("Leaving doSendAndReceiveInternal()"); 179 return response; 180 } 181 182 /* 183 * (non-Javadoc) 184 * 185 * @see ca.uhn.hl7v2.hoh.raw.client.IClient#getHost() 186 */ 187 public String getHost() { 188 return myHost; 189 } 190 191 /* 192 * (non-Javadoc) 193 * 194 * @see ca.uhn.hl7v2.hoh.raw.client.IClient#getPort() 195 */ 196 public int getPort() { 197 return myPort; 198 } 199 200 /* 201 * (non-Javadoc) 202 * 203 * @see ca.uhn.hl7v2.hoh.raw.client.IClient#getSocketFactory() 204 */ 205 public ISocketFactory getSocketFactory() { 206 return mySocketFactory; 207 } 208 209 public int getSoTimeout() { 210 return mySoTimeout; 211 } 212 213 /* 214 * (non-Javadoc) 215 * 216 * @see ca.uhn.hl7v2.hoh.raw.client.IClient#getUri() 217 */ 218 public String getUriPath() { 219 return myPath; 220 } 221 222 /** 223 * {@inheritDoc} 224 */ 225 public URL getUrl() { 226 return myUrl; 227 } 228 229 /** 230 * {@inheritDoc} 231 */ 232 public String getUrlString() { 233 return getUrl().toExternalForm(); 234 } 235 236 public boolean isKeepAlive() { 237 return myKeepAlive; 238 } 239 240 boolean isSocketConnected(Socket socket) { 241 return socket != null && !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown(); 242 } 243 244 /** 245 * Subclasses must override to provide a connected socket 246 */ 247 protected abstract Socket provideSocket() throws IOException; 248 249 /** 250 * Returns the socket provided by {@link #provideSocket()}. This method will 251 * always be called after the request is finished. 252 */ 253 protected abstract void returnSocket(Socket theSocket); 254 255 /** 256 * Sends a message, waits for the response, and then returns the response if 257 * any 258 * 259 * @param theMessageToSend 260 * The message to send 261 * @return The returned message, as well as associated metadata 262 * @throws DecodeException 263 * If a problem occurs (read error, socket disconnect, etc.) 264 * during communication, or the response is invalid in some way. 265 * Note that IO errors in trying to connect to the remote host 266 * or sending the message are thrown directly (i.e. as 267 * {@link IOException}), but IO errors in reading the response 268 * are thrown as DecodeException 269 * @throws IOException 270 * If the client is unable to connect to the remote host 271 * @throws EncodeException 272 * If a failure occurs while encoding the message into a 273 * sendable HTTP request 274 */ 275 public synchronized IReceivable<String> sendAndReceive(ISendable<?> theMessageToSend) throws DecodeException, IOException, EncodeException { 276 277 Socket socket = provideSocket(); 278 try { 279 return doSendAndReceiveInternal(theMessageToSend, socket); 280 } catch (DecodeException e) { 281 ourLog.debug("Decode exception, going to close socket", e); 282 closeSocket(socket); 283 throw e; 284 } catch (IOException e) { 285 ourLog.debug("Caught IOException, going to close socket", e); 286 closeSocket(socket); 287 throw e; 288 } catch (SignatureVerificationException e) { 289 ourLog.debug("Failed to verify message signature", e); 290 throw new DecodeException("Failed to verify message signature", e); 291 } finally { 292 returnSocket(socket); 293 } 294 295 } 296 297 /* 298 * (non-Javadoc) 299 * 300 * @see 301 * ca.uhn.hl7v2.hoh.raw.client.IClient#setAuthorizationCallback(ca.uhn.hl7v2 302 * .hoh.api.IAuthorizationClientCallback) 303 */ 304 public void setAuthorizationCallback(IAuthorizationClientCallback theAuthorizationCallback) { 305 myAuthorizationCallback = theAuthorizationCallback; 306 } 307 308 /** 309 * {@inheritDoc} 310 */ 311 public void setCharset(Charset theCharset) { 312 if (theCharset == null) { 313 throw new NullPointerException("Charset can not be null"); 314 } 315 myCharset = theCharset; 316 } 317 318 /** 319 * {@inheritDoc} 320 */ 321 public void setHost(String theHost) { 322 myHost = theHost; 323 if (isBlank(theHost)) { 324 throw new IllegalArgumentException("Host can not be blank/null"); 325 } 326 } 327 328 /** 329 * {@inheritDoc} 330 */ 331 public void setKeepAlive(boolean theKeepAlive) { 332 myKeepAlive = theKeepAlive; 333 } 334 335 /** 336 * {@inheritDoc} 337 */ 338 public void setPort(int thePort) { 339 myPort = thePort; 340 if (thePort <= 0) { 341 throw new IllegalArgumentException("Port must be a positive integer"); 342 } 343 } 344 345 346 /** 347 * {@inheritDoc} 348 */ 349 public void setResponseTimeout(long theResponseTimeout) { 350 if (theResponseTimeout <= 0) { 351 throw new IllegalArgumentException("Timeout can not be <= 0"); 352 } 353 myResponseTimeout = theResponseTimeout; 354 } 355 356 /** 357 * {@inheritDoc} 358 */ 359 public void setSigner(ISigner theSigner) { 360 mySigner = theSigner; 361 } 362 363 /** 364 * {@inheritDoc} 365 */ 366 public void setSocketFactory(ISocketFactory theSocketFactory) { 367 if (theSocketFactory == null) { 368 throw new NullPointerException("Socket factory can not be null"); 369 } 370 mySocketFactory = theSocketFactory; 371 } 372 373 /** 374 * {@inheritDoc} 375 */ 376 public void setSoTimeout(int theSoTimeout) { 377 mySoTimeout = theSoTimeout; 378 } 379 380 /** 381 * {@inheritDoc} 382 */ 383 public void setUriPath(String thePath) { 384 myPath = thePath; 385 386 if (isBlank(thePath)) { 387 myPath = "/"; 388 } 389 if (!thePath.startsWith("/")) { 390 throw new IllegalArgumentException("Invalid URI (must start with '/'): " + thePath); 391 } else if (thePath.contains(" ")) { 392 throw new IllegalArgumentException("Invalid URI: " + thePath); 393 } 394 395 // Validate for syntax 396 try { 397 new URI("http://localhost" + thePath); 398 } catch (URISyntaxException e) { 399 throw new IllegalArgumentException("Invalid URI: " + thePath); 400 } 401 402 } 403 404 /** 405 * {@inheritDoc} 406 */ 407 public void setUrl(URL theUrl) { 408 setHost(extractHost(theUrl)); 409 setPort(extractPort(theUrl)); 410 setUriPath(extractUri(theUrl)); 411 412 myUrl = theUrl; 413 414 if (getSocketFactory() == DEFAULT_SOCKET_FACTORY && theUrl.getProtocol().toLowerCase().equals("https")) { 415 setSocketFactory(new TlsSocketFactory()); 416 } 417 } 418 419 /** 420 * {@inheritDoc} 421 */ 422 public void setUrlString(String theString) { 423 try { 424 URL url = new URL(theString); 425 setUrl(url); 426 } catch (MalformedURLException e) { 427 throw new IllegalArgumentException("URL is not valid. Must be in the form http[s]:"); 428 } 429 String protocol = myUrl.getProtocol().toLowerCase(); 430 if (!protocol.equals("http") && !protocol.equals("https")) { 431 throw new IllegalStateException("URL protocol must be http or https"); 432 } 433 434 } 435 436 private static String extractHost(URL theUrl) { 437 return theUrl.getHost(); 438 } 439 440 private static int extractPort(URL theUrl) { 441 return theUrl.getPort() != -1 ? theUrl.getPort() : theUrl.getDefaultPort(); 442 } 443 444 private static String extractUri(URL theUrl) { 445 return theUrl.getPath(); 446 } 447}