Coverage Report - ca.uhn.hl7v2.hoh.raw.client.AbstractRawClient
 
Classes in this File Line Coverage Branch Coverage Complexity
AbstractRawClient
79%
121/152
45%
20/44
2.333
 
 1  
 package ca.uhn.hl7v2.hoh.raw.client;
 2  
 
 3  
 import static ca.uhn.hl7v2.hoh.util.StringUtils.isBlank;
 4  
 
 5  
 import java.io.BufferedInputStream;
 6  
 import java.io.BufferedOutputStream;
 7  
 import java.io.IOException;
 8  
 import java.io.OutputStream;
 9  
 import java.net.InetSocketAddress;
 10  
 import java.net.MalformedURLException;
 11  
 import java.net.Socket;
 12  
 import java.net.URI;
 13  
 import java.net.URISyntaxException;
 14  
 import java.net.URL;
 15  
 import java.nio.charset.Charset;
 16  
 
 17  
 import ca.uhn.hl7v2.hoh.api.DecodeException;
 18  
 import ca.uhn.hl7v2.hoh.api.EncodeException;
 19  
 import ca.uhn.hl7v2.hoh.api.IAuthorizationClientCallback;
 20  
 import ca.uhn.hl7v2.hoh.api.IClient;
 21  
 import ca.uhn.hl7v2.hoh.api.IReceivable;
 22  
 import ca.uhn.hl7v2.hoh.api.ISendable;
 23  
 import ca.uhn.hl7v2.hoh.api.MessageMetadataKeys;
 24  
 import ca.uhn.hl7v2.hoh.encoder.Hl7OverHttpRequestEncoder;
 25  
 import ca.uhn.hl7v2.hoh.encoder.Hl7OverHttpResponseDecoder;
 26  
 import ca.uhn.hl7v2.hoh.encoder.NoMessageReceivedException;
 27  
 import ca.uhn.hl7v2.hoh.raw.api.RawReceivable;
 28  
 import ca.uhn.hl7v2.hoh.sign.ISigner;
 29  
 import ca.uhn.hl7v2.hoh.sign.SignatureVerificationException;
 30  
 import ca.uhn.hl7v2.hoh.sockets.ISocketFactory;
 31  
 import ca.uhn.hl7v2.hoh.sockets.StandardSocketFactory;
 32  
 import ca.uhn.hl7v2.hoh.sockets.TlsSocketFactory;
 33  
 
 34  
 public abstract class AbstractRawClient implements IClient {
 35  
 
 36  
         /**
 37  
          * The default charset encoding (UTF-8)
 38  
          */
 39  5
         public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
 40  
 
 41  
         /**
 42  
          * The default connection timeout in milliseconds: 10000
 43  
          */
 44  
         public static final int DEFAULT_CONNECTION_TIMEOUT = 10000;
 45  
 
 46  
         /**
 47  
          * The default number of milliseconds to wait before timing out waiting for
 48  
          * a response: 60000
 49  
          */
 50  
         public static final int DEFAULT_RESPONSE_TIMEOUT = 60000;
 51  
 
 52  5
         private static final StandardSocketFactory DEFAULT_SOCKET_FACTORY = new StandardSocketFactory();
 53  
 
 54  5
         private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HohRawClientSimple.class);
 55  
 
 56  
         private IAuthorizationClientCallback myAuthorizationCallback;
 57  110
         private Charset myCharset = DEFAULT_CHARSET;
 58  110
         private int myConnectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
 59  
         private String myHost;
 60  
         private BufferedInputStream myInputStream;
 61  110
         private boolean myKeepAlive = true;
 62  
         private OutputStream myOutputStream;
 63  
         private String myPath;
 64  
         private int myPort;
 65  110
         private long myResponseTimeout = DEFAULT_RESPONSE_TIMEOUT;
 66  
         private ISigner mySigner;
 67  110
         private ISocketFactory mySocketFactory = DEFAULT_SOCKET_FACTORY;
 68  
         /**
 69  
          * Socket so_timeout value for newly created sockets
 70  
          */
 71  110
         private int mySoTimeout = 5000;
 72  
         private URL myUrl;
 73  
         /**
 74  
          * Constructor
 75  
          */
 76  40
         public AbstractRawClient() {
 77  
                 // nothing
 78  40
         }
 79  
         /**
 80  
          * Constructor
 81  
          * 
 82  
          * @param theHost
 83  
          *            The HOST (name/address). E.g. "192.168.1.1"
 84  
          * @param thePort
 85  
          *            The PORT. E.g. "8080"
 86  
          * @param thePath
 87  
          *            The path being requested (must either be blank or start with
 88  
          *            '/' and contain a path). E.g. "/Apps/Receiver.jsp"
 89  
          */
 90  50
         public AbstractRawClient(String theHost, int thePort, String thePath) {
 91  50
                 setHost(theHost);
 92  50
                 setPort(thePort);
 93  50
                 setUriPath(thePath);
 94  35
         }
 95  
         /**
 96  
          * Constructor
 97  
          * 
 98  
          * @param theUrl
 99  
          *            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  20
         public AbstractRawClient(URL theUrl) {
 106  20
                 setUrl(theUrl);
 107  20
         }
 108  
 
 109  
         protected void closeSocket(Socket theSocket) {
 110  100
                 ourLog.debug("Closing socket");
 111  
                 try {
 112  100
                         theSocket.close();
 113  0
                 } catch (IOException e) {
 114  0
                         ourLog.warn("Problem closing socket", e);
 115  100
                 }
 116  100
         }
 117  
 
 118  
         protected Socket connect() throws IOException {
 119  100
                 ourLog.debug("Creating new connection to {}:{} for URI {}", new Object[] { myHost, myPort, myPath });
 120  
 
 121  100
                 Socket socket = mySocketFactory.createClientSocket();
 122  100
                 socket.connect(new InetSocketAddress(myHost, myPort), myConnectionTimeout);
 123  95
                 socket.setSoTimeout(mySoTimeout);
 124  95
                 socket.setKeepAlive(myKeepAlive);
 125  95
                 ourLog.trace("Connection established to {}:{}", myHost, myPort);
 126  95
                 myOutputStream = new BufferedOutputStream(socket.getOutputStream());
 127  95
                 myInputStream = new BufferedInputStream(socket.getInputStream());
 128  95
                 return socket;
 129  
         }
 130  
 
 131  
         private IReceivable<String> doSendAndReceiveInternal(ISendable<?> theMessageToSend, Socket socket) throws IOException, DecodeException, SignatureVerificationException, EncodeException {
 132  115
                 ourLog.trace("Entering doSendAndReceiveInternal()");
 133  
                 
 134  115
                 Hl7OverHttpRequestEncoder enc = new Hl7OverHttpRequestEncoder();
 135  115
                 enc.setPath(myPath);
 136  115
                 enc.setHost(myHost);
 137  115
                 enc.setPort(myPort);
 138  115
                 enc.setCharset(myCharset);
 139  115
                 if (myAuthorizationCallback != null) {
 140  115
                         enc.setUsername(myAuthorizationCallback.provideUsername(myPath));
 141  115
                         enc.setPassword(myAuthorizationCallback.providePassword(myPath));
 142  
                 }
 143  115
                 enc.setSigner(mySigner);
 144  115
                 enc.setDataProvider(theMessageToSend);
 145  
 
 146  115
                 ourLog.debug("Writing message to OutputStream");
 147  115
                 enc.encodeToOutputStream(myOutputStream);
 148  115
                 myOutputStream.flush();
 149  
 
 150  110
                 ourLog.debug("Reading response from OutputStream");
 151  
 
 152  110
                 RawReceivable response = null;
 153  110
                 long endTime = System.currentTimeMillis() + myResponseTimeout;
 154  
                 do {
 155  
                         try {
 156  110
                                 Hl7OverHttpResponseDecoder d = new Hl7OverHttpResponseDecoder();
 157  110
                                 d.setSigner(mySigner);
 158  110
                                 d.setReadTimeout(myResponseTimeout);
 159  110
                                 d.readHeadersAndContentsFromInputStreamAndDecode(myInputStream);
 160  
 
 161  110
                                 response = new RawReceivable(d.getMessage());
 162  110
                                 InetSocketAddress remoteSocketAddress = (InetSocketAddress) socket.getRemoteSocketAddress();
 163  110
                                 String hostAddress = remoteSocketAddress.getAddress() != null ? remoteSocketAddress.getAddress().getHostAddress() : null;
 164  110
                                 response.addMetadata(MessageMetadataKeys.REMOTE_HOST_ADDRESS.name(), hostAddress);
 165  
 
 166  110
                                 if (d.isConnectionCloseHeaderPresent()) {
 167  30
                                         ourLog.debug("Found Connection=close header, closing socket");
 168  30
                                         closeSocket(socket);
 169  
                                 }
 170  
                                 
 171  0
                         } catch (NoMessageReceivedException ex) {
 172  0
                                 ourLog.debug("No message received yet");
 173  0
                         } catch (IOException e) {
 174  0
                                 throw new DecodeException("Failed to read response from remote host", e);
 175  110
                         }
 176  110
                 } while (response == null && System.currentTimeMillis() < endTime);
 177  
 
 178  110
                 ourLog.trace("Leaving doSendAndReceiveInternal()");
 179  110
                 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  100
                 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  100
                 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  40
                 return mySocketFactory;
 207  
         }
 208  
 
 209  
         public int getSoTimeout() {
 210  0
                 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  25
                 return myPath;
 220  
         }
 221  
 
 222  
         /**
 223  
          * {@inheritDoc}
 224  
          */
 225  
         public URL getUrl() {
 226  70
                 return myUrl;
 227  
         }
 228  
 
 229  
         /**
 230  
          * {@inheritDoc}
 231  
          */
 232  
         public String getUrlString() {
 233  0
                 return getUrl().toExternalForm();
 234  
         }
 235  
 
 236  
         public boolean isKeepAlive() {
 237  0
                 return myKeepAlive;
 238  
         }
 239  
 
 240  
         boolean isSocketConnected(Socket socket) {
 241  0
                 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  120
                 Socket socket = provideSocket();
 278  
                 try {
 279  225
                         return doSendAndReceiveInternal(theMessageToSend, socket);
 280  0
                 } catch (DecodeException e) {
 281  0
                         ourLog.debug("Decode exception, going to close socket", e);
 282  0
                         closeSocket(socket);
 283  0
                         throw e;
 284  5
                 } catch (IOException e) {
 285  5
                         ourLog.debug("Caught IOException, going to close socket", e);
 286  5
                         closeSocket(socket);
 287  5
                         throw e;
 288  0
                 } catch (SignatureVerificationException e) {
 289  0
                         ourLog.debug("Failed to verify message signature", e);
 290  0
                         throw new DecodeException("Failed to verify message signature", e);
 291  
                 } finally {
 292  115
                         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  65
                 myAuthorizationCallback = theAuthorizationCallback;
 306  65
         }
 307  
 
 308  
         /**
 309  
          * {@inheritDoc}
 310  
          */
 311  
         public void setCharset(Charset theCharset) {
 312  0
                 if (theCharset == null) {
 313  0
                         throw new NullPointerException("Charset can not be null");
 314  
                 }
 315  0
                 myCharset = theCharset;
 316  0
         }
 317  
 
 318  
         /**
 319  
          * {@inheritDoc}
 320  
          */
 321  
         public void setHost(String theHost) {
 322  110
                 myHost = theHost;
 323  110
                 if (isBlank(theHost)) {
 324  0
                         throw new IllegalArgumentException("Host can not be blank/null");
 325  
                 }
 326  110
         }
 327  
 
 328  
         /**
 329  
          * {@inheritDoc}
 330  
          */
 331  
         public void setKeepAlive(boolean theKeepAlive) {
 332  10
                 myKeepAlive = theKeepAlive;
 333  10
         }
 334  
 
 335  
         /**
 336  
          * {@inheritDoc}
 337  
          */
 338  
         public void setPort(int thePort) {
 339  110
                 myPort = thePort;
 340  110
                 if (thePort <= 0) {
 341  0
                         throw new IllegalArgumentException("Port must be a positive integer");
 342  
                 }
 343  110
         }
 344  
 
 345  
 
 346  
         /**
 347  
          * {@inheritDoc}
 348  
          */
 349  
         public void setResponseTimeout(long theResponseTimeout) {
 350  10
                 if (theResponseTimeout <= 0) {
 351  0
                         throw new IllegalArgumentException("Timeout can not be <= 0");
 352  
                 }
 353  10
                 myResponseTimeout = theResponseTimeout;
 354  10
         }
 355  
 
 356  
         /**
 357  
          * {@inheritDoc}
 358  
          */
 359  
         public void setSigner(ISigner theSigner) {
 360  0
                 mySigner = theSigner;
 361  0
         }
 362  
 
 363  
         /**
 364  
          * {@inheritDoc}
 365  
          */
 366  
         public void setSocketFactory(ISocketFactory theSocketFactory) {
 367  10
                 if (theSocketFactory == null) {
 368  0
                         throw new NullPointerException("Socket factory can not be null");
 369  
                 }
 370  10
                 mySocketFactory = theSocketFactory;
 371  10
         }
 372  
 
 373  
         /**
 374  
          * {@inheritDoc}
 375  
          */
 376  
         public void setSoTimeout(int theSoTimeout) {
 377  15
                 mySoTimeout = theSoTimeout;
 378  15
         }
 379  
 
 380  
         /**
 381  
          * {@inheritDoc}
 382  
          */
 383  
         public void setUriPath(String thePath) {
 384  110
                 myPath = thePath;
 385  
 
 386  110
                 if (isBlank(thePath)) {
 387  0
                         myPath = "/";
 388  
                 }
 389  110
                 if (!thePath.startsWith("/")) {
 390  5
                         throw new IllegalArgumentException("Invalid URI (must start with '/'): " + thePath);
 391  105
                 } else if (thePath.contains(" ")) {
 392  5
                         throw new IllegalArgumentException("Invalid URI: " + thePath);
 393  
                 }
 394  
                 
 395  
                 // Validate for syntax
 396  
                 try {
 397  100
                         new URI("http://localhost" + thePath);
 398  5
                 } catch (URISyntaxException e) {
 399  5
                         throw new IllegalArgumentException("Invalid URI: " + thePath);
 400  95
                 }
 401  
                 
 402  95
         }
 403  
 
 404  
         /**
 405  
          * {@inheritDoc}
 406  
          */
 407  
         public void setUrl(URL theUrl) {
 408  40
                 setHost(extractHost(theUrl));
 409  40
                 setPort(extractPort(theUrl));
 410  40
                 setUriPath(extractUri(theUrl));
 411  
 
 412  40
                 myUrl = theUrl;
 413  
 
 414  40
                 if (getSocketFactory() == DEFAULT_SOCKET_FACTORY && theUrl.getProtocol().toLowerCase().equals("https")) {
 415  5
                         setSocketFactory(new TlsSocketFactory());
 416  
                 }
 417  40
         }
 418  
 
 419  
         /**
 420  
          * {@inheritDoc}
 421  
          */
 422  
         public void setUrlString(String theString) {
 423  
                 try {
 424  5
                         URL url = new URL(theString);
 425  5
                         setUrl(url);
 426  0
                 } catch (MalformedURLException e) {
 427  0
                         throw new IllegalArgumentException("URL is not valid. Must be in the form http[s]:");
 428  5
                 }
 429  5
                 String protocol = myUrl.getProtocol().toLowerCase();
 430  5
                 if (!protocol.equals("http") && !protocol.equals("https")) {
 431  0
                         throw new IllegalStateException("URL protocol must be http or https");
 432  
                 }
 433  
 
 434  5
         }
 435  
 
 436  
         private static String extractHost(URL theUrl) {
 437  40
                 return theUrl.getHost();
 438  
         }
 439  
 
 440  
         private static int extractPort(URL theUrl) {
 441  40
                 return theUrl.getPort() != -1 ? theUrl.getPort() : theUrl.getDefaultPort();
 442  
         }
 443  
 
 444  
         private static String extractUri(URL theUrl) {
 445  40
                 return theUrl.getPath();
 446  
         }
 447  
 }