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