View Javadoc

1   package ca.uhn.hl7v2.hoh.raw.client;
2   
3   import static ca.uhn.hl7v2.hoh.util.StringUtils.*;
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  	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  	private static final StandardSocketFactory DEFAULT_SOCKET_FACTORY = new StandardSocketFactory();
53  
54  	private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(HohRawClientSimple.class);
55  
56  	/**
57  	 * Socket so_timeout value for newly created sockets
58  	 */
59  	static final int SO_TIMEOUT = 500;
60  
61  	private IAuthorizationClientCallback myAuthorizationCallback;
62  	private Charset myCharset = DEFAULT_CHARSET;
63  	private int myConnectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
64  	private String myHost;
65  	private BufferedInputStream myInputStream;
66  	private OutputStream myOutputStream;
67  	private String myPath;
68  	private int myPort;
69  	private long myResponseTimeout = DEFAULT_RESPONSE_TIMEOUT;
70  	private ISigner mySigner;
71  	private ISocketFactory mySocketFactory = DEFAULT_SOCKET_FACTORY;
72  	private URL myUrl;
73  
74  	/**
75  	 * Constructor
76  	 */
77  	public AbstractRawClient() {
78  		// nothing
79  	}
80  
81  	/**
82  	 * Constructor
83  	 * 
84  	 * @param theHost
85  	 *            The HOST (name/address). E.g. "192.168.1.1"
86  	 * @param thePort
87  	 *            The PORT. E.g. "8080"
88  	 * @param thePath
89  	 *            The path being requested (must either be blank or start with
90  	 *            '/' and contain a path). E.g. "/Apps/Receiver.jsp"
91  	 */
92  	public AbstractRawClient(String theHost, int thePort, String thePath) {
93  		setHost(theHost);
94  		setPort(thePort);
95  		setUriPath(thePath);
96  	}
97  
98  	/**
99  	 * Constructor
100 	 * 
101 	 * @param theUrl
102 	 *            The URL to connect to. Note that if the URL refers to the
103 	 *            "https" protocol, a {@link #setSocketFactory(ISocketFactory)
104 	 *            SocketFactory} which uses TLS will be set. If custom
105 	 *            certificates are used, a different factory may need to be
106 	 *            provided manually.
107 	 */
108 	public AbstractRawClient(URL theUrl) {
109 		setUrl(theUrl);
110 	}
111 
112 	protected void closeSocket(Socket theSocket) {
113 		ourLog.debug("Closing socket");
114 		try {
115 			theSocket.close();
116 		} catch (IOException e) {
117 			ourLog.warn("Problem closing socket", e);
118 		}
119 	}
120 
121 	protected Socket connect() throws IOException {
122 		ourLog.debug("Creating new connection to {}:{} for URI {}", new Object[] { myHost, myPort, myPath });
123 
124 		Socket socket = mySocketFactory.createClientSocket();
125 		socket.connect(new InetSocketAddress(myHost, myPort), myConnectionTimeout);
126 		socket.setSoTimeout(SO_TIMEOUT);
127 		ourLog.trace("Connection established to {}:{}", myHost, myPort);
128 		myOutputStream = new BufferedOutputStream(socket.getOutputStream());
129 		myInputStream = new BufferedInputStream(socket.getInputStream());
130 		return socket;
131 	}
132 
133 	private IReceivable<String> doSendAndReceiveInternal(ISendable<?> theMessageToSend, Socket socket) throws IOException, DecodeException, SignatureVerificationException, EncodeException {
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 			} catch (NoMessageReceivedException ex) {
167 				ourLog.debug("No message received yet");
168 			} catch (IOException e) {
169 				throw new DecodeException("Failed to read response from remote host", e);
170 			}
171 		} while (response == null && System.currentTimeMillis() < endTime);
172 
173 		return response;
174 	}
175 
176 	/*
177 	 * (non-Javadoc)
178 	 * 
179 	 * @see ca.uhn.hl7v2.hoh.raw.client.IClient#getHost()
180 	 */
181 	public String getHost() {
182 		return myHost;
183 	}
184 
185 	/*
186 	 * (non-Javadoc)
187 	 * 
188 	 * @see ca.uhn.hl7v2.hoh.raw.client.IClient#getPort()
189 	 */
190 	public int getPort() {
191 		return myPort;
192 	}
193 
194 	/*
195 	 * (non-Javadoc)
196 	 * 
197 	 * @see ca.uhn.hl7v2.hoh.raw.client.IClient#getSocketFactory()
198 	 */
199 	public ISocketFactory getSocketFactory() {
200 		return mySocketFactory;
201 	}
202 
203 	/*
204 	 * (non-Javadoc)
205 	 * 
206 	 * @see ca.uhn.hl7v2.hoh.raw.client.IClient#getUri()
207 	 */
208 	public String getUriPath() {
209 		return myPath;
210 	}
211 
212 	/**
213 	 * {@inheritDoc}
214 	 */
215 	public URL getUrl() {
216 		return myUrl;
217 	}
218 
219 	/**
220 	 * {@inheritDoc}
221 	 */
222 	public String getUrlString() {
223 		return getUrl().toExternalForm();
224 	}
225 
226 	boolean isSocketConnected(Socket socket) {
227 		return socket != null && !socket.isClosed() && !socket.isInputShutdown() && !socket.isOutputShutdown();
228 	}
229 
230 	/**
231 	 * Subclasses must override to provide a connected socket
232 	 */
233 	protected abstract Socket provideSocket() throws IOException;
234 
235 	/**
236 	 * Returns the socket provided by {@link #provideSocket()}. This method will
237 	 * always be called after the request is finished.
238 	 */
239 	protected abstract void returnSocket(Socket theSocket);
240 
241 	/**
242 	 * Sends a message, waits for the response, and then returns the response if
243 	 * any
244 	 * 
245 	 * @param theMessageToSend
246 	 *            The message to send
247 	 * @return The returned message, as well as associated metadata
248 	 * @throws DecodeException
249 	 *             If a problem occurs (read error, socket disconnect, etc.)
250 	 *             during communication, or the response is invalid in some way.
251 	 *             Note that IO errors in trying to connect to the remote host
252 	 *             or sending the message are thrown directly (i.e. as
253 	 *             {@link IOException}), but IO errors in reading the response
254 	 *             are thrown as DecodeException
255 	 * @throws IOException
256 	 *             If the client is unable to connect to the remote host
257 	 * @throws EncodeException
258 	 *             If a failure occurs while encoding the message into a
259 	 *             sendable HTTP request
260 	 */
261 	public IReceivable<String> sendAndReceive(ISendable<?> theMessageToSend) throws DecodeException, IOException, EncodeException {
262 
263 		Socket socket = provideSocket();
264 		try {
265 			return doSendAndReceiveInternal(theMessageToSend, socket);
266 		} catch (DecodeException e) {
267 			ourLog.debug("Decode exception, going to close socket", e);
268 			closeSocket(socket);
269 			throw e;
270 		} catch (IOException e) {
271 			ourLog.debug("Caught IOException, going to close socket", e);
272 			closeSocket(socket);
273 			throw e;
274 		} catch (SignatureVerificationException e) {
275 			ourLog.debug("Failed to verify message signature", e);
276 			throw new DecodeException("Failed to verify message signature", e);
277 		} finally {
278 			returnSocket(socket);
279 		}
280 
281 	}
282 
283 	/*
284 	 * (non-Javadoc)
285 	 * 
286 	 * @see
287 	 * ca.uhn.hl7v2.hoh.raw.client.IClient#setAuthorizationCallback(ca.uhn.hl7v2
288 	 * .hoh.api.IAuthorizationClientCallback)
289 	 */
290 	public void setAuthorizationCallback(IAuthorizationClientCallback theAuthorizationCallback) {
291 		myAuthorizationCallback = theAuthorizationCallback;
292 	}
293 
294 	/**
295 	 * {@inheritDoc}
296 	 */
297 	public void setCharset(Charset theCharset) {
298 		if (theCharset == null) {
299 			throw new NullPointerException("Charset can not be null");
300 		}
301 		myCharset = theCharset;
302 	}
303 
304 	/**
305 	 * {@inheritDoc}
306 	 */
307 	public void setHost(String theHost) {
308 		myHost = theHost;
309 		if (isBlank(theHost)) {
310 			throw new IllegalArgumentException("Host can not be blank/null");
311 		}
312 	}
313 
314 
315 	/**
316 	 * {@inheritDoc}
317 	 */
318 	public void setPort(int thePort) {
319 		myPort = thePort;
320 		if (thePort <= 0) {
321 			throw new IllegalArgumentException("Port must be a positive integer");
322 		}
323 	}
324 
325 	/**
326 	 * {@inheritDoc}
327 	 */
328 	public void setResponseTimeout(long theResponseTimeout) {
329 		if (theResponseTimeout <= 0) {
330 			throw new IllegalArgumentException("Timeout can not be <= 0");
331 		}
332 		myResponseTimeout = theResponseTimeout;
333 	}
334 
335 	/*
336 	 * (non-Javadoc)
337 	 * 
338 	 * @see
339 	 * ca.uhn.hl7v2.hoh.raw.client.IClient#setSigner(ca.uhn.hl7v2.hoh.sign.ISigner
340 	 * )
341 	 */
342 	public void setSigner(ISigner theSigner) {
343 		mySigner = theSigner;
344 	}
345 
346 	/*
347 	 * (non-Javadoc)
348 	 * 
349 	 * @see
350 	 * ca.uhn.hl7v2.hoh.raw.client.IClient#setSocketFactory(ca.uhn.hl7v2.hoh
351 	 * .sockets.ISocketFactory)
352 	 */
353 	public void setSocketFactory(ISocketFactory theSocketFactory) {
354 		if (theSocketFactory == null) {
355 			throw new NullPointerException("Socket factory can not be null");
356 		}
357 		mySocketFactory = theSocketFactory;
358 	}
359 
360 	/**
361 	 * {@inheritDoc}
362 	 */
363 	public void setUriPath(String thePath) {
364 		myPath = thePath;
365 
366 		if (isBlank(thePath)) {
367 			myPath = "/";
368 		}
369 		if (!thePath.startsWith("/")) {
370 			throw new IllegalArgumentException("Invalid URI (must start with '/'): " + thePath);
371 		} else if (thePath.contains(" ")) {
372 			throw new IllegalArgumentException("Invalid URI: " + thePath);
373 		}
374 		
375 		// Validate for syntax
376 		try {
377 			new URI("http://localhost" + thePath);
378 		} catch (URISyntaxException e) {
379 			throw new IllegalArgumentException("Invalid URI: " + thePath);
380 		}
381 		
382 	}
383 
384 	/**
385 	 * {@inheritDoc}
386 	 */
387 	public void setUrl(URL theUrl) {
388 		setHost(extractHost(theUrl));
389 		setPort(extractPort(theUrl));
390 		setUriPath(extractUri(theUrl));
391 
392 		myUrl = theUrl;
393 
394 		if (getSocketFactory() == DEFAULT_SOCKET_FACTORY && theUrl.getProtocol().toLowerCase().equals("https")) {
395 			setSocketFactory(new TlsSocketFactory());
396 		}
397 	}
398 
399 	/**
400 	 * {@inheritDoc}
401 	 */
402 	public void setUrlString(String theString) {
403 		try {
404 			URL url = new URL(theString);
405 			setUrl(url);
406 		} catch (MalformedURLException e) {
407 			throw new IllegalArgumentException("URL is not valid. Must be in the form http[s]:");
408 		}
409 		String protocol = myUrl.getProtocol().toLowerCase();
410 		if (!protocol.equals("http") && !protocol.equals("https")) {
411 			throw new IllegalStateException("URL protocol must be http or https");
412 		}
413 
414 	}
415 
416 	private static String extractHost(URL theUrl) {
417 		return theUrl.getHost();
418 	}
419 
420 	private static int extractPort(URL theUrl) {
421 		return theUrl.getPort() != -1 ? theUrl.getPort() : theUrl.getDefaultPort();
422 	}
423 
424 	private static String extractUri(URL theUrl) {
425 		return theUrl.getPath();
426 	}
427 }