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}