001package ca.uhn.fhir.rest.client; 002 003/* 004 * #%L 005 * HAPI FHIR - Core Library 006 * %% 007 * Copyright (C) 2014 - 2016 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; 031import java.util.concurrent.TimeUnit; 032 033import org.apache.commons.lang3.StringUtils; 034import org.apache.commons.lang3.Validate; 035import org.apache.http.HttpHost; 036import org.apache.http.auth.AuthScope; 037import org.apache.http.auth.UsernamePasswordCredentials; 038import org.apache.http.client.CredentialsProvider; 039import org.apache.http.client.HttpClient; 040import org.apache.http.client.config.RequestConfig; 041import org.apache.http.impl.client.BasicCredentialsProvider; 042import org.apache.http.impl.client.HttpClientBuilder; 043import org.apache.http.impl.client.HttpClients; 044import org.apache.http.impl.client.ProxyAuthenticationStrategy; 045import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 046import org.hl7.fhir.instance.model.api.IBaseResource; 047import org.hl7.fhir.instance.model.api.IPrimitiveType; 048 049import ca.uhn.fhir.context.ConfigurationException; 050import ca.uhn.fhir.context.FhirContext; 051import ca.uhn.fhir.context.FhirVersionEnum; 052import ca.uhn.fhir.rest.client.api.IRestfulClient; 053import ca.uhn.fhir.rest.client.exceptions.FhirClientConnectionException; 054import ca.uhn.fhir.rest.client.exceptions.FhirClientInappropriateForServerException; 055import ca.uhn.fhir.rest.method.BaseMethodBinding; 056import ca.uhn.fhir.rest.server.Constants; 057import ca.uhn.fhir.util.FhirTerser; 058 059public class RestfulClientFactory implements IRestfulClientFactory { 060 061 private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(RestfulClientFactory.class); 062 private int myConnectionRequestTimeout = DEFAULT_CONNECTION_REQUEST_TIMEOUT; 063 private int myConnectTimeout = DEFAULT_CONNECT_TIMEOUT; 064 private FhirContext myContext; 065 private HttpClient myHttpClient; 066 private Map<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory> myInvocationHandlers = new HashMap<Class<? extends IRestfulClient>, ClientInvocationHandlerFactory>(); 067 private HttpHost myProxy; 068 private ServerValidationModeEnum myServerValidationMode = DEFAULT_SERVER_VALIDATION_MODE; 069 private int mySocketTimeout = DEFAULT_SOCKET_TIMEOUT; 070 private Set<String> myValidatedServerBaseUrls = Collections.synchronizedSet(new HashSet<String>()); 071 private int myPoolMaxTotal = DEFAULT_POOL_MAX; 072 private int myPoolMaxPerRoute = DEFAULT_POOL_MAX_PER_ROUTE; 073 074 /** 075 * Constructor 076 */ 077 public RestfulClientFactory() { 078 } 079 080 /** 081 * Constructor 082 * 083 * @param theFhirContext 084 * The context 085 */ 086 public RestfulClientFactory(FhirContext theFhirContext) { 087 myContext = theFhirContext; 088 } 089 090 @Override 091 public int getConnectionRequestTimeout() { 092 return myConnectionRequestTimeout; 093 } 094 095 @Override 096 public int getConnectTimeout() { 097 return myConnectTimeout; 098 } 099 100 @Override 101 public synchronized HttpClient getHttpClient() { 102 if (myHttpClient == null) { 103 104 PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager(5000, TimeUnit.MILLISECONDS); 105 connectionManager.setMaxTotal(myPoolMaxTotal); 106 connectionManager.setDefaultMaxPerRoute(myPoolMaxPerRoute); 107 108 //@formatter:off 109 RequestConfig defaultRequestConfig = RequestConfig.custom() 110 .setSocketTimeout(mySocketTimeout) 111 .setConnectTimeout(myConnectTimeout) 112 .setConnectionRequestTimeout(myConnectionRequestTimeout) 113 .setStaleConnectionCheckEnabled(true) 114 .setProxy(myProxy) 115 .build(); 116 117 HttpClientBuilder builder = HttpClients.custom() 118 .setConnectionManager(connectionManager) 119 .setDefaultRequestConfig(defaultRequestConfig) 120 .disableCookieManagement(); 121 122 if (myProxy != null && StringUtils.isNotBlank(myProxyUsername) && StringUtils.isNotBlank(myProxyPassword)) { 123 CredentialsProvider credsProvider = new BasicCredentialsProvider(); 124 credsProvider.setCredentials(new AuthScope(myProxy.getHostName(), myProxy.getPort()), new UsernamePasswordCredentials(myProxyUsername, myProxyPassword)); 125 builder.setProxyAuthenticationStrategy(new ProxyAuthenticationStrategy()); 126 builder.setDefaultCredentialsProvider(credsProvider); 127 } 128 129 myHttpClient = builder.build(); 130 //@formatter:on 131 132 } 133 134 return myHttpClient; 135 } 136 137 private String myProxyUsername; 138 private String myProxyPassword; 139 140 @Override 141 public void setProxyCredentials(String theUsername, String thePassword) { 142 myProxyUsername=theUsername; 143 myProxyPassword=thePassword; 144 } 145 146 @Override 147 public ServerValidationModeEnum getServerValidationMode() { 148 return myServerValidationMode; 149 } 150 151 @Override 152 public int getSocketTimeout() { 153 return mySocketTimeout; 154 } 155 156 @Override 157 public int getPoolMaxTotal() { 158 return myPoolMaxTotal; 159 } 160 161 @Override 162 public int getPoolMaxPerRoute() { 163 return myPoolMaxPerRoute; 164 } 165 166 @SuppressWarnings("unchecked") 167 private <T extends IRestfulClient> T instantiateProxy(Class<T> theClientType, InvocationHandler theInvocationHandler) { 168 T proxy = (T) Proxy.newProxyInstance(theClientType.getClassLoader(), new Class[] { theClientType }, theInvocationHandler); 169 return proxy; 170 } 171 172 /** 173 * Instantiates a new client instance 174 * 175 * @param theClientType 176 * The client type, which is an interface type to be instantiated 177 * @param theServerBase 178 * The URL of the base for the restful FHIR server to connect to 179 * @return A newly created client 180 * @throws ConfigurationException 181 * If the interface type is not an interface 182 */ 183 @Override 184 public synchronized <T extends IRestfulClient> T newClient(Class<T> theClientType, String theServerBase) { 185 if (!theClientType.isInterface()) { 186 throw new ConfigurationException(theClientType.getCanonicalName() + " is not an interface"); 187 } 188 189 190 ClientInvocationHandlerFactory invocationHandler = myInvocationHandlers.get(theClientType); 191 if (invocationHandler == null) { 192 HttpClient httpClient = getHttpClient(); 193 invocationHandler = new ClientInvocationHandlerFactory(httpClient, myContext, theServerBase, theClientType); 194 for (Method nextMethod : theClientType.getMethods()) { 195 BaseMethodBinding<?> binding = BaseMethodBinding.bindMethod(nextMethod, myContext, null); 196 invocationHandler.addBinding(nextMethod, binding); 197 } 198 myInvocationHandlers.put(theClientType, invocationHandler); 199 } 200 201 T proxy = instantiateProxy(theClientType, invocationHandler.newInvocationHandler(this)); 202 203 return proxy; 204 } 205 206 @Override 207 public synchronized IGenericClient newGenericClient(String theServerBase) { 208 HttpClient httpClient = getHttpClient(); 209 return new GenericClient(myContext, httpClient, theServerBase, this); 210 } 211 212 /** 213 * This method is internal to HAPI - It may change in future versions, use with caution. 214 */ 215 public void validateServerBaseIfConfiguredToDoSo(String theServerBase, HttpClient theHttpClient, BaseClient theClient) { 216 String serverBase = normalizeBaseUrlForMap(theServerBase); 217 218 switch (myServerValidationMode) { 219 case NEVER: 220 break; 221 case ONCE: 222 if (!myValidatedServerBaseUrls.contains(serverBase)) { 223 validateServerBase(serverBase, theHttpClient, theClient); 224 } 225 break; 226 } 227 228 } 229 230 private String normalizeBaseUrlForMap(String theServerBase) { 231 String serverBase = theServerBase; 232 if (!serverBase.endsWith("/")) { 233 serverBase = serverBase + "/"; 234 } 235 return serverBase; 236 } 237 238 @Override 239 public synchronized void setConnectionRequestTimeout(int theConnectionRequestTimeout) { 240 myConnectionRequestTimeout = theConnectionRequestTimeout; 241 myHttpClient = null; 242 } 243 244 @Override 245 public synchronized void setConnectTimeout(int theConnectTimeout) { 246 myConnectTimeout = theConnectTimeout; 247 myHttpClient = null; 248 } 249 250 /** 251 * Sets the context associated with this client factory. Must not be called more than once. 252 */ 253 public void setFhirContext(FhirContext theContext) { 254 if (myContext != null && myContext != theContext) { 255 throw new IllegalStateException("RestfulClientFactory instance is already associated with one FhirContext. RestfulClientFactory instances can not be shared."); 256 } 257 myContext = theContext; 258 } 259 260 /** 261 * Sets the Apache HTTP client instance to be used by any new restful clients created by this factory. If set to 262 * <code>null</code>, which is the default, a new HTTP client with default settings will be created. 263 * 264 * @param theHttpClient 265 * An HTTP client instance to use, or <code>null</code> 266 */ 267 @Override 268 public synchronized void setHttpClient(HttpClient theHttpClient) { 269 myHttpClient = theHttpClient; 270 } 271 272 @Override 273 public void setProxy(String theHost, Integer thePort) { 274 if (theHost != null) { 275 myProxy = new HttpHost(theHost, thePort, "http"); 276 } else { 277 myProxy = null; 278 } 279 } 280 281 @Override 282 public void setServerValidationMode(ServerValidationModeEnum theServerValidationMode) { 283 Validate.notNull(theServerValidationMode, "theServerValidationMode may not be null"); 284 myServerValidationMode = theServerValidationMode; 285 } 286 287 @Override 288 public synchronized void setSocketTimeout(int theSocketTimeout) { 289 mySocketTimeout = theSocketTimeout; 290 myHttpClient = null; 291 } 292 293 @Override 294 public synchronized void setPoolMaxTotal(int thePoolMaxTotal) { 295 myPoolMaxTotal = thePoolMaxTotal; 296 myHttpClient = null; 297 } 298 299 @Override 300 public synchronized void setPoolMaxPerRoute(int thePoolMaxPerRoute) { 301 myPoolMaxPerRoute = thePoolMaxPerRoute; 302 myHttpClient = null; 303 } 304 305 @SuppressWarnings("unchecked") 306 void validateServerBase(String theServerBase, HttpClient theHttpClient, BaseClient theClient) { 307 308 GenericClient client = new GenericClient(myContext, theHttpClient, theServerBase, this); 309 client.setEncoding(theClient.getEncoding()); 310 for (IClientInterceptor interceptor : theClient.getInterceptors()) { 311 client.registerInterceptor(interceptor); 312 } 313 client.setDontValidateConformance(true); 314 315 IBaseResource conformance; 316 try { 317 @SuppressWarnings("rawtypes") 318 Class implementingClass = myContext.getResourceDefinition("Conformance").getImplementingClass(); 319 conformance = (IBaseResource) client.fetchConformance().ofType(implementingClass).execute(); 320 } catch (FhirClientConnectionException e) { 321 throw new FhirClientConnectionException(myContext.getLocalizer().getMessage(RestfulClientFactory.class, "failedToRetrieveConformance", theServerBase + Constants.URL_TOKEN_METADATA), e); 322 } 323 324 FhirTerser t = myContext.newTerser(); 325 String serverFhirVersionString = null; 326 Object value = t.getSingleValueOrNull(conformance, "fhirVersion"); 327 if (value instanceof IPrimitiveType) { 328 serverFhirVersionString = ((IPrimitiveType<?>) value).getValueAsString(); 329 } 330 FhirVersionEnum serverFhirVersionEnum = null; 331 if (StringUtils.isBlank(serverFhirVersionString)) { 332 // we'll be lenient and accept this 333 } else { 334 if (serverFhirVersionString.startsWith("0.80") || serverFhirVersionString.startsWith("0.0.8")) { 335 serverFhirVersionEnum = FhirVersionEnum.DSTU1; 336 } else if (serverFhirVersionString.startsWith("0.4")) { 337 serverFhirVersionEnum = FhirVersionEnum.DSTU2; 338 } else if (serverFhirVersionString.startsWith("0.5")) { 339 serverFhirVersionEnum = FhirVersionEnum.DSTU2; 340 } else { 341 // we'll be lenient and accept this 342 ourLog.debug("Server conformance statement indicates unknown FHIR version: {}", serverFhirVersionString); 343 } 344 } 345 346 if (serverFhirVersionEnum != null) { 347 FhirVersionEnum contextFhirVersion = myContext.getVersion().getVersion(); 348 if (!contextFhirVersion.isEquivalentTo(serverFhirVersionEnum)) { 349 throw new FhirClientInappropriateForServerException(myContext.getLocalizer().getMessage(RestfulClientFactory.class, "wrongVersionInConformance", theServerBase + Constants.URL_TOKEN_METADATA, serverFhirVersionString, serverFhirVersionEnum, contextFhirVersion)); 350 } 351 } 352 353 myValidatedServerBaseUrls.add(normalizeBaseUrlForMap(theServerBase)); 354 355 } 356 357 @Override 358 public ServerValidationModeEnum getServerValidationModeEnum() { 359 return getServerValidationMode(); 360 } 361 362 @Override 363 public void setServerValidationModeEnum(ServerValidationModeEnum theServerValidationMode) { 364 setServerValidationMode(theServerValidationMode); 365 } 366 367}