001package ca.uhn.fhir.rest.client.impl; 002 003/* 004 * #%L 005 * HAPI FHIR - Client Framework 006 * %% 007 * Copyright (C) 2014 - 2018 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 client.setEncoding(theClient.getEncoding()); 276 for (IClientInterceptor interceptor : theClient.getInterceptors()) { 277 client.registerInterceptor(interceptor); 278 } 279 client.setDontValidateConformance(true); 280 281 IBaseResource conformance; 282 try { 283 String capabilityStatementResourceName = "CapabilityStatement"; 284 if (myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 285 capabilityStatementResourceName = "Conformance"; 286 } 287 288 @SuppressWarnings("rawtypes") 289 Class implementingClass; 290 try { 291 implementingClass = myContext.getResourceDefinition(capabilityStatementResourceName).getImplementingClass(); 292 } catch (DataFormatException e) { 293 if (!myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3)) { 294 capabilityStatementResourceName = "Conformance"; 295 implementingClass = myContext.getResourceDefinition(capabilityStatementResourceName).getImplementingClass(); 296 } else { 297 throw e; 298 } 299 } 300 try { 301 conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute(); 302 } catch (FhirClientConnectionException e) { 303 if (!myContext.getVersion().getVersion().isOlderThan(FhirVersionEnum.DSTU3) && e.getCause() instanceof DataFormatException) { 304 capabilityStatementResourceName = "Conformance"; 305 implementingClass = myContext.getResourceDefinition(capabilityStatementResourceName).getImplementingClass(); 306 conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute(); 307 } else { 308 throw e; 309 } 310 } 311 } catch (FhirClientConnectionException e) { 312 String msg = myContext.getLocalizer().getMessage(RestfulClientFactory.class, "failedToRetrieveConformance", theServerBase + Constants.URL_TOKEN_METADATA); 313 throw new FhirClientConnectionException(msg, e); 314 } 315 316 FhirTerser t = myContext.newTerser(); 317 String serverFhirVersionString = null; 318 Object value = t.getSingleValueOrNull(conformance, "fhirVersion"); 319 if (value instanceof IPrimitiveType) { 320 serverFhirVersionString = IPrimitiveType.class.cast(value).getValueAsString(); 321 } 322 FhirVersionEnum serverFhirVersionEnum = null; 323 if (StringUtils.isBlank(serverFhirVersionString)) { 324 // we'll be lenient and accept this 325 ourLog.debug("Server conformance statement does not indicate the FHIR version"); 326 } else { 327 if (serverFhirVersionString.equals(FhirVersionEnum.DSTU2.getFhirVersionString())) { 328 serverFhirVersionEnum = FhirVersionEnum.DSTU2; 329 } else if (serverFhirVersionString.equals(FhirVersionEnum.DSTU2_1.getFhirVersionString())) { 330 serverFhirVersionEnum = FhirVersionEnum.DSTU2_1; 331 } else if (serverFhirVersionString.equals(FhirVersionEnum.DSTU3.getFhirVersionString())) { 332 serverFhirVersionEnum = FhirVersionEnum.DSTU3; 333 } else if (serverFhirVersionString.equals(FhirVersionEnum.R4.getFhirVersionString())) { 334 serverFhirVersionEnum = FhirVersionEnum.R4; 335 } else { 336 // we'll be lenient and accept this 337 ourLog.debug("Server conformance statement indicates unknown FHIR version: {}", serverFhirVersionString); 338 } 339 } 340 341 if (serverFhirVersionEnum != null) { 342 FhirVersionEnum contextFhirVersion = myContext.getVersion().getVersion(); 343 if (!contextFhirVersion.isEquivalentTo(serverFhirVersionEnum)) { 344 throw new FhirClientInappropriateForServerException(myContext.getLocalizer().getMessage(RestfulClientFactory.class, "wrongVersionInConformance", 345 theServerBase + Constants.URL_TOKEN_METADATA, serverFhirVersionString, serverFhirVersionEnum, contextFhirVersion)); 346 } 347 } 348 349 myValidatedServerBaseUrls.add(normalizeBaseUrlForMap(theServerBase)); 350 351 } 352 353 /** 354 * Get the http client for the given server base 355 * 356 * @param theServerBase 357 * the server base 358 * @return the http client 359 */ 360 protected abstract IHttpClient getHttpClient(String theServerBase); 361 362 /** 363 * Reset the http client. This method is used when parameters have been set and a 364 * new http client needs to be created 365 */ 366 protected abstract void resetHttpClient(); 367 368}