ee.ria.xroad.common.util.AbstractHttpSender.java Source code

Java tutorial

Introduction

Here is the source code for ee.ria.xroad.common.util.AbstractHttpSender.java

Source

/**
 * The MIT License
 * Copyright (c) 2015 Estonian Information System Authority (RIA), Population Register Centre (VRK)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package ee.ria.xroad.common.util;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import lombok.extern.slf4j.Slf4j;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.EofSensorInputStream;
import org.apache.http.conn.EofSensorWatcher;
import org.apache.http.entity.ContentType;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.entity.StringEntity;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.eclipse.jetty.http.HttpStatus;

import ee.ria.xroad.common.CodedException;
import ee.ria.xroad.common.SystemProperties;

import static ee.ria.xroad.common.ErrorCodes.*;

/**
 * Base class for a closeable HTTP sender.
 */
@Slf4j
public abstract class AbstractHttpSender implements Closeable {
    public static final int CHUNKED_LENGTH = -1;

    private static final int DEFAULT_CONNECTION_TIMEOUT = 30000; // default 30 sec

    private static final int DEFAULT_SOCKET_TIMEOUT = 0; // default infinite

    private final Map<String, String> additionalHeaders = new HashMap<>();

    private String responseContentType;
    private InputStream responseContent;
    private Map<String, String> responseHeaders;

    protected final HttpContext context = new BasicHttpContext();

    protected HttpRequestBase request;
    protected HttpEntity responseEntity;

    protected int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
    protected int socketTimeout = DEFAULT_SOCKET_TIMEOUT;

    /**
     * Sets the connection timeout in milliseconds.
     * @param newTimeout the new timeout value
     */
    public void setConnectionTimeout(int newTimeout) {
        this.connectionTimeout = newTimeout;
    }

    /**
     * Sets the socket timeout in milliseconds.
     * @param newTimeout the new timeout value
     */
    public void setSocketTimeout(int newTimeout) {
        this.socketTimeout = newTimeout;
    }

    /**
     * Sets the value of an attribute.
     * @param name attribute name
     * @param value attribute value
     */
    public void setAttribute(String name, Object value) {
        context.setAttribute(name, value);
    }

    /**
     * Adds an additional header to the request.
     * @param name header name
     * @param value header value
     */
    public void addHeader(String name, String value) {
        additionalHeaders.put(name, value);
    }

    /**
     * @return the response content type.
     */
    public String getResponseContentType() {
        return responseContentType;
    }

    /**
     * @return the response content input stream.
     */
    public InputStream getResponseContent() {
        return responseContent;
    }

    /**
     * @return all response headers returned in the response.
     */
    public Map<String, String> getResponseHeaders() {
        return responseHeaders;
    }

    protected void handleResponse(HttpResponse response) throws Exception {
        log.trace("handleResponse()");

        checkResponseStatus(response);

        responseHeaders = getResponseHeaders(response);
        this.responseEntity = getResponseEntity(response);
        responseContentType = getResponseContentType(responseEntity, this.request instanceof HttpGet);

        // Wrap the response input stream in order to catch EOF errors.
        responseContent = new EofSensorInputStream(responseEntity.getContent(), new ResponseStreamWatcher());
    }

    /**
     * Perform a GET request to the given address.
     * @param address URI of the address for the GET request
     * @throws Exception if any errors occur
     */
    public abstract void doGet(URI address) throws Exception;

    /**
     * Sends data using POST method to the given address.
     * @param address the address to send
     * @param content the content to send
     * @param contentType the content type of the input data
     * @throws Exception if an error occurs
     */
    public abstract void doPost(URI address, String content, String contentType) throws Exception;

    /**
     * Sends data using POST method to the given address.
     * @param address the address to send
     * @param content the content to send
     * @param contentLength length of the content in bytes
     * @param contentType the content type of the input data
     * @throws Exception if an error occurs
     */
    public abstract void doPost(URI address, InputStream content, long contentLength, String contentType)
            throws Exception;

    @Override
    public void close() {
        if (!SystemProperties.isEnableClientProxyPooledConnectionReuse()) {
            if (request != null) {
                request.releaseConnection();
            }
        } else {
            try {
                // consume and close the stream, returning the connection as reusable into the pool
                EntityUtils.consume(responseEntity);
            } catch (IOException e) {
                // reading/closing the stream failed for whatever reason, the broken connection should be cleaned up by
                // a pool monitor. Keep the contract set by releaseConnection and don't throw checked exceptions.
                // Nothing really to be done here anyway.
                log.warn("Closing response entity nicely failed", e);
            }
        }
    }

    protected void addAdditionalHeaders() {
        for (Entry<String, String> header : additionalHeaders.entrySet()) {
            request.addHeader(header.getKey(), header.getValue());
        }
    }

    protected RequestConfig getRequestConfig() {
        RequestConfig.Builder rb = RequestConfig.custom();
        rb.setConnectTimeout(connectionTimeout);
        rb.setConnectionRequestTimeout(connectionTimeout);
        rb.setSocketTimeout(socketTimeout);

        return rb.build();
    }

    protected static InputStreamEntity createInputStreamEntity(InputStream content, long contentLength,
            String contentType) {
        InputStreamEntity entity = new InputStreamEntity(content, contentLength);

        if (contentLength < 0) {
            entity.setChunked(true); // Just in case
        }

        entity.setContentType(contentType);

        return entity;
    }

    protected static StringEntity createStringEntity(String content, String contentType) {
        return new StringEntity(content, ContentType.create(contentType, MimeUtils.UTF8));
    }

    protected void checkResponseStatus(HttpResponse response) {
        switch (response.getStatusLine().getStatusCode()) {
        case HttpStatus.OK_200: // FALL THROUGH
            // R1126 An INSTANCE MUST return a "500 Internal Server Error"
            // HTTP status code if the response envelope is a Fault.
        case HttpStatus.INTERNAL_SERVER_ERROR_500:
            return;
        default:
            throw new CodedException(X_HTTP_ERROR, "Server responded with error %s: %s",
                    response.getStatusLine().getStatusCode(), response.getStatusLine().getReasonPhrase());
        }
    }

    protected static Map<String, String> getResponseHeaders(HttpResponse response) {
        Map<String, String> headers = new HashMap<>();

        for (Header header : response.getAllHeaders()) {
            headers.put(header.getName(), header.getValue());
        }

        return headers;
    }

    protected static HttpEntity getResponseEntity(HttpResponse response) {
        HttpEntity entity = response.getEntity();

        if (entity == null) {
            throw new CodedException(X_HTTP_ERROR, "Could not get content from response");
        }

        return entity;
    }

    protected String getResponseContentType(HttpEntity entity, boolean isGetRequest) {
        Header contentType = entity.getContentType();

        if (contentType == null) {
            if (isGetRequest) {
                return null;
            }

            throw new CodedException(X_INVALID_CONTENT_TYPE, "Could not get content type from response");
        }

        return contentType.getValue();
    }

    protected class ResponseStreamWatcher implements EofSensorWatcher {
        @Override
        public boolean eofDetected(InputStream wrapped) throws IOException {
            return true;
        }

        @Override
        public boolean streamClosed(InputStream wrapped) throws IOException {
            log.warn("Stream was closed before EOF was detected");

            return true;
        }

        @Override
        public boolean streamAbort(InputStream wrapped) throws IOException {
            throw new CodedException(X_IO_ERROR, "Stream was aborted");
        }
    }
}