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}