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}