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