org.microworld.mangopay.implementation.DefaultMangopayConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.microworld.mangopay.implementation.DefaultMangopayConnection.java

Source

/**
 * Copyright  2015 MicroWorld (contact@microworld.org)
 *
 * 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.
 */
package org.microworld.mangopay.implementation;

import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toMap;
import static org.microworld.mangopay.misc.Reflections.setFieldValue;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.time.Instant;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.stream.Stream;

import javax.net.ssl.HttpsURLConnection;

import org.microworld.mangopay.MangopayConnection;
import org.microworld.mangopay.entities.Amount;
import org.microworld.mangopay.entities.BankWirePayIn;
import org.microworld.mangopay.entities.BankWirePayOut;
import org.microworld.mangopay.entities.DirectCardPayIn;
import org.microworld.mangopay.entities.Error;
import org.microworld.mangopay.entities.ExecutionType;
import org.microworld.mangopay.entities.IncomeRange;
import org.microworld.mangopay.entities.LegalUser;
import org.microworld.mangopay.entities.NaturalUser;
import org.microworld.mangopay.entities.PayIn;
import org.microworld.mangopay.entities.PayInType;
import org.microworld.mangopay.entities.PayOut;
import org.microworld.mangopay.entities.PersonType;
import org.microworld.mangopay.entities.RateLimit;
import org.microworld.mangopay.entities.RateLimitInterval;
import org.microworld.mangopay.entities.Token;
import org.microworld.mangopay.entities.Transaction;
import org.microworld.mangopay.entities.User;
import org.microworld.mangopay.entities.WebCardPayIn;
import org.microworld.mangopay.entities.bankaccounts.BankAccount;
import org.microworld.mangopay.entities.bankaccounts.BankAccountType;
import org.microworld.mangopay.entities.bankaccounts.BritishBankAccount;
import org.microworld.mangopay.entities.bankaccounts.CanadianBankAccount;
import org.microworld.mangopay.entities.bankaccounts.IbanBankAccount;
import org.microworld.mangopay.entities.bankaccounts.OtherBankAccount;
import org.microworld.mangopay.entities.bankaccounts.UsaBankAccount;
import org.microworld.mangopay.exceptions.MangopayException;
import org.microworld.mangopay.exceptions.MangopayUnauthorizedException;
import org.microworld.mangopay.implementation.serialization.AmountAdapter;
import org.microworld.mangopay.implementation.serialization.IncomeRangeAdapter;
import org.microworld.mangopay.implementation.serialization.InstantAdapter;
import org.microworld.mangopay.implementation.serialization.LocalDateAdapter;
import org.microworld.mangopay.implementation.serialization.MangopayUnauthorizedExceptionDeserializer;
import org.microworld.mangopay.implementation.serialization.YearMonthAdapter;
import org.microworld.mangopay.misc.HttpMethod;
import org.microworld.mangopay.search.Filter;
import org.microworld.mangopay.search.Page;
import org.microworld.mangopay.search.ParameterHolder;
import org.microworld.mangopay.search.Sort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;

public class DefaultMangopayConnection implements MangopayConnection {
    private static final Logger LOG = LoggerFactory.getLogger(DefaultMangopayConnection.class);
    private static final int READ_TIMEOUT = 60000;
    private static final int CONNECT_TIMEOUT = 60000;
    private static final String API_VERSION = "v2.01";
    private final String host;
    private final String clientId;
    private final String encodedAuthenticationString;
    private final Map<RateLimitInterval, RateLimit> rateLimits;
    private final Gson gson;
    private Token token;

    public DefaultMangopayConnection(final String host, final String clientId, final String passphrase) {
        this.host = requireNonNull(host, "The host must not be null.");
        this.clientId = requireNonNull(clientId, "The clientId must not be null.");
        this.encodedAuthenticationString = encodeAuthenticationString(clientId, passphrase);
        this.rateLimits = new EnumMap<>(RateLimitInterval.class);
        final GsonBuilder builder = new GsonBuilder().disableHtmlEscaping();
        builder.registerTypeAdapter(MangopayUnauthorizedException.class,
                new MangopayUnauthorizedExceptionDeserializer());
        builder.registerTypeAdapter(Instant.class, new InstantAdapter());
        builder.registerTypeAdapter(Amount.class, new AmountAdapter());
        builder.registerTypeAdapter(LocalDate.class, new LocalDateAdapter());
        builder.registerTypeAdapter(YearMonth.class, new YearMonthAdapter());
        builder.registerTypeAdapter(IncomeRange.class, new IncomeRangeAdapter());
        this.gson = builder.create();
    }

    @Override
    public String getClientId() {
        return this.clientId;
    }

    @Override
    public RateLimit getRateLimit(final RateLimitInterval interval) {
        final RateLimit limit = rateLimits.get(interval);
        if (limit == null) {
            request(HttpMethod.GET, "/clients", new String[] {}, emptyMap(), null, false);
            return rateLimits.get(interval);
        } else {
            return limit;
        }
    }

    @Override
    public <T> List<T> queryForList(final Class<T> type, final HttpMethod method, final String path,
            final Filter filter, final Sort sort, final Page page, final String... pathParameters) {
        final JsonArray array = new JsonParser()
                .parse(request(method, path, pathParameters, toQuery(filter, sort, page), null, false))
                .getAsJsonArray();
        final List<T> result = new ArrayList<>(array.size());
        array.forEach(e -> result.add(convert(e, type)));
        return result;
    }

    @Override
    public <T> T queryForObject(final Class<T> type, final HttpMethod method, final String path, final Object data,
            final String... pathParameters) {
        return convert(new JsonParser().parse(request(method, path, pathParameters, emptyMap(), data, false)),
                type);
    }

    @Override
    public void query(final HttpMethod method, final String path, final Object data,
            final String... pathParameters) {
        request(method, path, pathParameters, emptyMap(), data, false);
    }

    private String request(final HttpMethod method, final String path, final String[] pathParameters,
            final Map<String, String> query, final Object data, final boolean isAuthorizationRequest) {
        try {
            final Map<String, String> headers = new HashMap<>();
            if (isAuthorizationRequest) {
                headers.put("Host", host);
                headers.put("Authorization", "Basic " + encodedAuthenticationString);
                headers.put("Content-Type", "application/x-www-form-urlencoded");
            } else {
                headers.put(Token.HEADER_NAME, getToken().getHeaderValue());
                headers.put("Content-Type", "application/json");
            }

            final URL url = createUrl(isAuthorizationRequest, path, pathParameters, query);
            final HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
            connection.setDoInput(true);
            connection.setDoOutput(true);
            connection.setUseCaches(false);
            connection.setConnectTimeout(CONNECT_TIMEOUT);
            connection.setReadTimeout(READ_TIMEOUT);
            connection.setRequestMethod(method.name());
            headers.forEach(connection::addRequestProperty);

            LOG.debug("Request: {} {}", connection.getRequestMethod(), url);
            if (data != null) {
                try (OutputStreamWriter output = new OutputStreamWriter(connection.getOutputStream(), UTF_8)) {
                    final String json = data instanceof String ? (String) data : toJson(data);
                    LOG.debug("Request data: {}", json);
                    output.write(json);
                    output.flush();
                }
            }
            final int responseCode = connection.getResponseCode();
            parseRateLimits(rateLimits, connection.getHeaderFields());

            LOG.debug("Response code: {}", responseCode);
            switch (responseCode) {
            case HttpURLConnection.HTTP_INTERNAL_ERROR:
                throw new RuntimeException("Internal Server Error");
            case HttpURLConnection.HTTP_UNAUTHORIZED:
                throw gson.fromJson(getContent(connection.getErrorStream()), MangopayUnauthorizedException.class);
            case HttpURLConnection.HTTP_OK:
            case HttpURLConnection.HTTP_NO_CONTENT:
                return getContent(connection.getInputStream());
            default:
                throw new MangopayException(gson.fromJson(getContent(connection.getErrorStream()), Error.class));
            }
        } catch (final IOException e) {
            throw new RuntimeException(e);
        }
    }

    private String toJson(final Object data) {
        final JsonObject jsonObject = gson.toJsonTree(data).getAsJsonObject();
        jsonObject.remove("Id");
        jsonObject.remove("CreationDate");
        return gson.toJson(jsonObject);
    }

    private Token getToken() {
        if (token == null || token.isExpired()) {
            final Map<String, String> data = new HashMap<>();
            data.put("grant_type", "client_credentials");
            token = gson.fromJson(request(HttpMethod.POST, "/oauth/token", new String[] {}, emptyMap(), data, true),
                    Token.class);
        }
        return token;
    }

    private Map<String, String> toQuery(final ParameterHolder... holders) {
        return Stream.of(holders).filter(Objects::nonNull).map(ParameterHolder::getParameters).map(Map::entrySet)
                .flatMap(Collection::stream).collect(toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    private URL createUrl(final boolean isAuthorizationRequest, final String path, final String[] pathParameters,
            final Map<String, String> query) throws MalformedURLException {
        final StringBuilder url = new StringBuilder("https://");
        url.append(host);
        url.append("/").append(API_VERSION);
        if (!isAuthorizationRequest) {
            url.append("/").append(clientId);
        }

        String parsedPath = path;
        for (int i = 0; i < pathParameters.length; i++) {
            parsedPath = parsedPath.replace("{" + i + "}", pathParameters[i]);
        }
        url.append(parsedPath);

        final String queryString = query.entrySet().stream().map(DefaultMangopayConnection::toNameValue)
                .collect(joining("&"));
        if (!query.isEmpty()) {
            url.append("?").append(queryString);
        }
        return new URL(url.toString());
    }

    private static String toNameValue(final Entry<String, String> entry) {
        try {
            return URLEncoder.encode(entry.getKey(), ISO_8859_1.name()) + "="
                    + URLEncoder.encode(entry.getValue(), ISO_8859_1.name());
        } catch (final UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    private String getContent(final InputStream inputStream) throws IOException {
        final StringBuilder content = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, UTF_8))) {
            String line;
            while ((line = reader.readLine()) != null) {
                content.append(line).append('\n');
            }
        }
        LOG.debug("Response content: {}", content);
        return content.toString();
    }

    @SuppressWarnings("unchecked")
    private <T> T convert(final JsonElement element, final Class<T> type) {
        final JsonObject object = element.getAsJsonObject();
        if (type.equals(User.class)) {
            return (T) convertUser(object);
        } else if (type.equals(BankAccount.class)) {
            return (T) convertBankAccount(object);
        } else if (type.equals(PayOut.class)) {
            return (T) convert(object, BankWirePayOut.class);
        } else if (type.equals(PayIn.class)) {
            return (T) convertPayIn(object);
        } else if (type.equals(BankWirePayIn.class)) {
            return (T) convertBankWirePayIn(object);
        } else if (type.equals(Transaction.class)) {
            return (T) convertTransaction(object);
        } else {
            return gson.fromJson(object, type);
        }
    }

    private Transaction convertTransaction(final JsonObject object) {
        // See https://github.com/Mangopay/mangopay/issues/13 about why this code is not used.
        // switch (TransactionType.valueOf(object.get("Type").getAsString())) {
        // case PAYIN:
        // return convert(object, PayIn.class);
        // case PAYOUT:
        // return convert(object, PayOut.class);
        // case TRANSFER:
        // default:
        // return convert(object, Transfer.class);
        // }
        return gson.fromJson(object, Transaction.class);
    }

    private PayIn convertPayIn(final JsonObject object) {
        switch (PayInType.valueOf(object.get("PaymentType").getAsString())) {
        case BANK_WIRE:
            return convertBankWirePayIn(object);
        case CARD:
        default:
            return convertCardPayIn(object);
        }
    }

    private PayIn convertCardPayIn(final JsonObject object) {
        switch (ExecutionType.valueOf(object.get("ExecutionType").getAsString())) {
        case DIRECT:
            return gson.fromJson(object, DirectCardPayIn.class);
        case WEB:
        default:
            return gson.fromJson(object, WebCardPayIn.class);
        }
    }

    private PayIn convertBankWirePayIn(final JsonObject object) {
        final JsonObject bankAccount = object.remove("BankAccount").getAsJsonObject();
        final BankWirePayIn payIn = gson.fromJson(object, BankWirePayIn.class);
        setFieldValue(BankWirePayIn.class, "bankAccount", payIn, convertBankAccount(bankAccount));
        return payIn;
    }

    private BankAccount convertBankAccount(final JsonObject object) {
        switch (BankAccountType.valueOf(object.get("Type").getAsString())) {
        case IBAN:
            return gson.fromJson(object, IbanBankAccount.class);
        case GB:
            return gson.fromJson(object, BritishBankAccount.class);
        case US:
            return gson.fromJson(object, UsaBankAccount.class);
        case CA:
            return gson.fromJson(object, CanadianBankAccount.class);
        case OTHER:
        default:
            return gson.fromJson(object, OtherBankAccount.class);
        }
    }

    private User convertUser(final JsonObject object) {
        switch (PersonType.valueOf(object.get("PersonType").getAsString())) {
        case LEGAL:
            return gson.fromJson(object, LegalUser.class);
        case NATURAL:
        default:
            return gson.fromJson(object, NaturalUser.class);
        }
    }

    static String encodeAuthenticationString(final String clientId, final String passphrase) {
        return Base64.getEncoder()
                .encodeToString((clientId + ":" + requireNonNull(passphrase, "The passphrase must not be null."))
                        .getBytes(ISO_8859_1));
    }

    static void parseRateLimits(final Map<RateLimitInterval, RateLimit> limits,
            final Map<String, List<String>> headers) {
        final int numberOfIntervals = RateLimitInterval.values().length;
        final List<String> madeList = headers.getOrDefault("X-RateLimit", emptyList());
        final List<String> remainingList = headers.getOrDefault("X-RateLimit-Remaining", emptyList());
        final List<String> resetList = headers.getOrDefault("X-RateLimit-Reset", emptyList());
        if (resetList.size() >= numberOfIntervals && remainingList.size() >= numberOfIntervals
                && madeList.size() >= numberOfIntervals) {
            for (final RateLimitInterval interval : RateLimitInterval.values()) {
                final int callsMade = Integer.parseInt(madeList.get(numberOfIntervals - 1 - interval.ordinal()));
                final int callsRemaining = Integer
                        .parseInt(remainingList.get(numberOfIntervals - 1 - interval.ordinal()));
                final Instant reset = Instant
                        .ofEpochSecond(Long.parseLong(resetList.get(numberOfIntervals - 1 - interval.ordinal())));
                limits.put(interval, new RateLimit(interval, callsMade, callsRemaining, reset));
            }
        }
    }
}