View Javadoc
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  import java.nio.charset.StandardCharsets;
17  
18  import ca.uhn.hl7v2.hoh.api.DecodeException;
19  import ca.uhn.hl7v2.hoh.api.EncodeException;
20  import ca.uhn.hl7v2.hoh.api.IAuthorizationClientCallback;
21  import ca.uhn.hl7v2.hoh.api.IClient;
22  import ca.uhn.hl7v2.hoh.api.IReceivable;
23  import ca.uhn.hl7v2.hoh.api.ISendable;
24  import ca.uhn.hl7v2.hoh.api.MessageMetadataKeys;
25  import ca.uhn.hl7v2.hoh.encoder.Hl7OverHttpRequestEncoder;
26  import ca.uhn.hl7v2.hoh.encoder.Hl7OverHttpResponseDecoder;
27  import ca.uhn.hl7v2.hoh.encoder.NoMessageReceivedException;
28  import ca.uhn.hl7v2.hoh.raw.api.RawReceivable;
29  import ca.uhn.hl7v2.hoh.sign.ISigner;
30  import ca.uhn.hl7v2.hoh.sign.SignatureVerificationException;
31  import ca.uhn.hl7v2.hoh.sockets.ISocketFactory;
32  import ca.uhn.hl7v2.hoh.sockets.StandardSocketFactory;
33  import ca.uhn.hl7v2.hoh.sockets.TlsSocketFactory;
34  
35  public abstract class AbstractRawClient implements IClient {
36  
37  	/**
38  	 * The default charset encoding (UTF-8)
39  	 */
40  	public static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
41  
42  	/**
43  	 * The default connection timeout in milliseconds: 10000
44  	 */
45  	public static final int DEFAULT_CONNECTION_TIMEOUT = 10000;
46  
47  	/**
48  	 * The default number of milliseconds to wait before timing out waiting for
49  	 * a response: 60000
50  	 */
51  	public static final int DEFAULT_RESPONSE_TIMEOUT = 60000;
52  
53  	private static final StandardSocketFactorytandardSocketFactory">StandardSocketFactory DEFAULT_SOCKET_FACTORY = new StandardSocketFactory();
54  
55  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HohRawClientSimple.class);
56  
57  	private IAuthorizationClientCallback myAuthorizationCallback;
58  	private Charset myCharset = DEFAULT_CHARSET;
59  	private String myHost;
60  	private BufferedInputStream myInputStream;
61  	private boolean myKeepAlive = true;
62  	private OutputStream myOutputStream;
63  	private String myPath;
64  	private int myPort;
65  	private long myResponseTimeout = DEFAULT_RESPONSE_TIMEOUT;
66  	private ISigner mySigner;
67  	private ISocketFactory mySocketFactory = DEFAULT_SOCKET_FACTORY;
68  	/**
69  	 * Socket so_timeout value for newly created sockets
70  	 */
71  	private int mySoTimeout = 5000;
72  	private URL myUrl;
73  	/**
74  	 * Constructor
75  	 */
76  	public AbstractRawClient() {
77  		// nothing
78  	}
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  	public AbstractRawClient(String theHost, int thePort, String thePath) {
91  		setHost(theHost);
92  		setPort(thePort);
93  		setUriPath(thePath);
94  	}
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 	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 {}", myHost, myPort, myPath);
120 
121 		Socket socket = mySocketFactory.createClientSocket();
122 		socket.connect(new InetSocketAddress(myHost, myPort), DEFAULT_CONNECTION_TIMEOUT);
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 }