package com.thinkbiganalytics.rest; /*- * #%L * thinkbig-commons-rest-client * %% * Copyright (C) 2017 ThinkBig Analytics * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ import com.fasterxml.jackson.databind.ObjectMapper; import com.google.common.io.ByteStreams; import org.apache.commons.lang3.StringUtils; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.LayeredConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.DefaultHostnameVerifier; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.glassfish.jersey.SslConfigurator; import org.glassfish.jersey.apache.connector.ApacheClientProperties; import org.glassfish.jersey.client.ClientConfig; import org.glassfish.jersey.client.ClientProperties; import org.glassfish.jersey.client.HttpUrlConnectorProvider; import org.glassfish.jersey.client.JerseyClientBuilder; import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; import org.glassfish.jersey.jackson.JacksonFeature; import org.glassfish.jersey.media.multipart.Boundary; import org.glassfish.jersey.media.multipart.MultiPart; import org.glassfish.jersey.media.multipart.MultiPartFeature; import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.util.Map; import java.util.concurrent.Future; import javax.net.ssl.HostnameVerifier; import javax.net.ssl.SSLContext; import javax.ws.rs.NotAcceptableException; import javax.ws.rs.client.Client; import javax.ws.rs.client.Entity; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.Form; import javax.ws.rs.core.GenericType; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; /** * Generic JerseyRestClient */ public class JerseyRestClient { public static final String HOST_NOT_SET_VALUE = "NOT_SET"; protected static final Logger log = LoggerFactory.getLogger(JerseyRestClient.class); /** * Flag to indicate if the client is configured correctly and available to be used. */ public boolean isHostConfigured; /** * The Jersey Client */ protected Client client; /** * the base uri to connect to set by the configuration of the REST Client. * This constructor will set this value using the {@link JerseyClientConfig#getUrl()} * Users of this client can then reference the uri from this client class. * * @see #JerseyRestClient(JerseyClientConfig) */ protected String uri; /** * The username to use to connect set by configuration of the REST Client * The constructor will set this value using {@link JerseyClientConfig#username} * Users of this client can then reference the username from this client class. * * @see #JerseyRestClient(JerseyClientConfig) */ private String username; /** * if the supplied endpoint doesnt accept JSON but rather Plain Text, this mapper will be used to resolve the text and turn it into JSON/object */ private ObjectMapper objectMapper; /** * flag to use the PoolingHttpClientConnectionManager from Apache instead of the Jersey Manager The PoolingHttpClientConnectionManager doesnt support some JSON header which is why this is turned * off by default. */ private boolean useConnectionPooling = false; public JerseyRestClient(JerseyClientConfig config) { useConnectionPooling = config.isUseConnectionPooling(); SSLContext sslContext = null; if (config.isHttps()) { SslConfigurator sslConfig = null; byte[] keyStoreFile = null; byte[] truststoreFile = null; try { if (StringUtils.isNotBlank(config.getKeystorePath())) { InputStream keystore = JerseyRestClient.class.getResourceAsStream(config.getKeystorePath()); if (keystore != null) { keyStoreFile = ByteStreams.toByteArray(keystore); } } } catch (IOException e) { } try { if (StringUtils.isNotBlank(config.getTruststorePath())) { InputStream truststore = JerseyRestClient.class.getResourceAsStream(config.getTruststorePath()); if (truststore != null) { truststoreFile = ByteStreams.toByteArray(truststore); } } } catch (IOException e) { } if (keyStoreFile != null) { sslConfig = SslConfigurator.newInstance() .trustStoreBytes(truststoreFile != null ? truststoreFile : keyStoreFile) .trustStorePassword(config.getTruststorePassword() != null ? config.getTruststorePassword() : config.getKeystorePassword()) .trustStoreType(config.getTrustStoreType()) .keyStoreBytes(keyStoreFile != null ? keyStoreFile : truststoreFile) .keyStorePassword(config.getKeystorePassword()); } else { sslConfig = SslConfigurator.newInstance() .keyStoreFile(config.getKeystorePath() == null ? config.getTruststorePath() : config.getKeystorePath()) .keyStorePassword(config.getKeystorePassword() == null ? config.getTruststorePassword() : config.getKeystorePassword()) .trustStoreFile(config.getTruststorePath() == null ? config.getKeystorePath() : config.getTruststorePath()) .trustStorePassword(config.getTruststorePassword() == null ? config.getKeystorePassword() : config.getTruststorePassword()) .trustStoreType(config.getTrustStoreType()); } try { sslContext = sslConfig.createSSLContext(); } catch (Exception e) { log.error("ERROR creating CLient SSL Context. " + e.getMessage() + " Falling back to Jersey Client without SSL. Rest Integration with '" + config.getUrl() + "' will probably not work until this is fixed!"); } } ClientConfig clientConfig = new ClientConfig(); // Add in Timeouts if configured. Values are in milliseconds if (config.getReadTimeout() != null) { clientConfig.property(ClientProperties.READ_TIMEOUT, config.getReadTimeout()); } if (config.getConnectTimeout() != null) { clientConfig.property(ClientProperties.CONNECT_TIMEOUT, config.getConnectTimeout()); } if (useConnectionPooling) { PoolingHttpClientConnectionManager connectionManager = null; if (sslContext != null) { HostnameVerifier defaultHostnameVerifier = new DefaultHostnameVerifier(); LayeredConnectionSocketFactory sslSocketFactory = new SSLConnectionSocketFactory( sslContext, defaultHostnameVerifier); final Registry<ConnectionSocketFactory> registry = RegistryBuilder.<ConnectionSocketFactory>create() .register("http", PlainConnectionSocketFactory.getSocketFactory()) .register("https", sslSocketFactory) .build(); connectionManager = new PoolingHttpClientConnectionManager(registry); } else { connectionManager = new PoolingHttpClientConnectionManager(); } connectionManager.setDefaultMaxPerRoute(100); // # of connections allowed per host/address connectionManager.setMaxTotal(200); // number of connections allowed in total clientConfig.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager); HttpUrlConnectorProvider connectorProvider = new HttpUrlConnectorProvider(); clientConfig.connectorProvider(connectorProvider); } clientConfig.register(MultiPartFeature.class); if (sslContext != null) { log.info("Created new Jersey Client with SSL connecting to {} ", config.getUrl()); client = new JerseyClientBuilder().withConfig(clientConfig).sslContext(sslContext).build(); } else { log.info("Created new Jersey Client without SSL connecting to {} ", config.getUrl()); client = JerseyClientBuilder.createClient(clientConfig); } // Register Jackson objectMapper = new JacksonObjectMapperProvider().getContext(null); client.register(JacksonObjectMapperProvider.class); client.register(JacksonFeature.class); // Configure authentication if (StringUtils.isNotBlank(config.getUsername())) { HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic(config.getUsername(), config.getPassword()); client.register(feature); } this.uri = config.getUrl(); this.username = config.getUsername(); if (StringUtils.isNotBlank(config.getHost()) && !HOST_NOT_SET_VALUE.equals(config.getHost())) { this.isHostConfigured = true; } else { log.info("Jersey Rest Client not initialized. Host name is Not set!!"); } } /** * Flag to detect if this client is configured correctly. * * @return true if configured correctly, false if not. */ public boolean isHostConfigured() { return isHostConfigured; } /** * Get the username connecting to this REST service * * @return the user connecting */ public String getUsername() { return username; } /** * The base target that will be used upon each request. * All rest calls will go through this method. * Specific clients that extend this class can override this method to specify a given root path. * * @return the target to use to make th REST request */ protected WebTarget getBaseTarget() { WebTarget target = client.target(uri); return target; } /** * prepends the supplied {@link this#uri} to the supplied path * * @param path the path to append to the {@link this#uri} * @return the target to use to make the REST request */ protected WebTarget getTargetFromPath(String path) { String updatedPath = uri + path; WebTarget target = client.target(updatedPath); return target; } /** * Build a target adding the supplied query parameters to the the request * * @param path the path to access * @param params the key,value parameters to add to the request * @return the target to use to make the REST request */ private WebTarget buildTarget(String path, Map<String, Object> params) { WebTarget target = getBaseTarget().path(path); if (params != null) { for (Map.Entry<String, Object> entry : params.entrySet()) { target = target.queryParam(entry.getKey(), entry.getValue()); } } return target; } /** * Perform a asynchronous GET request * * @param path the path to access * @param params the key,value parameters to add to the request * @param clazz the returned class type * @return a Future of type T */ public <T> Future<T> getAsync(String path, Map<String, Object> params, Class<T> clazz) { WebTarget target = buildTarget(path, params); return target.request(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON_TYPE).async().get(clazz); } /** * Perform a asynchronous GET request * * @param path the path to access * @param params the parameters to add to the request * @param type the returned class type * @return a Future of type T */ public <T> Future<T> getAsync(String path, Map<String, Object> params, GenericType<T> type) { WebTarget target = buildTarget(path, params); return target.request(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON_TYPE).async().get(type); } /** * Perform a GET request * * @param path the path to access * @param params key, value parameters to add to the request * @param clazz the class type to return as the response from the GET request * @param <T> the returned class type * @return the returned object of the specified Class */ public <T> T get(String path, Map<String, Object> params, Class<T> clazz) { return get(path, params, clazz, true); } /** * Perform a GET request. Exceptions will not be logged * * @param path the path to access * @param params key, value parameters to add to the request * @param clazz the class type to return as the response from the GET request * @param logError true to log an exceptions, false to not * @param <T> the returned class type * @return the returned object of the specified Class */ public <T> T get(String path, Map<String, Object> params, Class<T> clazz, boolean logError) { WebTarget target = buildTarget(path, params); T obj = null; try { obj = target.request(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE).get(clazz); } catch (Exception e) { if (e instanceof NotAcceptableException) { obj = handleNotAcceptableGetRequestJsonException(target, clazz); } else { if (logError) { log.error("Failed to process request " + path, e); } } } return obj; } /** * Perform a GET request. if it returns an exception the message will not be logged. * * @param path the path to access * @param params key, value parameters to add to the request * @param clazz the class type to return as the response from the GET request * @param <T> the returned class type * @return the returned object of the specified Class */ public <T> T getWithoutErrorLogging(String path, Map<String, Object> params, Class<T> clazz) { WebTarget target = buildTarget(path, params); T obj = null; try { obj = target.request(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE).get(clazz); } catch (Exception e) { if (e instanceof NotAcceptableException) { obj = handleNotAcceptableGetRequestJsonException(target, clazz); } } return obj; } /** * Sometimes QueryParams dont fit the model of key,value pairs. * This method can be used to call a GET request using the path passed in * Allow a client to create the target passing in a full url path with ? and & query params * If you have known key,value pairs its recommend you use the {@link this#get(String, Map, Class)} * * @param path the path to access, including the root target and the ?key=value&key2=value&key3 * @param clazz the class type to return as the response from the GET request * @param <T> the returned class type * @return the returned object of the specified Class */ public <T> T getFromPathString(String path, Class<T> clazz) { WebTarget target = getTargetFromPath(path); T obj = null; try { obj = target.request(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE).get(clazz); } catch (Exception e) { if (e instanceof NotAcceptableException) { obj = handleNotAcceptableGetRequestJsonException(target, clazz); } else { log.error("Failed to process request " + path, e); } } return obj; } /** * call a GET request * * @param path the path to call. * @param params key, value parameters to add to the request * @param type the GenericType of Class T * @param <T> the class to return * @return the response of class type T */ public <T> T get(String path, Map<String, Object> params, GenericType<T> type) { WebTarget target = buildTarget(path, params); return target.request(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.APPLICATION_JSON_TYPE, MediaType.APPLICATION_XML_TYPE).get(type); } /** * POST an object to a given url * * @param path the path to access * @param o the object to post * @return the response */ public Response post(String path, Object o) { WebTarget target = buildTarget(path, null); return target.request().post(Entity.entity(o, MediaType.APPLICATION_JSON_TYPE)); } /** * POST an object with multiplepart. For example Uploading a file. * Below is a sample on how to create a Multipart from a string and post it * String xml = "some string"; * final FormDataBodyPart templatePart = new FormDataBodyPart("template", xml, MediaType.APPLICATION_OCTET_STREAM_TYPE); * * FormDataContentDisposition.FormDataContentDispositionBuilder disposition = FormDataContentDisposition.name(templatePart.getName()); * disposition.fileName("fileName"); * templatePart.setFormDataContentDisposition(disposition.build()); * // Combine parts * MultiPart multiPart = new MultiPart(); * multiPart.bodyPart(templatePart); * multiPart.setMediaType(MediaType.MULTIPART_FORM_DATA_TYPE); * * @param path the path to access * @param object the multiplart object to post * @param returnType the type to return * @return returns the response of the type T */ public <T> T postMultiPart(String path, MultiPart object, Class<T> returnType) { WebTarget target = buildTarget(path, null); MediaType contentType = MediaType.MULTIPART_FORM_DATA_TYPE; contentType = Boundary.addBoundary(contentType); return target.request().post(Entity.entity(object, contentType), returnType); } /** * POST a multipart object streaming * * @param path the path to access * @param name the name of the param the endpoint is expecting * @param fileName the name of the file * @param stream the stream itself * @param returnType the type to return from the post * @return the response of type T */ public <T> T postMultiPartStream(String path, String name, String fileName, InputStream stream, Class<T> returnType) { WebTarget target = buildTarget(path, null); MultiPart multiPart = new MultiPart(MediaType.MULTIPART_FORM_DATA_TYPE); StreamDataBodyPart streamDataBodyPart = new StreamDataBodyPart(name, stream, fileName, MediaType.APPLICATION_OCTET_STREAM_TYPE); multiPart.getBodyParts().add(streamDataBodyPart); MediaType contentType = MediaType.MULTIPART_FORM_DATA_TYPE; contentType = Boundary.addBoundary(contentType); return target.request().post( Entity.entity(multiPart, contentType), returnType); } /** * POST an object * * @param path the path to access * @param object the object to post * @param returnType the class to return * @return the response of type T */ public <T> T post(String path, Object object, Class<T> returnType) { WebTarget target = buildTarget(path, null); return target.request().post(Entity.entity(object, MediaType.APPLICATION_JSON), returnType); } /** * PUT request * * @param path the path to access * @param object the object to PUT * @param returnType the class to return * @return the response of type T */ public <T> T put(String path, Object object, Class<T> returnType) { WebTarget target = buildTarget(path, null); return target.request().put(Entity.entity(object, MediaType.APPLICATION_JSON), returnType); } /** * DELETE request * * @param path the path to access * @param params Any additional Query params to add to the DELETE call * @param returnType the class to return * @return the response of type T */ public <T> T delete(String path, Map<String, Object> params, Class<T> returnType) { WebTarget target = buildTarget(path, params); return target.request().delete(returnType); } /** * POST a request from a {@link Form} object * * @param path the path to access * @param form the Form to POST * @param returnType the class to return * @return the response of type T */ public <T> T postForm(String path, Form form, Class<T> returnType) { WebTarget target = buildTarget(path, null); return target.request().post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), returnType); } /** * POST a request Async. * * @param path the path to access * @param object the object to POST * @param returnType the class to return * @return a Future of type T */ public <T> Future<T> postAsync(String path, Object object, Class<T> returnType) { WebTarget target = buildTarget(path, null); return target.request().async().post(Entity.entity(object, MediaType.APPLICATION_JSON), returnType); } /** * if a request doesnt like the accepted type (i.e. its coded for TEXT instead of JSON, try to resolve the JSON by getting the JSON string * This can be called in the Exception of a particular GET request which will attempt to resolve the correct object from the Response string. * * @param target the WebTarget * @param clazz the class to return * @return the response of type T * @see JerseyRestClient#get(String, Map, Class) * @see JerseyRestClient#getFromPathString(String, Class) */ private <T> T handleNotAcceptableGetRequestJsonException(WebTarget target, Class<T> clazz) { T obj = null; try { //the response didnt link getting data in JSON.. attempt to get it in TEXT and convert to JSON String jsonString = target.request(MediaType.APPLICATION_JSON_TYPE).accept(MediaType.TEXT_PLAIN_TYPE).get(String.class); if (StringUtils.isNotBlank(jsonString)) { try { obj = objectMapper.readValue(jsonString, clazz); } catch (Exception ex) { //unable to deserialize string log.error("Unable to deserialize request to JSON for string {}, for target {} returning class {} ", jsonString, target, clazz); } } } catch (Exception ex1) { //swallow the exception. cant do anything about it. log.error("Unable to deserialize request for target {} returning class {} ", target, clazz); } return obj; } }