
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}