All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.klarna.rest.http_transport.HttpUrlConnectionTransport Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2018 Klarna AB
 *
 * 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 com.klarna.rest.http_transport;

import com.klarna.rest.model.ApiException;
import com.klarna.rest.model.ApiResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.net.*;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import javax.ws.rs.core.MediaType;

/**
 * HttpURLConnection implementation of Transpoert interface.
 * Used to send HTTP requests to API server.
 */
public class HttpUrlConnectionTransport implements HttpTransport {
    /**
     * Default HTTP request timeout.
     */
    private static final int DEFAULT_TIMEOUT = 30000;

    /**
     * Default request Media-Type.
     */
    private static final String DEFAULT_MEDIA_TYPE = MediaType.APPLICATION_JSON;

    /**
     * Logger instance.
     */
    private static final Logger log = LoggerFactory.getLogger(HttpUrlConnectionTransport.class);

    /**
     * Base API server URL.
     */
    protected URI baseUri;

    /**
     * Merchant ID.
     */
    protected String merchantId;

    /**
     * Merchant shared secret key.
     */
    protected String sharedSecret;

    /**
     * HTTP UserAgent.
     */
    protected String userAgent;

    /**
     * HTTP request timeout.
     */
    protected int timeout = DEFAULT_TIMEOUT;

    /**
     * HttpUrlConnection Proxy settings.
     * @see Proxy
     */
    protected Proxy proxy;

    /**
     * Proxy authenticator.
     * @see Authenticator
     */
    protected Authenticator proxyAuth;

    /**
     * Sets up required params for Klarna API.
     *
     * @param merchantId Merchant ID/Username (UID)
     * @param sharedSecret Merchant shared secret/password
     * @param baseUri Klarna API URLs.
     *
     * @see HttpTransport Base URLs
     */
    public HttpUrlConnectionTransport(final String merchantId,
                                      final String sharedSecret,
                                      final URI baseUri) {
        this.baseUri = baseUri;
        this.merchantId = merchantId;
        this.sharedSecret = sharedSecret;
        this.userAgent = HttpTransport.USER_AGENT;

        HttpUrlConnectionTransport.allowMethods("PATCH"); // Workaround for PATCH method
    }

    /**
     * Sends HTTP GET request to specified path.
     *
     * @param path URL path
     * @param headers HTTP request headers
     * @return Processed response
     * @throws ApiException if API server returned non-20x HTTP CODE and response contains
     *                      a Error
     * @throws IOException if an error occurred when connecting to the server or when parsing a response.
     */
    public ApiResponse get(final String path, Map headers) throws
            ApiException, IOException {
        HttpURLConnection conn = this.buildConnection(path, headers);
        conn.setRequestMethod("GET");

        return this.makeRequest(conn, null);
    }

    /**
     * Sends HTTP POST request to specified path.
     *
     * @param path URL path
     * @param data Data to be sent to API server in a payload
     * @param headers HTTP request headers
     * @return Processed response
     * @throws ApiException if API server returned non-20x HTTP CODE and response contains
     *                      a Error
     * @throws IOException if an error occurred when connecting to the server or when parsing a response.
     */
    public ApiResponse post(final String path, final byte[] data, Map headers) throws
            ApiException, IOException {
        HttpURLConnection conn = this.buildConnection(path, headers);
        conn.setRequestMethod("POST");

        return this.makeRequest(conn, data);
    }

    /**
     * Sends HTTP PUT request to specified path.
     *
     * @param path URL path
     * @param data Data to be sent to API server in a payload
     * @param headers HTTP request headers
     * @return Processed response
     * @throws ApiException if API server returned non-20x HTTP CODE and response contains
     *                      a Error
     * @throws IOException if an error occurred when connecting to the server or when parsing a response.
     */
    public ApiResponse put(final String path, final byte[] data, Map headers) throws
            ApiException, IOException {
        HttpURLConnection conn = this.buildConnection(path, headers);
        conn.setRequestMethod("PUT");

        return this.makeRequest(conn, data);
    }

    /**
     * Sends HTTP PATCH request to specified path.
     *
     * @param path URL path
     * @param data Data to be sent to API server in a payload
     * @param headers HTTP request headers
     * @return Processed response
     * @throws ApiException if API server returned non-20x HTTP CODE and response contains
     *                      a Error
     * @throws IOException if an error occurred when connecting to the server or when parsing a response.
     */
    public ApiResponse patch(final String path, final byte[] data, Map headers) throws
            ApiException, IOException {
        HttpURLConnection conn = this.buildConnection(path, headers);
        conn.setRequestMethod("PATCH");

        return this.makeRequest(conn, data);
    }

    /**
     * Sends HTTP DELETE request to specified path.
     *
     * @deprecated As of 3.1.0, adding `data` params to delete in order to cover the InstantShoppingApi
     *              Use {@link #delete(String, byte[], Map)} instead.
     *
     * @param path URL path
     * @param headers HTTP request headers
     * @return Processed response
     * @throws ApiException if API server returned non-20x HTTP CODE and response contains
     *                      a Error
     * @throws IOException if an error occurred when connecting to the server or when parsing a response.
     */
    @Deprecated
    public ApiResponse delete(final String path, Map headers) throws
            ApiException, IOException {
        HttpURLConnection conn = this.buildConnection(path, headers);
        conn.setRequestMethod("DELETE");

        return this.makeRequest(conn, null);
    }

    /**
     * Sends HTTP DELETE request to specified path.
     *
     * @param path URL path
     * @param headers HTTP request headers
     * @return Processed response
     * @throws ApiException if API server returned non-20x HTTP CODE and response contains
     *                      a Error
     * @throws IOException if an error occurred when connecting to the server or when parsing a response.
     */
    public ApiResponse delete(final String path, final byte[] data, Map headers) throws
            ApiException, IOException {
        HttpURLConnection conn = this.buildConnection(path, headers);
        conn.setRequestMethod("DELETE");

        return this.makeRequest(conn, data);
    }

    /**
     * Gets current UserAgent.
     *
     * @return UserAgent
     */
    public String getUserAgent() {
        return this.userAgent;
    }

    /**
     * Sets new UserAgent. The UserAgent will be added as 'User-Agent' header to the HTTP request.
     *
     * @param userAgent new UserAgent
     * @return self
     */
    public HttpUrlConnectionTransport setUserAgent(String userAgent) {
        this.userAgent = userAgent;

        return this;
    }

    /**
     * Gets current Timeout limit (in seconds) for an HTTP request.
     *
     * @return Timeout
     */
    public int getTimeout() {
        return this.timeout;
    }

    /**
     * Sets current Timeout limit (in seconds) for an HTTP request.
     *
     * @param timeout Timeout in milliseconds
     * @return self
     */
    public HttpUrlConnectionTransport setTimeout(int timeout) {
        this.timeout = timeout;

        return this;
    }

    /**
     * Sets new Proxy settings
     *
     * @param proxy Proxy
     */
    public void setProxy(Proxy proxy) {
        this.proxy = proxy;
    }

    /**
     * Sets new Proxy settings
     *
     * @param scheme Proxy scheme (http, https)
     * @param host Proxy host
     * @param port Proxy port
     */
    public void setProxy(final Proxy.Type scheme, final String host, final int port) {
        this.proxy = new Proxy(scheme, new InetSocketAddress(host, port));
    }

    /**
     * Sets new Proxy settings
     *
     * @param scheme Proxy scheme (http, https)
     * @param host Proxy host
     * @param port Proxy port
     * @param username Auth: Proxy user name
     * @param password Auth: Proxy password
     */
    public void setProxy(final Proxy.Type scheme, final String host, final int port, final String username, final String password) {
        this.setProxy(scheme, host, port);
        this.proxyAuth = new Authenticator() {
            public PasswordAuthentication getPasswordAuthentication() {
                return (new PasswordAuthentication(username,
                        password.toCharArray()));
            }
        };
        Authenticator.setDefault(this.proxyAuth);
    }

    protected HttpURLConnection buildConnection(String path, Map headers) throws IOException {
        URL url = this.buildPath(path);

        HttpURLConnection conn;
        if (this.proxy != null) {
            conn = (HttpURLConnection) url.openConnection(this.proxy);
        } else {
            conn = (HttpURLConnection) url.openConnection();
        }

        conn.setRequestProperty("Content-Type", DEFAULT_MEDIA_TYPE);
        conn.setRequestProperty("User-Agent", this.userAgent);
        conn.setConnectTimeout(this.timeout);
        conn.setReadTimeout(this.timeout);

        this.authorize(conn);

        if (headers != null) {
            for (String key : headers.keySet()) {
                conn.setRequestProperty(key, headers.get(key));
            }
        }

        return conn;
    }

    protected void authorize(HttpURLConnection conn) throws IOException {
        this.setBase64Auth(conn, this.merchantId, this.sharedSecret);
    }

    protected ApiResponse makeRequest(HttpURLConnection conn, byte[] payout) throws IOException {
        if (log.isDebugEnabled()) {
            log.debug("DEBUG MODE: Request\n"
                    + ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n"
                    + conn.getRequestMethod() + ": " + conn.getURL() + "\n"
                    + "Headers: " + conn.getRequestProperties() + "\n"
                    + "Payout: " + (payout == null ? "null" : new String(payout)) + "\n");
        }

        if (payout != null) {
            conn.setDoOutput(true);

            try (OutputStream os = conn.getOutputStream()) {
                os.write(payout);
            }
        }

        ApiResponse response = new ApiResponse();

        response.setStatus(conn.getResponseCode());
        response.setHeaders(conn.getHeaderFields());

        try (InputStream is = response.isSuccessful() ? conn.getInputStream() : conn.getErrorStream()) {
            if (is != null) {
                try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {
                    int bytes;
                    while ((bytes = is.read()) != -1) {
                        os.write(bytes);
                    }

                    response.setBody(os.toByteArray());
                }
            }
        }

        if (log.isDebugEnabled()) {
            log.debug("DEBUG MODE: Response\n"
                    + "<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<\n"
                    + "Headers: " + conn.getHeaderFields() + "\n"
                    + "Body: " + (response.getBody() == null ? "null" : new String(response.getBody())) + "\n");
        }

        return response;
    }

    private URL buildPath(String path) throws MalformedURLException {
        URI uri = this.baseUri;
        String newPath = uri.getPath() + path;
        uri = uri.resolve(newPath);
        return uri.toURL();
    }

    private void setBase64Auth(HttpURLConnection conn, String username, String password) throws IOException {
        byte[] message = (username + ":" + password).getBytes(StandardCharsets.UTF_8);
        String encoded = javax.xml.bind.DatatypeConverter.printBase64Binary(message);

        conn.setRequestProperty("Authorization", "Basic " + encoded);
    }

    /**
     * Workaround: Extends HttpURLConnection with PATCH method. This method does not exist in the "Allowed HTTP methods"
     * but still can be sent.
     *
     * @param methods HTTP methods to be allowed by HttpURLConnection
     */
    private static void allowMethods(String... methods) {
        try {
            Field methodsField = HttpURLConnection.class.getDeclaredField("methods");

            Field modifiersField = Field.class.getDeclaredField("modifiers");
            modifiersField.setAccessible(true);
            modifiersField.setInt(methodsField, methodsField.getModifiers() & ~Modifier.FINAL);

            methodsField.setAccessible(true);

            String[] oldMethods = (String[]) methodsField.get(null);
            Set methodsSet = new LinkedHashSet<>(Arrays.asList(oldMethods));
            methodsSet.addAll(Arrays.asList(methods));
            String[] newMethods = methodsSet.toArray(new String[0]);

            methodsField.set(null/*static field*/, newMethods);
        } catch (NoSuchFieldException | IllegalAccessException e) {
            log.error(e.getMessage(), e);
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy