Java tutorial
/* * Copyright 2006-2018 Micro Focus International plc. * Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. */ package com.autonomy.aci.client.transport.impl; import com.autonomy.aci.client.services.AciConstants; import com.autonomy.aci.client.transport.AciHttpClient; import com.autonomy.aci.client.transport.AciHttpException; import com.autonomy.aci.client.transport.AciParameter; import com.autonomy.aci.client.transport.AciResponseInputStream; import com.autonomy.aci.client.transport.AciServerDetails; import com.autonomy.aci.client.transport.ActionParameter; import com.autonomy.aci.client.transport.EncryptionCodec; import com.autonomy.aci.client.transport.EncryptionCodecException; import com.autonomy.aci.client.util.ActionParameters; import com.autonomy.aci.client.util.EncryptionCodecUtils; import org.apache.commons.lang.Validate; import org.apache.http.Header; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.utils.URIBuilder; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Set; /** * Implementation of the {@link com.autonomy.aci.client.transport.AciHttpClient} interface that provides the actual HTTP * communication mechanism. This implementation uses the HttpClient provided by the <a href="http://hc.apache.org/"> * Apache HttpComponents</a> project. It defaults to using the HTTP <tt>GET</tt> method, if you wish to send ACI actions * with the HTTP <tt>POST</tt> method, then call the {@link #setUsePostMethod(boolean)} method with {@code true}. * <p> * This implementation of the {@link com.autonomy.aci.client.transport.AciHttpClient} interface does no configuration of * the {@code HttpClient} that it uses. It expects all the configuration to have been done by the user before passing it * to this object. This configuration can be done in normal code, via the * {@link com.autonomy.aci.client.transport.impl.HttpClientFactory}, or via an IoC container like * <a href="http://www.springsource.org/">Spring</a>. * @see <a href="http://hc.apache.org/">Apache HttpComponents</a> */ public class AciHttpClientImpl implements AciHttpClient { /** * Class logger... */ private static final Logger LOGGER = LoggerFactory.getLogger(AciHttpClientImpl.class); /** * Holds the {@code HttpClient} that will do the work. By allowing it to be passed in as a parameter, it means it * can be configured in an IoC container like {@code Spring} before being injected. */ private HttpClient httpClient; /** * Holds value of property usePostMethod. */ private boolean usePostMethod; /** * Creates a new instance of AciHttpClientImpl. The {@code setHttpClient} method <strong>must</strong> must be * called before tyring to use this object to execute an ACI action, otherwise a {@code NullPointerException} will * be generated. */ public AciHttpClientImpl() { // Empty... } /** * Creates a new instance of AciHttpClientImpl. * @param httpClient The {@code HttpClient} to use */ public AciHttpClientImpl(final HttpClient httpClient) { // Save the httpClient... this.httpClient = httpClient; } /** * Turns the {@code parameters} and {@code serverDetails} into either an HTTP GET or POST request. * @param serverDetails The details of the ACI server the request will be sent to * @param parameters The parameters to send with the ACI action. * @return A HTTP GET or POST request that can be used to execute the ACI action * @throws EncryptionCodecException If something went wrong encrypting the parameters * @throws URISyntaxException If something went wrong creating the URI to send the action to * @throws UnsupportedEncodingException If there was a problem working with the parameters in the specified * character encoding */ private HttpUriRequest constructHttpRequest(final AciServerDetails serverDetails, final Set<? extends ActionParameter<?>> parameters) throws EncryptionCodecException, URISyntaxException, UnsupportedEncodingException { LOGGER.trace("constructHttpMethod() called..."); // Copy the parameters... final Set<? extends ActionParameter<?>> params = (serverDetails.getEncryptionCodec() == null) ? parameters : createEncryptedParameters(serverDetails, parameters); final boolean hasPostParameter = parameters.stream().anyMatch(ActionParameter::requiresPostRequest); // If an InputStream parameter has been provided, use a post request regardless if (usePostMethod || hasPostParameter) { return createPostMethod(serverDetails, params); } else { return createGetMethod(serverDetails, params); } } /** * Takes the passed in set of parameters and encrypts them. * @param serverDetails The details of the ACI server the request will be sent to * @param parameters The parameters to send with the ACI action. * @return A set of encrypted parameters * @throws EncryptionCodecException if something went wrong encrypting the parameters */ private Set<? extends ActionParameter<?>> createEncryptedParameters(final AciServerDetails serverDetails, final Set<? extends ActionParameter<?>> parameters) throws EncryptionCodecException { LOGGER.trace("createEncryptedParameters() called..."); // Generate the query String and put it through the codec... final String data = EncryptionCodecUtils.getInstance().encrypt(serverDetails.getEncryptionCodec(), convertParameters(parameters, serverDetails.getCharsetName()), serverDetails.getCharsetName()); // Create the parameters for an encrypted action... return new ActionParameters(new AciParameter(AciConstants.PARAM_ACTION, AciConstants.ACTION_ENCRYPTED), new AciParameter(AciConstants.PARAM_DATA, data)); } /** * Create a {@code GetMethod} and adds the ACI parameters to the query string. * @param serverDetails The details of the ACI server the request will be sent to * @param parameters The parameters to send with the ACI action. * @return a {@code HttpGet} that is ready to execute the ACI action. * @throws URISyntaxException If there was a problem construction the request URI from the <tt>serverDetails</tt> * and <tt>parameters</tt> */ private HttpUriRequest createGetMethod(final AciServerDetails serverDetails, final Set<? extends ActionParameter<?>> parameters) throws URISyntaxException { LOGGER.trace("createGetMethod() called..."); // Create the URI to use... final URI uri = new URIBuilder() .setScheme(serverDetails.getProtocol().toString().toLowerCase(Locale.ENGLISH)) .setHost(serverDetails.getHost()).setPort(serverDetails.getPort()).setPath("/") .setQuery(convertParameters(parameters, serverDetails.getCharsetName())).build(); // Return the constructed get method... return new HttpGet(uri); } /** * Create a {@code PostMethod} and adds the ACI parameters to the request body. * @param serverDetails The details of the ACI server the request will be sent to * @param parameters The parameters to send with the ACI action. * @return An {@code HttpPost} that is ready to execute the ACI action. * @throws UnsupportedEncodingException Will be thrown if <tt>serverDetails.getCharsetName()</tt> returns a * charset that is not supported by the JVM * @throws URISyntaxException If there was a problem construction the request URI from the * <tt>serverDetails</tt> and <tt>parameters</tt> */ private HttpUriRequest createPostMethod(final AciServerDetails serverDetails, final Set<? extends ActionParameter<?>> parameters) throws URISyntaxException, UnsupportedEncodingException { LOGGER.trace("createPostMethod() called..."); // Create the URI to use... final URI uri = new URIBuilder() .setScheme(serverDetails.getProtocol().toString().toLowerCase(Locale.ENGLISH)) .setHost(serverDetails.getHost()).setPort(serverDetails.getPort()).setPath("/").build(); // Create the method... final HttpPost method = new HttpPost(uri); final Charset charset = Charset.forName(serverDetails.getCharsetName()); final boolean requiresMultipart = parameters.stream().anyMatch(ActionParameter::requiresPostRequest); if (requiresMultipart) { final MultipartEntityBuilder multipartEntityBuilder = MultipartEntityBuilder.create(); multipartEntityBuilder.setCharset(charset); parameters.forEach(parameter -> parameter.addToEntity(multipartEntityBuilder, charset)); // Convert the parameters into an entity... method.setEntity(multipartEntityBuilder.build()); } else { method.setEntity(new StringEntity(convertParameters(parameters, serverDetails.getCharsetName()), serverDetails.getCharsetName())); } // Return the method... return method; } /** * Converts a list of {@code AciParameter} objects into an array of {@code NameValuePair} objects suitable for use * in both POST and GET methods. * @param parameters The set of parameters to convert. * @param charsetName The name of the charset to use when encoding the parameters * @return an <tt>String</tt> representing the query string portion of a URI */ private String convertParameters(final Set<? extends ActionParameter<?>> parameters, final String charsetName) { LOGGER.trace("convertParameters() called..."); // Just incase, remove the allowed null entry... parameters.remove(null); final List<NameValuePair> pairs = new ArrayList<>(parameters.size()); LOGGER.debug("Converting {} parameters...", parameters.size()); NameValuePair actionPair = null; for (final ActionParameter<?> parameter : parameters) { final Object value = parameter.getValue(); if (value instanceof String) { final String stringValue = (String) value; if (AciConstants.PARAM_ACTION.equalsIgnoreCase(parameter.getName())) { actionPair = new BasicNameValuePair(parameter.getName(), stringValue); } else { pairs.add(new BasicNameValuePair(parameter.getName(), stringValue)); } } } // Ensure that the action=XXX parameter is the first thing in the list... Validate.isTrue(actionPair != null, "No action parameter found in parameter set, please set one before trying to execute an ACI request."); pairs.add(0, actionPair); // Convert to a string and return... return URLEncodedUtils.format(pairs, charsetName); } /** * Execute an ACI action on the specific ACI server. * @param serverDetails Details of the ACI server to send the action to * @param parameters The parameters to send with the ACI action * @return An <tt>AciResponseInputStream</tt> containing the ACI response * @throws IOException If an I/O (transport) error occurs. Some transport exceptions can be recovered from * @throws AciHttpException If a protocol exception occurs. Usually protocol exceptions cannot be recovered from * @throws IllegalArgumentException if the <tt>httpClient</tt> property is <tt>null</tt> or <tt>parameters</tt> is <tt>null</tt> */ @Override public AciResponseInputStream executeAction(final AciServerDetails serverDetails, final Set<? extends ActionParameter<?>> parameters) throws IOException, AciHttpException { LOGGER.trace("executeAction() called..."); Validate.notNull(httpClient, "You must set the HttpClient instance to use before using this class."); Validate.notEmpty(parameters, "The parameter set must not be null or empty."); try { // Create the method we're going to use... final HttpUriRequest request = constructHttpRequest(serverDetails, parameters); LOGGER.debug("Executing action on {}:{}...", serverDetails.getHost(), serverDetails.getPort()); // Execute the method... final HttpResponse response = httpClient.execute(request); final int statusCode = response.getStatusLine().getStatusCode(); LOGGER.debug("Executed method and got status code - {}...", statusCode); // Treat anything other than a 2xx status code as an error... if ((statusCode < 200) || (statusCode >= 300)) { // close the connection so it can be reused EntityUtils.consume(response.getEntity()); throw new AciHttpException("The server returned a status code, " + statusCode + ", that wasn't in the 2xx Success range."); } // Decorate the InputStream so we can release the HTTP connection once the stream's been read... return decryptResponse(serverDetails.getEncryptionCodec(), response) ? new DecryptingAciResponseInputStreamImpl(serverDetails, response) : new AciResponseInputStreamImpl(response); } catch (final ClientProtocolException cpe) { throw new AciHttpException( "A HTTP protocol Exception has been caught while trying to execute the ACI request.", cpe); } catch (final EncryptionCodecException ece) { throw new AciHttpException("Unable to send the ACI request due to an encryption failure.", ece); } catch (final URISyntaxException urise) { throw new AciHttpException("Unable to construct the URI required to send the ACI request.", urise); } } private boolean decryptResponse(final EncryptionCodec encryptionCodec, final HttpResponse response) { LOGGER.trace("decryptResponse() called..."); // If there is no encryptionCodec then we don't need to check the headers... boolean decryptResponse = (encryptionCodec != null); LOGGER.debug("Using an EncryptionCodec - {}...", decryptResponse); if (decryptResponse) { LOGGER.debug("Checking AUTN-Content-Type response header..."); // This response header is only supplied with encrypted responses, so if it's not there we don't decrypt, // i.e. it's either an OEM IDOL, or they did encryptResponse=false... final Header header = response.getFirstHeader("AUTN-Content-Type"); if (header == null) { LOGGER.debug("No AUTN-Content-Type response header, so don't auto decrypt response..."); decryptResponse = false; } } // Send back the flag... return decryptResponse; } /** * Getter for property httpClient. * @return Value of property httpClient */ public HttpClient getHttpClient() { return this.httpClient; } /** * Setter for property httpClient. * @param httpClient New value of property httpClient */ public void setHttpClient(final HttpClient httpClient) { this.httpClient = httpClient; } /** * Getter for property usePostMethod. * @return Value of property usePostMethod */ public boolean isUsePostMethod() { return this.usePostMethod; } /** * Setter for property usePostMethod. * @param usePostMethod New value of property usePostMethod */ public void setUsePostMethod(final boolean usePostMethod) { this.usePostMethod = usePostMethod; } }