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 FhirContext myContext;
061        private final Map<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory> myInvocationHandlers =
062                        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         * Constructor
077         *
078         * @param theFhirContext The context
079         */
080        public RestfulClientFactory(FhirContext theFhirContext) {
081                myContext = theFhirContext;
082        }
083
084        @Override
085        public synchronized int getConnectionRequestTimeout() {
086                return myConnectionRequestTimeout;
087        }
088
089        @Override
090        public synchronized int getConnectTimeout() {
091                return myConnectTimeout;
092        }
093
094        /**
095         * Return the proxy username to authenticate with the HTTP proxy
096         */
097        protected synchronized String getProxyUsername() {
098                return myProxyUsername;
099        }
100
101        /**
102         * Return the proxy password to authenticate with the HTTP proxy
103         */
104        protected synchronized String getProxyPassword() {
105                return myProxyPassword;
106        }
107
108        @Override
109        public synchronized void setProxyCredentials(String theUsername, String thePassword) {
110                myProxyUsername = theUsername;
111                myProxyPassword = thePassword;
112        }
113
114        @Override
115        public synchronized ServerValidationModeEnum getServerValidationMode() {
116                return myServerValidationMode;
117        }
118
119        @Override
120        public synchronized int getSocketTimeout() {
121                return mySocketTimeout;
122        }
123
124        @Override
125        public synchronized int getPoolMaxTotal() {
126                return myPoolMaxTotal;
127        }
128
129        @Override
130        public synchronized int getPoolMaxPerRoute() {
131                return myPoolMaxPerRoute;
132        }
133
134        @SuppressWarnings("unchecked")
135        private <T extends IRestfulClient> T instantiateProxy(
136                        Class<T> theClientType, InvocationHandler theInvocationHandler) {
137                return (T) Proxy.newProxyInstance(
138                                theClientType.getClassLoader(), new Class[] {theClientType}, theInvocationHandler);
139        }
140
141        /**
142         * Instantiates a new client instance
143         *
144         * @param theClientType The client type, which is an interface type to be instantiated
145         * @param theServerBase The URL of the base for the restful FHIR server to connect to
146         * @return A newly created client
147         * @throws ConfigurationException If the interface type is not an interface
148         */
149        @Override
150        public synchronized <T extends IRestfulClient> T newClient(Class<T> theClientType, String theServerBase) {
151                validateConfigured();
152
153                if (!theClientType.isInterface()) {
154                        throw new ConfigurationException(
155                                        Msg.code(1354) + theClientType.getCanonicalName() + " is not an interface");
156                }
157
158                ClientInvocationHandlerFactory invocationHandler = myInvocationHandlers.get(theClientType);
159                if (invocationHandler == null) {
160                        IHttpClient httpClient = getHttpClient(theServerBase);
161                        invocationHandler = new ClientInvocationHandlerFactory(httpClient, myContext, theServerBase, theClientType);
162                        for (Method nextMethod : theClientType.getMethods()) {
163                                BaseMethodBinding<?> binding = BaseMethodBinding.bindMethod(nextMethod, myContext, null);
164                                invocationHandler.addBinding(nextMethod, binding);
165                        }
166                        myInvocationHandlers.put(theClientType, invocationHandler);
167                }
168
169                return instantiateProxy(theClientType, invocationHandler.newInvocationHandler(this));
170        }
171
172        /**
173         * Called automatically before the first use of this factory to ensure that
174         * the configuration is sane. Subclasses may override, but should also call
175         * <code>super.validateConfigured()</code>
176         */
177        protected void validateConfigured() {
178                if (getFhirContext() == null) {
179                        throw new IllegalStateException(Msg.code(1355) + getClass().getSimpleName()
180                                        + " does not have FhirContext defined. This must be set via "
181                                        + 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(
219                                        Msg.code(1356)
220                                                        + "RestfulClientFactory instance is already associated with one FhirContext. RestfulClientFactory instances can not be shared.");
221                }
222                myContext = theContext;
223        }
224
225        /**
226         * Return the fhir context
227         *
228         * @return the fhir context
229         */
230        public FhirContext getFhirContext() {
231                return myContext;
232        }
233
234        @Override
235        public synchronized void setServerValidationMode(ServerValidationModeEnum theServerValidationMode) {
236                Validate.notNull(theServerValidationMode, "theServerValidationMode may not be null");
237                myServerValidationMode = theServerValidationMode;
238        }
239
240        @Override
241        public synchronized void setSocketTimeout(int theSocketTimeout) {
242                mySocketTimeout = theSocketTimeout;
243                resetHttpClient();
244        }
245
246        @Override
247        public synchronized void setPoolMaxTotal(int thePoolMaxTotal) {
248                myPoolMaxTotal = thePoolMaxTotal;
249                resetHttpClient();
250        }
251
252        @Override
253        public synchronized void setPoolMaxPerRoute(int thePoolMaxPerRoute) {
254                myPoolMaxPerRoute = thePoolMaxPerRoute;
255                resetHttpClient();
256        }
257
258        @Deprecated // override deprecated method
259        @Override
260        public synchronized ServerValidationModeEnum getServerValidationModeEnum() {
261                return getServerValidationMode();
262        }
263
264        @Deprecated // override deprecated method
265        @Override
266        public synchronized void setServerValidationModeEnum(ServerValidationModeEnum theServerValidationMode) {
267                setServerValidationMode(theServerValidationMode);
268        }
269
270        @Override
271        public void validateServerBaseIfConfiguredToDoSo(
272                        String theServerBase, IHttpClient theHttpClient, IRestfulClient theClient) {
273                String serverBase = normalizeBaseUrlForMap(theServerBase);
274
275                switch (getServerValidationMode()) {
276                        case NEVER:
277                                break;
278
279                        case ONCE:
280                                synchronized (myValidatedServerBaseUrls) {
281                                        if (myValidatedServerBaseUrls.add(serverBase)) {
282                                                validateServerBase(serverBase, theHttpClient, theClient);
283                                        }
284                                }
285                                break;
286                }
287        }
288
289        @SuppressWarnings("unchecked")
290        @Override
291        public void validateServerBase(String theServerBase, IHttpClient theHttpClient, IRestfulClient theClient) {
292                GenericClient client = new GenericClient(myContext, theHttpClient, theServerBase, this);
293
294                client.setInterceptorService(theClient.getInterceptorService());
295                client.setEncoding(theClient.getEncoding());
296                client.setDontValidateConformance(true);
297
298                IBaseResource conformance;
299                try {
300                        String capabilityStatementResourceName = "CapabilityStatement";
301                        if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
302                                capabilityStatementResourceName = "Conformance";
303                        }
304
305                        @SuppressWarnings("rawtypes")
306                        Class implementingClass;
307                        try {
308                                implementingClass = myContext
309                                                .getResourceDefinition(capabilityStatementResourceName)
310                                                .getImplementingClass();
311                        } catch (DataFormatException e) {
312                                if (!myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) {
313                                        capabilityStatementResourceName = "Conformance";
314                                        implementingClass = myContext
315                                                        .getResourceDefinition(capabilityStatementResourceName)
316                                                        .getImplementingClass();
317                                } else {
318                                        throw e;
319                                }
320                        }
321                        try {
322                                conformance = (IBaseResource)
323                                                client.fetchConformance().ofType(implementingClass).execute();
324                        } catch (FhirClientConnectionException e) {
325                                if (!myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)
326                                                && e.getCause() instanceof DataFormatException) {
327                                        capabilityStatementResourceName = "CapabilityStatement";
328                                        implementingClass = myContext
329                                                        .getResourceDefinition(capabilityStatementResourceName)
330                                                        .getImplementingClass();
331                                        conformance = (IBaseResource)
332                                                        client.fetchConformance().ofType(implementingClass).execute();
333                                } else {
334                                        throw e;
335                                }
336                        }
337                } catch (FhirClientConnectionException e) {
338                        String msg = myContext
339                                        .getLocalizer()
340                                        .getMessage(
341                                                        RestfulClientFactory.class,
342                                                        "failedToRetrieveConformance",
343                                                        theServerBase + Constants.URL_TOKEN_METADATA);
344                        throw new FhirClientConnectionException(Msg.code(1357) + msg, e);
345                }
346
347                FhirTerser t = myContext.newTerser();
348                String serverFhirVersionString = null;
349                Object value = t.getSingleValueOrNull(conformance, "fhirVersion");
350                if (value instanceof IPrimitiveType) {
351                        serverFhirVersionString = ((IPrimitiveType<?>) value).getValueAsString();
352                }
353                FhirVersionEnum serverFhirVersionEnum = null;
354                if (StringUtils.isBlank(serverFhirVersionString)) {
355                        // we'll be lenient and accept this
356                        ourLog.debug("Server conformance statement does not indicate the FHIR version");
357                } else {
358                        if (serverFhirVersionString.equals(FhirVersionEnum.DSTU2.getFhirVersionString())) {
359                                serverFhirVersionEnum = FhirVersionEnum.DSTU2;
360                        } else if (serverFhirVersionString.equals(FhirVersionEnum.DSTU2_1.getFhirVersionString())) {
361                                serverFhirVersionEnum = FhirVersionEnum.DSTU2_1;
362                        } else if (serverFhirVersionString.equals(FhirVersionEnum.DSTU3.getFhirVersionString())) {
363                                serverFhirVersionEnum = FhirVersionEnum.DSTU3;
364                        } else if (serverFhirVersionString.equals(FhirVersionEnum.R4.getFhirVersionString())) {
365                                serverFhirVersionEnum = FhirVersionEnum.R4;
366                        } else {
367                                // we'll be lenient and accept this
368                                ourLog.debug(
369                                                "Server conformance statement indicates unknown FHIR version: {}", serverFhirVersionString);
370                        }
371                }
372
373                if (serverFhirVersionEnum != null) {
374                        FhirVersionEnum contextFhirVersion = myContext.getVersion().getVersion();
375                        if (!contextFhirVersion.isEquivalentTo(serverFhirVersionEnum)) {
376                                throw new FhirClientInappropriateForServerException(Msg.code(1358)
377                                                + myContext
378                                                                .getLocalizer()
379                                                                .getMessage(
380                                                                                RestfulClientFactory.class,
381                                                                                "wrongVersionInConformance",
382                                                                                theServerBase + Constants.URL_TOKEN_METADATA,
383                                                                                serverFhirVersionString,
384                                                                                serverFhirVersionEnum,
385                                                                                contextFhirVersion));
386                        }
387                }
388
389                String serverBase = normalizeBaseUrlForMap(theServerBase);
390                myValidatedServerBaseUrls.add(serverBase);
391        }
392
393        /**
394         * Get the http client for the given server base
395         *
396         * @param theServerBase the server base
397         * @return the http client
398         */
399        protected abstract IHttpClient getHttpClient(String theServerBase);
400
401        /**
402         * Reset the http client. This method is used when parameters have been set and a
403         * new http client needs to be created
404         */
405        protected abstract void resetHttpClient();
406}