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