com.maxmind.minfraud.WebServiceClient.java Source code

Java tutorial

Introduction

Here is the source code for com.maxmind.minfraud.WebServiceClient.java

Source

package com.maxmind.minfraud;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.InjectableValues;
import com.fasterxml.jackson.databind.InjectableValues.Std;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.maxmind.minfraud.exception.*;
import com.maxmind.minfraud.request.Transaction;
import com.maxmind.minfraud.response.FactorsResponse;
import com.maxmind.minfraud.response.InsightsResponse;
import com.maxmind.minfraud.response.ScoreResponse;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;

import java.io.Closeable;
import java.io.IOException;
import java.net.*;
import java.util.*;

import static org.apache.http.entity.ContentType.APPLICATION_JSON;

/**
 * Client for MaxMind minFraud Score, Insights, and Factors
 */
public final class WebServiceClient implements Closeable {
    private static final String pathBase = "/minfraud/v2.0/";

    private final String host;
    private final int port;
    private final boolean useHttps;
    private final List<String> locales;
    private final String licenseKey;
    private final int userId;

    private final ObjectMapper mapper;
    private final CloseableHttpClient httpClient;

    private WebServiceClient(WebServiceClient.Builder builder) {
        host = builder.host;
        port = builder.port;
        useHttps = builder.useHttps;
        locales = builder.locales;
        licenseKey = builder.licenseKey;
        userId = builder.userId;

        mapper = new ObjectMapper();
        mapper.disable(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS);
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        RequestConfig.Builder configBuilder = RequestConfig.custom().setConnectTimeout(builder.connectTimeout)
                .setSocketTimeout(builder.readTimeout);

        if (builder.proxy != null) {
            InetSocketAddress address = (InetSocketAddress) builder.proxy.address();
            HttpHost proxyHost = new HttpHost(address.getHostName(), address.getPort());
            configBuilder.setProxy(proxyHost);
        }

        RequestConfig config = configBuilder.build();
        httpClient = HttpClientBuilder.create().setUserAgent(userAgent()).setDefaultRequestConfig(config).build();
    }

    /**
     * <p>
     * {@code Builder} creates instances of {@code WebServiceClient}
     * from values set by the methods.
     * </p>
     * <p>
     * This example shows how to create a {@code WebServiceClient} object
     * with the {@code Builder}:
     * </p>
     * <p>
     * WebServiceClient client = new
     * WebServiceClient.Builder(12,"licensekey").host
     * ("geoip.maxmind.com").build();
     * </p>
     * <p>
     * Only the values set in the {@code Builder} constructor are required.
     * </p>
     */
    public static final class Builder {
        final int userId;
        final String licenseKey;

        String host = "minfraud.maxmind.com";
        int port = 443;
        boolean useHttps = true;

        int connectTimeout = -1;
        int readTimeout = -1;

        List<String> locales = Collections.singletonList("en");
        private Proxy proxy;

        /**
         * @param userId     Your MaxMind user ID.
         * @param licenseKey Your MaxMind license key.
         */
        public Builder(int userId, String licenseKey) {
            this.userId = userId;
            this.licenseKey = licenseKey;
        }

        /**
         * @param val Timeout in milliseconds to establish a connection to the
         *            web service. There is no timeout by default.
         * @return Builder object
         */
        public WebServiceClient.Builder connectTimeout(int val) {
            connectTimeout = val;
            return this;
        }

        /**
         * Disables HTTPS to connect to a test server or proxy. The minFraud ScoreResponse and InsightsResponse web services require
         * HTTPS.
         *
         * @return Builder object
         */
        public WebServiceClient.Builder disableHttps() {
            useHttps = false;
            return this;
        }

        /**
         * @param val The host to use.
         * @return Builder object
         */
        public WebServiceClient.Builder host(String val) {
            host = val;
            return this;
        }

        /**
         * @param val The port to use.
         * @return Builder object
         */
        public WebServiceClient.Builder port(int val) {
            port = val;
            return this;
        }

        /**
         * @param val List of locale codes to use in name property from most
         *            preferred to least preferred.
         * @return Builder object
         */
        public WebServiceClient.Builder locales(List<String> val) {
            if (locales == null) {
                throw new IllegalArgumentException("locales must not be null");
            }
            locales = new ArrayList<>(val);
            return this;
        }

        /**
         * @param val readTimeout in milliseconds to read data from an
         *            established connection to the web service. There is no
         *            timeout by default.
         * @return Builder object
         */
        public WebServiceClient.Builder readTimeout(int val) {
            readTimeout = val;
            return this;
        }

        /**
         * @param val the proxy to use when making this request.
         * @return Builder object
         */
        public Builder proxy(Proxy val) {
            this.proxy = val;
            return this;
        }

        /**
         * @return an instance of {@code WebServiceClient} created from the
         * fields set on this builder.
         */
        public WebServiceClient build() {
            return new WebServiceClient(this);
        }
    }

    /**
     * Make a minFraud Factors request to the web service using the transaction
     * request object passed to the method.
     *
     * @param transaction A transaction request object.
     * @return An Factors model object
     * @throws InsufficientFundsException  when there are insufficient funds on
     *                                     the account.
     * @throws AuthenticationException     when there is a problem authenticating.
     * @throws InvalidRequestException     when the request is invalid for some
     *                                     other reason.
     * @throws PermissionRequiredException when permission is required to use the
     *                                     service.
     * @throws MinFraudException           when the web service returns unexpected
     *                                     content.
     * @throws HttpException               when the web service returns an unexpected
     *                                     response.
     * @throws IOException                 when some other IO error occurs.
     */
    public FactorsResponse factors(Transaction transaction)
            throws IOException, MinFraudException, InsufficientFundsException, InvalidRequestException,
            AuthenticationException, PermissionRequiredException, HttpException {
        return responseFor("factors", transaction, FactorsResponse.class);
    }

    /**
     * Make a minFraud Insights request to the web service using the transaction
     * request object passed to the method.
     *
     * @param transaction A transaction request object.
     * @return An Insights model object
     * @throws InsufficientFundsException  when there are insufficient funds on
     *                                     the account.
     * @throws AuthenticationException     when there is a problem authenticating.
     * @throws InvalidRequestException     when the request is invalid for some
     *                                     other reason.
     * @throws PermissionRequiredException when permission is required to use the
     *                                     service.
     * @throws MinFraudException           when the web service returns unexpected
     *                                     content.
     * @throws HttpException               when the web service returns an unexpected
     *                                     response.
     * @throws IOException                 when some other IO error occurs.
     */
    public InsightsResponse insights(Transaction transaction)
            throws IOException, MinFraudException, InsufficientFundsException, InvalidRequestException,
            AuthenticationException, PermissionRequiredException, HttpException {
        return responseFor("insights", transaction, InsightsResponse.class);
    }

    /**
     * Make a minFraud Score request to the web service using the transaction
     * request object passed to the method.
     *
     * @param transaction A transaction request object.
     * @return An Score model object
     * @throws InsufficientFundsException  when there are insufficient funds on
     *                                     the account.
     * @throws AuthenticationException     when there is a problem authenticating.
     * @throws InvalidRequestException     when the request is invalid for some
     *                                     other reason.
     * @throws PermissionRequiredException when permission is required to use the
     *                                     service.
     * @throws MinFraudException           when the web service returns unexpected
     *                                     content.
     * @throws HttpException               when the web service returns an unexpected
     *                                     response.
     * @throws IOException                 when some other IO error occurs.
     */
    public ScoreResponse score(Transaction transaction)
            throws IOException, MinFraudException, InsufficientFundsException, InvalidRequestException,
            AuthenticationException, PermissionRequiredException, HttpException {
        return responseFor("score", transaction, ScoreResponse.class);
    }

    private <T> T responseFor(String service, Transaction transaction, Class<T> cls)
            throws IOException, MinFraudException {
        if (transaction == null) {
            throw new IllegalArgumentException("transaction must not be null");
        }
        URL url = createUrl(WebServiceClient.pathBase + service);
        HttpPost request = requestFor(transaction, url);

        try (CloseableHttpResponse response = httpClient.execute(request)) {
            return handleResponse(response, url, cls);
        }
    }

    private HttpPost requestFor(Transaction transaction, URL url) throws MinFraudException, IOException {
        Credentials credentials = new UsernamePasswordCredentials(Integer.toString(userId), licenseKey);

        HttpPost request;
        try {
            request = new HttpPost(url.toURI());
        } catch (URISyntaxException e) {
            throw new MinFraudException("Error parsing request URL", e);
        }
        try {
            request.addHeader(new BasicScheme().authenticate(credentials, request, null));
        } catch (org.apache.http.auth.AuthenticationException e) {
            throw new AuthenticationException("Error setting up request authentication", e);
        }
        request.addHeader("Accept", "application/json");
        request.addHeader("User-Agent", this.userAgent());

        String requestBody = transaction.toJson();

        StringEntity input = new StringEntity(requestBody, APPLICATION_JSON);

        request.setEntity(input);
        return request;
    }

    private <T> T handleResponse(CloseableHttpResponse response, URL url, Class<T> cls)
            throws MinFraudException, IOException {
        int status = response.getStatusLine().getStatusCode();
        if (status >= 400 && status < 500) {
            this.handle4xxStatus(response, url);
        } else if (status >= 500 && status < 600) {
            throw new HttpException("Received a server error (" + status + ") for " + url, status, url);
        } else if (status != 200) {
            throw new HttpException("Received an unexpected HTTP status (" + status + ") for " + url, status, url);
        }

        HttpEntity entity = response.getEntity();

        if (entity.getContentLength() <= 0L) {
            throw new HttpException("Received a 200 response for " + url + " but there was no message body.", 200,
                    url);
        }

        InjectableValues inject = new Std().addValue("locales", locales);

        try {
            return mapper.readerFor(cls).with(inject).readValue(entity.getContent());
        } catch (IOException e) {
            throw new MinFraudException("Received a 200 response but could not decode it as JSON", e);
        } finally {
            EntityUtils.consume(entity);
        }
    }

    private void handle4xxStatus(HttpResponse response, URL url) throws IOException, InsufficientFundsException,
            InvalidRequestException, AuthenticationException, PermissionRequiredException {
        HttpEntity entity = response.getEntity();
        int status = response.getStatusLine().getStatusCode();

        if (entity.getContentLength() <= 0L) {
            throw new HttpException("Received a " + status + " error for " + url + " with no body", status, url);
        }

        String body = EntityUtils.toString(entity, "UTF-8");

        Map<String, String> content;
        try {
            content = mapper.readValue(body, new TypeReference<HashMap<String, String>>() {
            });
            this.handleErrorWithJsonBody(content, body, status, url);
        } catch (HttpException e) {
            throw e;
        } catch (IOException e) {
            throw new HttpException("Received a " + status + " error for " + url
                    + " but it did not include the expected JSON body: " + body, status, url, e);
        }
    }

    private void handleErrorWithJsonBody(Map<String, String> content, String body, int status, URL url)
            throws HttpException, InsufficientFundsException, InvalidRequestException, AuthenticationException,
            PermissionRequiredException {
        String error = content.get("error");
        String code = content.get("code");

        if (error == null || code == null) {
            throw new HttpException(
                    "Error response contains JSON but it does not specify code or error keys: " + body, status,
                    url);
        }

        switch (code) {
        case "AUTHORIZATION_INVALID":
        case "LICENSE_KEY_REQUIRED":
        case "USER_ID_REQUIRED":
            throw new AuthenticationException(error);
        case "INSUFFICIENT_FUNDS":
            throw new InsufficientFundsException(error);
        case "PERMISSION_REQUIRED":
            throw new PermissionRequiredException(error);
        default:
            throw new InvalidRequestException(error, code, status, url, null);
        }
    }

    private URL createUrl(String path) throws MinFraudException {
        try {
            return new URIBuilder().setScheme(useHttps ? "https" : "http").setHost(this.host).setPort(this.port)
                    .setPath(path).build().toURL();
        } catch (MalformedURLException | URISyntaxException e) {
            throw new MinFraudException("Error creating service URL", e);
        }
    }

    private String userAgent() {
        return "minFraud-API/" + getClass().getPackage().getImplementationVersion() + " Java/"
                + System.getProperty("java.version");
    }

    /**
     * Close any open connections and return resources to the system.
     */
    @Override
    public void close() throws IOException {
        httpClient.close();
    }

    @Override
    public String toString() {
        return "WebServiceClient{" + "host='" + host + '\'' + ", port=" + port + ", useHttps=" + useHttps
                + ", locales=" + locales + ", licenseKey='" + licenseKey + '\'' + ", userId=" + userId + ", mapper="
                + mapper + ", httpClient=" + httpClient + '}';
    }
}