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