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 | |
|
38 | |
|
39 | 5 | public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); |
40 | |
|
41 | |
|
42 | |
|
43 | |
|
44 | |
public static final int DEFAULT_CONNECTION_TIMEOUT = 10000; |
45 | |
|
46 | |
|
47 | |
|
48 | |
|
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 | |
|
70 | |
|
71 | 110 | private int mySoTimeout = 5000; |
72 | |
private URL myUrl; |
73 | |
|
74 | |
|
75 | |
|
76 | 40 | public AbstractRawClient() { |
77 | |
|
78 | 40 | } |
79 | |
|
80 | |
|
81 | |
|
82 | |
|
83 | |
|
84 | |
|
85 | |
|
86 | |
|
87 | |
|
88 | |
|
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 | |
|
97 | |
|
98 | |
|
99 | |
|
100 | |
|
101 | |
|
102 | |
|
103 | |
|
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 | |
|
184 | |
|
185 | |
|
186 | |
|
187 | |
public String getHost() { |
188 | 100 | return myHost; |
189 | |
} |
190 | |
|
191 | |
|
192 | |
|
193 | |
|
194 | |
|
195 | |
|
196 | |
public int getPort() { |
197 | 100 | return myPort; |
198 | |
} |
199 | |
|
200 | |
|
201 | |
|
202 | |
|
203 | |
|
204 | |
|
205 | |
public ISocketFactory getSocketFactory() { |
206 | 40 | return mySocketFactory; |
207 | |
} |
208 | |
|
209 | |
public int getSoTimeout() { |
210 | 0 | return mySoTimeout; |
211 | |
} |
212 | |
|
213 | |
|
214 | |
|
215 | |
|
216 | |
|
217 | |
|
218 | |
public String getUriPath() { |
219 | 25 | return myPath; |
220 | |
} |
221 | |
|
222 | |
|
223 | |
|
224 | |
|
225 | |
public URL getUrl() { |
226 | 70 | return myUrl; |
227 | |
} |
228 | |
|
229 | |
|
230 | |
|
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 | |
|
246 | |
|
247 | |
protected abstract Socket provideSocket() throws IOException; |
248 | |
|
249 | |
|
250 | |
|
251 | |
|
252 | |
|
253 | |
protected abstract void returnSocket(Socket theSocket); |
254 | |
|
255 | |
|
256 | |
|
257 | |
|
258 | |
|
259 | |
|
260 | |
|
261 | |
|
262 | |
|
263 | |
|
264 | |
|
265 | |
|
266 | |
|
267 | |
|
268 | |
|
269 | |
|
270 | |
|
271 | |
|
272 | |
|
273 | |
|
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 | |
|
299 | |
|
300 | |
|
301 | |
|
302 | |
|
303 | |
|
304 | |
public void setAuthorizationCallback(IAuthorizationClientCallback theAuthorizationCallback) { |
305 | 65 | myAuthorizationCallback = theAuthorizationCallback; |
306 | 65 | } |
307 | |
|
308 | |
|
309 | |
|
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 | |
|
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 | |
|
330 | |
|
331 | |
public void setKeepAlive(boolean theKeepAlive) { |
332 | 10 | myKeepAlive = theKeepAlive; |
333 | 10 | } |
334 | |
|
335 | |
|
336 | |
|
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 | |
|
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 | |
|
358 | |
|
359 | |
public void setSigner(ISigner theSigner) { |
360 | 0 | mySigner = theSigner; |
361 | 0 | } |
362 | |
|
363 | |
|
364 | |
|
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 | |
|
375 | |
|
376 | |
public void setSoTimeout(int theSoTimeout) { |
377 | 15 | mySoTimeout = theSoTimeout; |
378 | 15 | } |
379 | |
|
380 | |
|
381 | |
|
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 | |
|
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 | |
|
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 | |
|
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 | |
} |