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