org.mule.modules.apifortress.ApiFortressClient.java Source code

Java tutorial

Introduction

Here is the source code for org.mule.modules.apifortress.ApiFortressClient.java

Source

/**
 * (c) 20013-2016 API Fortress, Inc. The software in this package is published under the terms of the Commercial Free Software license V.1, a copy of which has been included with this distribution in the LICENSE.md file.
 */
package org.mule.modules.apifortress;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.json.JSONObject;
import org.mule.modules.apifortress.config.Config;
import org.mule.modules.apifortress.exceptions.ApiFortressIOException;
import org.mule.modules.apifortress.exceptions.ApiFortressParseException;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

/**
 * The API Fortress logic
 * @author Simone Pezzano - simone@apifortress.com
 */
@Component
public class ApiFortressClient {

    /**
     * Jackson object mapper to perform JSON/Object Object/JSON conversions
     */
    private static final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * A pooling connection manager
     */
    private final PoolingHttpClientConnectionManager connectionManager;

    /**
     * an HttpClient
     */
    private CloseableHttpClient client;

    /**
     * True when no notifications are to be sent
     */
    private final boolean silent;

    /**
     * True when no events have to be stored 
     */
    private final boolean dryRun;

    /**
     * The threshold limiter
     */
    private final int threshold;

    /**
     * Counts how many tests the current instance processed
     */
    private int counter = 0;

    public static final String CT_APPLICATION_JSON = "application/json";
    public static final String CT_TEXT_XML = "text/xml";
    public static final String CT_TEXT_PLAIN = "text/plain";

    public static final String HEADER_CONTENT_TYPE = "content-type";

    public static final String MODE_RUN = "run";
    public static final String MODE_AUTOMATCH = "automatch";

    public static final String ATTR_PARAMS = "params";
    public static final String ATTR_CONTENT_TYPE = "Content-Type";
    public static final String ATTR_PAYLOAD = "payload";
    public static final String ATTR_PAYLOAD_RESPONSE = "payload_response";
    public static final String ATTR_HEADERS = "headers";

    /**
     * Constructor
     * @param config the connector config. Won't be referenced
     */
    public ApiFortressClient(Config config) {
        super();

        connectionManager = new PoolingHttpClientConnectionManager();
        connectionManager.setMaxTotal(config.getTotalConnections());

        final RequestConfig requestConfig = RequestConfig.custom()
                .setConnectTimeout(config.getConnectTimeout() * 1000)
                .setSocketTimeout(config.getSocketTimeout() * 1000).build();
        client = HttpClients.custom().setConnectionManager(connectionManager).setDefaultRequestConfig(requestConfig)
                .build();
        silent = config.isSilent();
        dryRun = config.isDryRun();
        threshold = config.getThreshold();

    }

    /**
     * Runs a single test
     * @param payload the payload
     * @param headers the response headers
     * @param params variables to be injected in the scope of the test
     * @param hook the API Hook url
     * @param testId the test id
     * @param synchronous true if the test has to return the result
     * @return The data received from API Fortress, or null if the threshold limiter denied the test execution
     * @throws ApiFortressIOException when the communication with an API Fortress service instance fails 
     * @throws ApiFortressParseException when the connector did not succeed in converting a payload object into JSON
     */
    public String runTest(Object payload, Map<String, Object> headers, Map<String, Object> params, URL hook,
            String testId, boolean synchronous) throws ApiFortressIOException, ApiFortressParseException {
        counter++;
        /*
         * Limiter. Only multiple of 'threshold' are processed
         */
        if (counter % threshold != 0) {
            return null;
        }

        final String url = composeUrl(hook.toString(), testId, MODE_RUN, synchronous, silent, dryRun);
        try {
            final String body = new JSONObject(buildBodyMap(payload, headers, params)).toString();
            return run(url, body);
        } catch (IOException ex) {
            throw new ApiFortressIOException("Could not communicate with API Fortress", ex);
        }
    }

    private String run(String url, String body) throws IOException {
        final HttpPost post = new HttpPost(url);
        final StringEntity entity = new StringEntity(body);
        entity.setContentType(CT_APPLICATION_JSON);
        post.setEntity(entity);
        final HttpResponse response = client.execute(post);
        final int statusCode = response.getStatusLine().getStatusCode();

        final HttpEntity responseEntity = response.getEntity();
        final InputStream is = responseEntity.getContent();
        final String dataBack = IOUtils.toString(is);
        is.close();
        EntityUtils.consume(responseEntity);

        /**
         * failure when test execution has been rejected for some reason
         */
        if (statusCode != 200) {
            throw new ApiFortressIOException("Test execution unsuccessful. Status code=" + statusCode);
        }

        return dataBack;
    }

    /**
     * Builds the map representing the data to be sent to API Fortress
     * @param payload the payload
     * @param headers the response headers
     * @param params variables to be injected in the scope of the test
     * @return a map representing the message to be sent to API Fortress
     * @throws ApiFortressParseException when the payload object cannot be converted into JSON 
     */
    public static Map<String, Object> buildBodyMap(Object payload, Map<String, Object> headers,
            Map<String, Object> params) throws ApiFortressParseException {
        final HashMap<String, Object> map = new HashMap<>();

        /*
         * if the payload is a string, we can proceed straight forward
         */

        if (payload instanceof String) {
            map.put(ATTR_PAYLOAD, payload);
        }
        /*
         * If the payload is an object, then we convert it to a string
         */
        else {
            try {
                map.put(ATTR_PAYLOAD, objectMapper.writeValueAsString(payload));
            } catch (JsonProcessingException ex) {
                throw new ApiFortressParseException("Could not transform payload object into JSON", ex);
            }
        }
        /*
         * Content-Type is very important to API Fortress to understand what it has
         * to do
         */
        map.put(ATTR_CONTENT_TYPE, getContentType(headers));

        /*
         * Params are extra values you might want in the API Fortress variable
         * scope
         */
        map.put(ATTR_PARAMS, params);

        /*
         * payload_response is the context of the response itself
         */
        final HashMap<String, Object> response = new HashMap<>();
        params.put(ATTR_PAYLOAD_RESPONSE, response);

        response.put(ATTR_HEADERS, extractHeaders(headers));
        return map;
    }

    /**
     * Given a set of headers, it filters out all the content that shouldn't be sent to API Fortress
     * and returns a clean map of headers
     * @param originalHeaders the provided headers headers
     * @return a map representing the cleaned headers
     */
    public static Map<String, Object> extractHeaders(Map<String, Object> originalHeaders) {
        final HashMap<String, Object> headers = new HashMap<>();

        final Iterator<Entry<String, Object>> pxIterator = originalHeaders.entrySet().iterator();
        while (pxIterator.hasNext()) {
            final Entry<String, Object> entry = pxIterator.next();
            /*
             * http. , mule.  and apif. are reserved prefixes and will not contain headers.
             */
            String key = entry.getKey();
            final boolean validHeader = !key.startsWith("http.") && !key.startsWith("mule.")
                    && !key.startsWith("apif.");

            // If a header prefix has been configured, then only the items with that header prefix will be collected
            if (validHeader) {
                headers.put(key, entry.getValue());
            }
        }
        return headers;
    }

    /**
     * Adds a URL element in the body to be sent to API Fortress
     * @param map the request to API Fortress, in the form of a map
     * @param automatch the automatch string
     * @return the modified message
     */
    public static Map<String, Object> addUrlToBodyMap(Map<String, Object> map, String automatch) {
        if (StringUtils.isNotEmpty(automatch)) {
            map.put("url", automatch);
        } else {
            throw new IllegalArgumentException("the apif.url outbound property is required");
        }
        return map;
    }

    /**
     * Retrieves and sanitizes the content type from the headers
     * @param headers the headers map
     * @return the retrieved content-type
     */
    public static String getContentType(Map<String, Object> headers) {
        String contentType = (String) headers.get(HEADER_CONTENT_TYPE);
        if (contentType == null) {
            contentType = CT_TEXT_PLAIN;
        } else if (contentType.contains(CT_APPLICATION_JSON)) {
            contentType = CT_APPLICATION_JSON;
        } else if (contentType.contains(CT_TEXT_XML)) {
            contentType = CT_TEXT_XML;
        }
        return contentType;
    }

    /**
     * Runs an automatch test
     * @param payload the payload
     * @param headers the response headers
     * @param params variables to be injected in the scope of the test
     * @param hook the API Hook URL
     * @param synchronous true if the tests need to return the results
     * @return the results from API Fortress or null if the threshold limiter avoided the test execution
     * @throws ApiFortressIOException when the communication with an API Fortress service instance fails 
     * @throws ApiFortressParseException when the connector did not succeed in converting a payload object into JSON
     */
    public String runAutomatch(Object payload, Map<String, Object> headers, Map<String, Object> params, URL hook,
            boolean synchronous, String automatch) throws ApiFortressIOException, ApiFortressParseException {

        counter++;
        /*
         * Limiter. Only multiple of 'threshold' are processed
         */
        if (counter % threshold != 0) {
            return null;
        }
        final String url = composeUrl(hook.toString(), null, MODE_AUTOMATCH, synchronous, silent, dryRun);
        try {
            String body = new JSONObject(addUrlToBodyMap(buildBodyMap(payload, headers, params), automatch))
                    .toString();
            return run(url, body);
        } catch (IOException ex) {
            throw new ApiFortressIOException("Could not communicate with API Fortress", ex);
        }
    }

    /**
     * Composes the API Fortress URL, based on the settings
     * @param hook the hook URL
     * @param testId the test id
     * @param mode the mode, either run or automatch
     * @param synchronous true if the test needs to return the evaluation result
     * @param silent true if notifications need to be sent
     * @param dryRun true if no event should be saved in API Fortress
     * @return the composed URL
     */
    public static String composeUrl(String hook, String testId, String mode, boolean synchronous, boolean silent,
            boolean dryRun) {
        String url = hook + "/tests/";
        if (mode.equals(MODE_RUN)) {
            url += testId + "/";
        }
        url += mode;
        url += ("?sync=" + synchronous + "&silent=" + silent + "&dryrun=" + dryRun + "&nosets=true");
        return url;
    }

}