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