001package ca.uhn.fhir.rest.client;
002
003/*
004 * #%L
005 * HAPI FHIR - Core Library
006 * %%
007 * Copyright (C) 2014 - 2017 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;
031
032import org.apache.commons.lang3.StringUtils;
033import org.apache.commons.lang3.Validate;
034import org.hl7.fhir.instance.model.api.IBaseResource;
035import org.hl7.fhir.instance.model.api.IPrimitiveType;
036
037import ca.uhn.fhir.context.ConfigurationException;
038import ca.uhn.fhir.context.FhirContext;
039import ca.uhn.fhir.context.FhirVersionEnum;
040import ca.uhn.fhir.parser.DataFormatException;
041import ca.uhn.fhir.rest.client.api.IHttpClient;
042import ca.uhn.fhir.rest.client.api.IRestfulClient;
043import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException;
044import ca.uhn.fhir.rest.client.exceptions.FhirClientInappropriateForServerException;
045import ca.uhn.fhir.rest.method.BaseMethodBinding;
046import ca.uhn.fhir.rest.server.Constants;
047import ca.uhn.fhir.util.FhirTerser;
048
049/**
050 * Base class for a REST client factory implementation
051 */
052public abstract class RestfulClientFactory implements IRestfulClientFactory {
053
054        private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulClientFactory.class);
055        private int myConnectionRequestTimeout = DEFAULT_CONNECTION_REQUEST_TIMEOUT;
056        private int myConnectTimeout = DEFAULT_CONNECT_TIMEOUT;
057        private FhirContext myContext;
058        private Map<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory> myInvocationHandlers = new HashMap<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory>();
059        private ServerValidationModeEnum myServerValidationMode = DEFAULT_SERVER_VALIDATION_MODE;
060        private int mySocketTimeout = DEFAULT_SOCKET_TIMEOUT;
061        private Set<String> myValidatedServerBaseUrls = Collections.synchronizedSet(new HashSet<String>());
062        private String myProxyUsername;
063        private String myProxyPassword; 
064        private int myPoolMaxTotal = DEFAULT_POOL_MAX;
065        private int myPoolMaxPerRoute = DEFAULT_POOL_MAX_PER_ROUTE;
066        
067        /**
068         * Constructor
069         */
070        public RestfulClientFactory() {
071        }
072
073        /**
074         * Constructor
075         * 
076         * @param theFhirContext
077         *            The context
078         */
079        public RestfulClientFactory(FhirContext theFhirContext) {
080                myContext = theFhirContext;
081        }
082
083        @Override
084        public int getConnectionRequestTimeout() {
085                return myConnectionRequestTimeout;
086        }
087
088        @Override
089        public int getConnectTimeout() {
090                return myConnectTimeout;
091        }
092
093        /**
094         * Return the proxy username to authenticate with the HTTP proxy
095         * @param The proxy username
096         */     
097        protected String getProxyUsername() {
098                return myProxyUsername;
099        }
100        
101        /**
102         * Return the proxy password to authenticate with the HTTP proxy
103         * @param The proxy password
104         */     
105        protected String getProxyPassword() {
106                return myProxyPassword;
107        }
108
109        @Override
110        public void setProxyCredentials(String theUsername, String thePassword) {
111                myProxyUsername=theUsername;
112                myProxyPassword=thePassword;
113        }
114        
115        @Override
116        public ServerValidationModeEnum getServerValidationMode() {
117                return myServerValidationMode;
118        }
119
120        @Override
121        public int getSocketTimeout() {
122                return mySocketTimeout;
123        }
124
125        @Override
126        public int getPoolMaxTotal() {
127                return myPoolMaxTotal;
128        }
129
130        @Override
131        public int getPoolMaxPerRoute() {
132                return myPoolMaxPerRoute;
133        }
134
135        @SuppressWarnings("unchecked")
136        private <T extends IRestfulClient> T instantiateProxy(Class<T> theClientType, InvocationHandler theInvocationHandler) {
137                T proxy = (T) Proxy.newProxyInstance(theClientType.getClassLoader(), new Class[] { theClientType }, theInvocationHandler);
138                return proxy;
139        }
140
141        /**
142         * Instantiates a new client instance
143         * 
144         * @param theClientType
145         *            The client type, which is an interface type to be instantiated
146         * @param theServerBase
147         *            The URL of the base for the restful FHIR server to connect to
148         * @return A newly created client
149         * @throws ConfigurationException
150         *             If the interface type is not an interface
151         */
152        @Override
153        public synchronized <T extends IRestfulClient> T newClient(Class<T> theClientType, String theServerBase) {
154                validateConfigured();
155                
156                if (!theClientType.isInterface()) {
157                        throw new ConfigurationException(theClientType.getCanonicalName() + " is not an interface");
158                }
159                
160                ClientInvocationHandlerFactory invocationHandler = myInvocationHandlers.get(theClientType);
161                if (invocationHandler == null) {
162                        IHttpClient httpClient = getHttpClient(theServerBase);
163                        invocationHandler = new ClientInvocationHandlerFactory(httpClient, myContext, theServerBase, theClientType);
164                        for (Method nextMethod : theClientType.getMethods()) {
165                                BaseMethodBinding<?> binding = BaseMethodBinding.bindMethod(nextMethod, myContext, null);
166                                invocationHandler.addBinding(nextMethod, binding);
167                        }
168                        myInvocationHandlers.put(theClientType, invocationHandler);
169                }
170
171                T proxy = instantiateProxy(theClientType, invocationHandler.newInvocationHandler(this));
172
173                return proxy;
174        }
175
176        /**
177         * Called automatically before the first use of this factory to ensure that
178         * the configuration is sane. Subclasses may override, but should also call 
179         * <code>super.validateConfigured()</code>
180         */
181        protected void validateConfigured() {
182                if (getFhirContext() == null) {
183                        throw new IllegalStateException(getClass().getSimpleName() + " does not have FhirContext defined. This must be set via " + getClass().getSimpleName() + "#setFhirContext(FhirContext)");
184                }
185        }
186
187        @Override
188        public synchronized IGenericClient newGenericClient(String theServerBase) {
189                validateConfigured();
190                IHttpClient httpClient = getHttpClient(theServerBase);
191                
192                return new GenericClient(myContext, httpClient, theServerBase, this);
193        }
194
195        @Override
196        public void validateServerBaseIfConfiguredToDoSo(String theServerBase, IHttpClient theHttpClient, BaseClient theClient) {
197                String serverBase = normalizeBaseUrlForMap(theServerBase);
198
199                switch (getServerValidationMode()) {
200                case NEVER:
201                        break;
202                case ONCE:
203                        if (!myValidatedServerBaseUrls.contains(serverBase)) {
204                                validateServerBase(serverBase, theHttpClient, theClient);
205                        }
206                        break;
207                }
208
209        }
210
211        private String normalizeBaseUrlForMap(String theServerBase) {
212                String serverBase = theServerBase;
213                if (!serverBase.endsWith("/")) {
214                        serverBase = serverBase + "/";
215                }
216                return serverBase;
217        }
218
219        @Override
220        public synchronized void setConnectionRequestTimeout(int theConnectionRequestTimeout) {
221                myConnectionRequestTimeout = theConnectionRequestTimeout;
222                resetHttpClient();
223        }
224
225        @Override
226        public synchronized void setConnectTimeout(int theConnectTimeout) {
227                myConnectTimeout = theConnectTimeout;
228                resetHttpClient();
229        }
230
231        /**
232         * Sets the context associated with this client factory. Must not be called more than once.
233         */
234        public void setFhirContext(FhirContext theContext) {
235                if (myContext != null && myContext != theContext) {
236                        throw new IllegalStateException("RestfulClientFactory instance is already associated with one FhirContext. RestfulClientFactory instances can not be shared.");
237                }
238                myContext = theContext;
239        }
240        
241        /**
242         * Return the fhir context
243         * @return the fhir context 
244         */
245        public FhirContext getFhirContext() {
246                return myContext;
247        }
248
249        @Override
250        public void setServerValidationMode(ServerValidationModeEnum theServerValidationMode) {
251                Validate.notNull(theServerValidationMode, "theServerValidationMode may not be null");
252                myServerValidationMode = theServerValidationMode;
253        }
254
255        @Override
256        public synchronized void setSocketTimeout(int theSocketTimeout) {
257                mySocketTimeout = theSocketTimeout;
258                resetHttpClient();
259        }
260
261        @Override
262        public synchronized void setPoolMaxTotal(int thePoolMaxTotal) {
263                myPoolMaxTotal = thePoolMaxTotal;
264                resetHttpClient();
265        }
266
267        @Override
268        public synchronized void setPoolMaxPerRoute(int thePoolMaxPerRoute) {
269                myPoolMaxPerRoute = thePoolMaxPerRoute;
270                resetHttpClient();
271        }
272
273        @SuppressWarnings("unchecked")
274        @Override
275        public void validateServerBase(String theServerBase, IHttpClient theHttpClient, BaseClient theClient) {
276                GenericClient client = new GenericClient(myContext, theHttpClient, theServerBase, this);
277                client.setEncoding(theClient.getEncoding());
278                for (IClientInterceptor interceptor : theClient.getInterceptors()) {
279                        client.registerInterceptor(interceptor);
280                }
281                client.setDontValidateConformance(true);
282                
283                IBaseResource conformance;
284                try {
285                        String capabilityStatementResourceName = "CapabilityStatement";
286                        if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
287                                capabilityStatementResourceName = "Conformance";
288                        }
289                        
290                        @SuppressWarnings("rawtypes")
291                        Class implementingClass;
292                        try {
293                                implementingClass = myContext.getResourceDefinition(capabilityStatementResourceName).getImplementingClass();
294                        } catch (DataFormatException e) {
295                                if (!myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
296                                        capabilityStatementResourceName = "Conformance";
297                                        implementingClass = myContext.getResourceDefinition(capabilityStatementResourceName).getImplementingClass();
298                                } else {
299                                        throw e;
300                                }
301                        }
302                        try {
303                                conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute();
304                        } catch (FhirClientConnectionException e) {
305                                if (!myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3) && e.getCause() instanceof DataFormatException) {
306                                        capabilityStatementResourceName = "Conformance";
307                                        implementingClass = myContext.getResourceDefinition(capabilityStatementResourceName).getImplementingClass();
308                                        conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute();
309                                } else {
310                                        throw e;
311                                }
312                        }
313                } catch (FhirClientConnectionException e) {
314                        String msg = myContext.getLocalizer().getMessage(RestfulClientFactory.class, "failedToRetrieveConformance", theServerBase + Constants.URL_TOKEN_METADATA);
315                        throw new FhirClientConnectionException(msg, e);
316                }
317
318                FhirTerser t = myContext.newTerser();
319                String serverFhirVersionString = null;
320                Object value = t.getSingleValueOrNull(conformance, "fhirVersion");
321                if (value instanceof IPrimitiveType) {
322                        serverFhirVersionString = IPrimitiveType.class.cast(value).getValueAsString();
323                }
324                FhirVersionEnum serverFhirVersionEnum = null;
325                if (StringUtils.isBlank(serverFhirVersionString)) {
326                        // we'll be lenient and accept this
327                } else {
328                        //FIXME null access on serverFhirVersionString
329                        if (serverFhirVersionString.startsWith("0.80") || serverFhirVersionString.startsWith("0.0.8")) {
330                                serverFhirVersionEnum = FhirVersionEnum.DSTU1;
331                        } else if (serverFhirVersionString.startsWith("0.4")) {
332                                serverFhirVersionEnum = FhirVersionEnum.DSTU2;
333                        } else if (serverFhirVersionString.startsWith("0.5")) {
334                                serverFhirVersionEnum = FhirVersionEnum.DSTU2;
335                        } else {
336                                // we'll be lenient and accept this
337                                ourLog.debug("Server conformance statement indicates unknown FHIR version: {}", serverFhirVersionString);
338                        }
339                }
340
341                if (serverFhirVersionEnum != null) {
342                        FhirVersionEnum contextFhirVersion = myContext.getVersion().getVersion();
343                        if (!contextFhirVersion.isEquivalentTo(serverFhirVersionEnum)) {
344                                throw new FhirClientInappropriateForServerException(myContext.getLocalizer().getMessage(RestfulClientFactory.class, "wrongVersionInConformance", theServerBase + Constants.URL_TOKEN_METADATA, serverFhirVersionString, serverFhirVersionEnum, contextFhirVersion));
345                        }
346                }
347                
348                myValidatedServerBaseUrls.add(normalizeBaseUrlForMap(theServerBase));
349
350        }
351
352        @Deprecated //override deprecated method
353        @Override
354        public ServerValidationModeEnum getServerValidationModeEnum() {
355                return getServerValidationMode();
356        }
357
358        @Deprecated //override deprecated method
359        @Override
360        public void setServerValidationModeEnum(ServerValidationModeEnum theServerValidationMode) {
361                setServerValidationMode(theServerValidationMode);
362        }
363        
364        /**
365         * Get the http client for the given server base
366         * @param theServerBase the server base
367         * @return the http client
368         */
369        protected abstract IHttpClient getHttpClient(String theServerBase);
370        
371        /**
372         * Reset the http client. This method is used when parameters have been set and a
373         * new http client needs to be created
374         */
375        protected abstract void resetHttpClient();
376
377}