001package ca.uhn.fhir.rest.client;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2016 University Health Network
008 * %%
009 * Licensed under the Apache License, Version 2.0 (the "License");
010 * you may not use this file except in compliance with the License.
011 * You may obtain a copy of the License at
012 * 
013 *      http://www.apache.org/licenses/LICENSE-2.0
014 * 
015 * Unless required by applicable law or agreed to in writing, software
016 * distributed under the License is distributed on an "AS IS" BASIS,
017 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
018 * See the License for the specific language governing permissions and
019 * limitations under the License.
020 * #L%
021 */
022
023import java.lang.reflect.InvocationHandler;
024import java.lang.reflect.Method;
025import java.lang.reflect.Proxy;
026import java.util.Collections;
027import java.util.HashMap;
028import java.util.HashSet;
029import java.util.Map;
030import java.util.Set;
031import java.util.concurrent.TimeUnit;
032
033import org.apache.commons.lang3.StringUtils;
034import org.apache.commons.lang3.Validate;
035import org.apache.http.HttpHost;
036import org.apache.http.auth.AuthScope;
037import org.apache.http.auth.UsernamePasswordCredentials;
038import org.apache.http.client.CredentialsProvider;
039import org.apache.http.client.HttpClient;
040import org.apache.http.client.config.RequestConfig;
041import org.apache.http.impl.client.BasicCredentialsProvider;
042import org.apache.http.impl.client.HttpClientBuilder;
043import org.apache.http.impl.client.HttpClients;
044import org.apache.http.impl.client.ProxyAuthenticationStrategy;
045import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
046import org.hl7.fhir.instance.model.api.IBaseResource;
047import org.hl7.fhir.instance.model.api.IPrimitiveType;
048
049import ca.uhn.fhir.context.ConfigurationException;
050import ca.uhn.fhir.context.FhirContext;
051import ca.uhn.fhir.context.FhirVersionEnum;
052import ca.uhn.fhir.rest.client.api.IRestfulClient;
053import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
054import ca.uhn.fhir.rest.client.exceptions.FhirClientInappropriateForServerException;
055import ca.uhn.fhir.rest.method.BaseMethodBinding;
056import ca.uhn.fhir.rest.server.Constants;
057import ca.uhn.fhir.util.FhirTerser;
058
059public class RestfulClientFactory implements IRestfulClientFactory {
060
061        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulClientFactory.class);
062        private int myConnectionRequestTimeout = DEFAULT_CONNECTION_REQUEST_TIMEOUT;
063        private int myConnectTimeout = DEFAULT_CONNECT_TIMEOUT;
064        private FhirContext myContext;
065        private HttpClient myHttpClient;
066        private Map<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory> myInvocationHandlers = new HashMap<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory>();
067        private HttpHost myProxy;
068        private ServerValidationModeEnum myServerValidationMode = DEFAULT_SERVER_VALIDATION_MODE;
069        private int mySocketTimeout = DEFAULT_SOCKET_TIMEOUT;
070        private Set<String> myValidatedServerBaseUrls = Collections.synchronizedSet(new HashSet<String>());
071        private int myPoolMaxTotal = DEFAULT_POOL_MAX;
072        private int myPoolMaxPerRoute = DEFAULT_POOL_MAX_PER_ROUTE;
073        
074        /**
075         * Constructor
076         */
077        public RestfulClientFactory() {
078        }
079
080        /**
081         * Constructor
082         * 
083         * @param theFhirContext
084         *            The context
085         */
086        public RestfulClientFactory(FhirContext theFhirContext) {
087                myContext = theFhirContext;
088        }
089
090        @Override
091        public int getConnectionRequestTimeout() {
092                return myConnectionRequestTimeout;
093        }
094
095        @Override
096        public int getConnectTimeout() {
097                return myConnectTimeout;
098        }
099
100        @Override
101        public synchronized HttpClient getHttpClient() {
102                if (myHttpClient == null) {
103
104                        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS);
105                        connectionManager.setMaxTotal(myPoolMaxTotal);
106                        connectionManager.setDefaultMaxPerRoute(myPoolMaxPerRoute);
107                        
108                        //@formatter:off
109                        RequestConfig defaultRequestConfig = RequestConfig.custom()
110                                    .setSocketTimeout(mySocketTimeout)
111                                    .setConnectTimeout(myConnectTimeout)
112                                    .setConnectionRequestTimeout(myConnectionRequestTimeout)
113                                    .setStaleConnectionCheckEnabled(true)
114                                    .setProxy(myProxy)
115                                    .build();
116                        
117                        HttpClientBuilder builder = HttpClients.custom()
118                                .setConnectionManager(connectionManager)
119                                .setDefaultRequestConfig(defaultRequestConfig)
120                                .disableCookieManagement();
121                        
122                        if (myProxy != null && StringUtils.isNotBlank(myProxyUsername) && StringUtils.isNotBlank(myProxyPassword)) {
123                                CredentialsProvider credsProvider = new BasicCredentialsProvider();
124                                credsProvider.setCredentials(new AuthScope(myProxy.getHostName(), myProxy.getPort()), new UsernamePasswordCredentials(myProxyUsername, myProxyPassword));
125                                builder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy());
126                                builder.setDefaultCredentialsProvider(credsProvider);
127                        }
128                        
129                        myHttpClient = builder.build();
130                        //@formatter:on
131
132                }
133
134                return myHttpClient;
135        }
136        
137        private String myProxyUsername;
138        private String myProxyPassword;
139
140        @Override
141        public void setProxyCredentials(String theUsername, String thePassword) {
142                myProxyUsername=theUsername;
143                myProxyPassword=thePassword;
144        }
145        
146        @Override
147        public ServerValidationModeEnum getServerValidationMode() {
148                return myServerValidationMode;
149        }
150
151        @Override
152        public int getSocketTimeout() {
153                return mySocketTimeout;
154        }
155
156        @Override
157        public int getPoolMaxTotal() {
158                return myPoolMaxTotal;
159        }
160
161        @Override
162        public int getPoolMaxPerRoute() {
163                return myPoolMaxPerRoute;
164        }
165
166        @SuppressWarnings("unchecked")
167        private <T extends IRestfulClient> T instantiateProxy(Class<T> theClientType, InvocationHandler theInvocationHandler) {
168                T proxy = (T) Proxy.newProxyInstance(theClientType.getClassLoader(), new Class[] { theClientType }, theInvocationHandler);
169                return proxy;
170        }
171
172        /**
173         * Instantiates a new client instance
174         * 
175         * @param theClientType
176         *            The client type, which is an interface type to be instantiated
177         * @param theServerBase
178         *            The URL of the base for the restful FHIR server to connect to
179         * @return A newly created client
180         * @throws ConfigurationException
181         *             If the interface type is not an interface
182         */
183        @Override
184        public synchronized <T extends IRestfulClient> T newClient(Class<T> theClientType, String theServerBase) {
185                if (!theClientType.isInterface()) {
186                        throw new ConfigurationException(theClientType.getCanonicalName() + " is not an interface");
187                }
188
189                
190                ClientInvocationHandlerFactory invocationHandler = myInvocationHandlers.get(theClientType);
191                if (invocationHandler == null) {
192                        HttpClient httpClient = getHttpClient();
193                        invocationHandler = new ClientInvocationHandlerFactory(httpClient, myContext, theServerBase, theClientType);
194                        for (Method nextMethod : theClientType.getMethods()) {
195                                BaseMethodBinding<?> binding = BaseMethodBinding.bindMethod(nextMethod, myContext, null);
196                                invocationHandler.addBinding(nextMethod, binding);
197                        }
198                        myInvocationHandlers.put(theClientType, invocationHandler);
199                }
200
201                T proxy = instantiateProxy(theClientType, invocationHandler.newInvocationHandler(this));
202
203                return proxy;
204        }
205
206        @Override
207        public synchronized IGenericClient newGenericClient(String theServerBase) {
208                HttpClient httpClient = getHttpClient();
209                return new GenericClient(myContext, httpClient, theServerBase, this);
210        }
211
212        /**
213         * This method is internal to HAPI - It may change in future versions, use with caution.
214         */
215        public void validateServerBaseIfConfiguredToDoSo(String theServerBase, HttpClient theHttpClient, BaseClient theClient) {
216                String serverBase = normalizeBaseUrlForMap(theServerBase);
217
218                switch (myServerValidationMode) {
219                case NEVER:
220                        break;
221                case ONCE:
222                        if (!myValidatedServerBaseUrls.contains(serverBase)) {
223                                validateServerBase(serverBase, theHttpClient, theClient);
224                        }
225                        break;
226                }
227
228        }
229
230        private String normalizeBaseUrlForMap(String theServerBase) {
231                String serverBase = theServerBase;
232                if (!serverBase.endsWith("/")) {
233                        serverBase = serverBase + "/";
234                }
235                return serverBase;
236        }
237
238        @Override
239        public synchronized void setConnectionRequestTimeout(int theConnectionRequestTimeout) {
240                myConnectionRequestTimeout = theConnectionRequestTimeout;
241                myHttpClient = null;
242        }
243
244        @Override
245        public synchronized void setConnectTimeout(int theConnectTimeout) {
246                myConnectTimeout = theConnectTimeout;
247                myHttpClient = null;
248        }
249
250        /**
251         * Sets the context associated with this client factory. Must not be called more than once.
252         */
253        public void setFhirContext(FhirContext theContext) {
254                if (myContext != null && myContext != theContext) {
255                        throw new IllegalStateException("RestfulClientFactory instance is already associated with one FhirContext. RestfulClientFactory instances can not be shared.");
256                }
257                myContext = theContext;
258        }
259
260        /**
261         * Sets the Apache HTTP client instance to be used by any new restful clients created by this factory. If set to
262         * <code>null</code>, which is the default, a new HTTP client with default settings will be created.
263         * 
264         * @param theHttpClient
265         *            An HTTP client instance to use, or <code>null</code>
266         */
267        @Override
268        public synchronized void setHttpClient(HttpClient theHttpClient) {
269                myHttpClient = theHttpClient;
270        }
271
272        @Override
273        public void setProxy(String theHost, Integer thePort) {
274                if (theHost != null) {
275                        myProxy = new HttpHost(theHost, thePort, "http");
276                } else {
277                        myProxy = null;
278                }
279        }
280
281        @Override
282        public void setServerValidationMode(ServerValidationModeEnum theServerValidationMode) {
283                Validate.notNull(theServerValidationMode, "theServerValidationMode may not be null");
284                myServerValidationMode = theServerValidationMode;
285        }
286
287        @Override
288        public synchronized void setSocketTimeout(int theSocketTimeout) {
289                mySocketTimeout = theSocketTimeout;
290                myHttpClient = null;
291        }
292
293        @Override
294        public synchronized void setPoolMaxTotal(int thePoolMaxTotal) {
295                myPoolMaxTotal = thePoolMaxTotal;
296                myHttpClient = null;
297        }
298
299        @Override
300        public synchronized void setPoolMaxPerRoute(int thePoolMaxPerRoute) {
301                myPoolMaxPerRoute = thePoolMaxPerRoute;
302                myHttpClient = null;
303        }
304
305        @SuppressWarnings("unchecked")
306        void validateServerBase(String theServerBase, HttpClient theHttpClient, BaseClient theClient) {
307
308                GenericClient client = new GenericClient(myContext, theHttpClient, theServerBase, this);
309                client.setEncoding(theClient.getEncoding());
310                for (IClientInterceptor interceptor : theClient.getInterceptors()) {
311                        client.registerInterceptor(interceptor);
312                }
313                client.setDontValidateConformance(true);
314                
315                IBaseResource conformance;
316                try {
317                        @SuppressWarnings("rawtypes")
318                        Class implementingClass = myContext.getResourceDefinition("Conformance").getImplementingClass();
319                        conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute();
320                } catch (FhirClientConnectionException e) {
321                        throw new FhirClientConnectionException(myContext.getLocalizer().getMessage(RestfulClientFactory.class, "failedToRetrieveConformance", theServerBase + Constants.URL_TOKEN_METADATA), e);
322                }
323
324                FhirTerser t = myContext.newTerser();
325                String serverFhirVersionString = null;
326                Object value = t.getSingleValueOrNull(conformance, "fhirVersion");
327                if (value instanceof IPrimitiveType) {
328                        serverFhirVersionString = ((IPrimitiveType<?>) value).getValueAsString();
329                }
330                FhirVersionEnum serverFhirVersionEnum = null;
331                if (StringUtils.isBlank(serverFhirVersionString)) {
332                        // we'll be lenient and accept this
333                } else {
334                        if (serverFhirVersionString.startsWith("0.80") || serverFhirVersionString.startsWith("0.0.8")) {
335                                serverFhirVersionEnum = FhirVersionEnum.DSTU1;
336                        } else if (serverFhirVersionString.startsWith("0.4")) {
337                                serverFhirVersionEnum = FhirVersionEnum.DSTU2;
338                        } else if (serverFhirVersionString.startsWith("0.5")) {
339                                serverFhirVersionEnum = FhirVersionEnum.DSTU2;
340                        } else {
341                                // we'll be lenient and accept this
342                                ourLog.debug("Server conformance statement indicates unknown FHIR version: {}", serverFhirVersionString);
343                        }
344                }
345
346                if (serverFhirVersionEnum != null) {
347                        FhirVersionEnum contextFhirVersion = myContext.getVersion().getVersion();
348                        if (!contextFhirVersion.isEquivalentTo(serverFhirVersionEnum)) {
349                                throw new FhirClientInappropriateForServerException(myContext.getLocalizer().getMessage(RestfulClientFactory.class, "wrongVersionInConformance", theServerBase + Constants.URL_TOKEN_METADATA, serverFhirVersionString, serverFhirVersionEnum, contextFhirVersion));
350                        }
351                }
352                
353                myValidatedServerBaseUrls.add(normalizeBaseUrlForMap(theServerBase));
354
355        }
356
357        @Override
358        public ServerValidationModeEnum getServerValidationModeEnum() {
359                return getServerValidationMode();
360        }
361
362        @Override
363        public void setServerValidationModeEnum(ServerValidationModeEnum theServerValidationMode) {
364                setServerValidationMode(theServerValidationMode);
365        }
366
367}