package gluu.scim2.client; import gluu.scim2.client.rest.FreelyAccessible; import gluu.scim2.client.rest.provider.AuthorizationInjectionFilter; import gluu.scim2.client.rest.provider.ListResponseProvider; import gluu.scim2.client.rest.provider.ScimResourceProvider; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jboss.resteasy.client.jaxrs.ResteasyClient; import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget; import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine; import javax.ws.rs.core.Response; import java.io.Serializable; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.Optional; /** * The base class for specific SCIM clients. * <p>Upon initialization, this class internally creates a RestEasy proxy client based on parameters passed. This proxy * is used to invoke the operations the service offers. The exact methods that can be called are driven by the interface * class passed in the constructor.</p> * <p>When a service method is invoked through an instance obtained by any of the factory methods of * {@link gluu.scim2.client.factory.ScimClientFactory ScimClientFactory}, the call is dispatched by the {@link #invoke(Object, Method, Object[]) invoke} * method of this class, which properly handles the authorization details in conjuction with the filter * {@link gluu.scim2.client.rest.provider.AuthorizationInjectionFilter AuthorizationInjectionFilter}.</p> * <p>Concrete subclasses of this class must provide {@link #getAuthenticationHeader() getAuthenticationHeader} and * {@link #authorize(Response) authorize} methods that must implement specific ways to obtain access tokens depending * on how the SCIM service is being protected.</p> * @param <T> The type of the internal RestEasy proxy used by this class. This is the same type that * {@link gluu.scim2.client.factory.ScimClientFactory ScimClientFactory} methods return. */ /* * @author Yuriy Movchan Date: 08/23/2013 * Re-engineered by jgomer on 2017-09-14. */ public abstract class AbstractScimClient<T> implements InvocationHandler, Serializable { private static final long serialVersionUID = 9098930517944520482L; private Logger logger = LogManager.getLogger(getClass()); //All method calls using scimService (with exception of close) will return a javax.ws.rs.core.Response object. //The underlying data can be read using the readEntity method private T scimService; private ResteasyClient client; AbstractScimClient(String domain, Class<T> serviceClass) { /* Configures a proxy to interact with the service using the new JAX-RS 2.0 Client API, see section "Resteasy Proxy Framework" of RESTEasy JAX-RS user guide */ if (System.getProperty("httpclient.multithreaded") == null) { client = new ResteasyClientBuilder().build(); } else { PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager(); //Change defaults if supplied getIntegerProperty("httpclient.multithreaded.maxtotal").ifPresent(cm::setMaxTotal); getIntegerProperty("httpclient.multithreaded.maxperroute").ifPresent(cm::setDefaultMaxPerRoute); getIntegerProperty("httpclient.multithreaded.validateafterinactivity").ifPresent(cm::setValidateAfterInactivity); logger.debug("Using multithreaded support with maxTotalConnections={} and maxPerRoutConnections={}", cm.getMaxTotal(), cm.getDefaultMaxPerRoute()); logger.warn("Ensure your oxTrust 'rptConnectionPoolUseConnectionPooling' property is set to true"); CloseableHttpClient httpClient = HttpClients.custom() .setDefaultRequestConfig(RequestConfig.custom().setCookieSpec(CookieSpecs.STANDARD).build()) .setConnectionManager(cm).build(); ApacheHttpClient4Engine engine = new ApacheHttpClient4Engine(httpClient); client = new ResteasyClientBuilder().httpEngine(engine).build(); } ResteasyWebTarget target = client.target(domain); scimService = target.proxy(serviceClass); target.register(ListResponseProvider.class); target.register(AuthorizationInjectionFilter.class); target.register(ScimResourceProvider.class); ClientMap.update(client, null); } /* * The actual call to service methods is done here. Note that the response is buffered so the underlying input * stream is fully consumed, so there is no need to create finally blocks with close(). Also, the readEntity method * can be called any number of times. For instance, the "raw" response can be inspected by using readEntity(String.class) */ private Response invokeServiceMethod(Method method, Object[] args) throws ReflectiveOperationException { logger.trace("Sending service request for method {}", method.getName()); Response response = (Response) method.invoke(scimService, args); boolean buffered = false; try { //This try block helps prevent a RestEasy NPE that arises when the response is empty (has no content) buffered = response.bufferEntity(); } catch (Exception e) { logger.trace(e.getMessage(), e); } logger.trace("Received response entity was{} buffered", buffered ? "" : " not"); logger.trace("Response status code was {}", response.getStatus()); return response; } /** * This method is the single point of dispatch for any and all the requests made to the service. It takes care of * requesting access tokens when necessary and make them available when requests are bound to be issued. * <p>As with all methods of this class and its subclasses, invoke is not called directly by developers: the calls are * triggered when the objects returned by factory methods of {@link gluu.scim2.client.factory.ScimClientFactory ScimClientFactory} * are manipulated.</p> * * @return The response associated to the invocation (normally a javax.ws.rs.core.Response instance) */ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (methodName.equals("close")) { logger.info("Closing RestEasy client"); ClientMap.remove(client); return null; } else { Response response; FreelyAccessible unprotected = method.getAnnotation(FreelyAccessible.class); //Set authorization header if needed if (unprotected != null) { response = invokeServiceMethod(method, args); } else { ClientMap.update(client, getAuthenticationHeader()); response = invokeServiceMethod(method, args); if (response.getStatus() == Response.Status.UNAUTHORIZED.getStatusCode()) { if (authorize(response)) { logger.trace("Trying second attempt of request (former received unauthorized response code)"); ClientMap.update(client, getAuthenticationHeader()); response = invokeServiceMethod(method, args); } else { logger.error("Could not get access token for current request: {}", methodName); } } } return response; } } abstract String getAuthenticationHeader(); abstract boolean authorize(Response response); private Optional<Integer> getIntegerProperty(String name) { return Optional.ofNullable(System.getProperty(name)).map(prop -> { try { return new Integer(prop); } catch (Exception e) { return null; } }); } }