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}