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

com.microsoft.windowsazure.services.core.utils.pipeline.HttpURLConnectionClientHandler Maven / Gradle / Ivy

The newest version!
/**
 * Copyright Microsoft Corporation
 * 
 * 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.microsoft.windowsazure.services.core.utils.pipeline;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.ProtocolException;
import java.util.List;
import java.util.Map;

import javax.ws.rs.core.MultivaluedMap;

import com.microsoft.windowsazure.services.core.utils.CommaStringBuilder;
import com.sun.jersey.api.client.ClientHandlerException;
import com.sun.jersey.api.client.ClientRequest;
import com.sun.jersey.api.client.ClientResponse;
import com.sun.jersey.api.client.CommittingOutputStream;
import com.sun.jersey.api.client.TerminatingClientHandler;
import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.core.header.InBoundHeaders;

public class HttpURLConnectionClientHandler extends TerminatingClientHandler {

    private final int connectionTimeoutMillis;
    private final int readTimeoutMillis;

    public HttpURLConnectionClientHandler(ClientConfig clientConfig) {
        connectionTimeoutMillis = readTimeoutFromConfig(clientConfig, ClientConfig.PROPERTY_CONNECT_TIMEOUT);
        readTimeoutMillis = readTimeoutFromConfig(clientConfig, ClientConfig.PROPERTY_READ_TIMEOUT);
    }

    private static int readTimeoutFromConfig(ClientConfig config, String propertyName) {
        Integer property = (Integer) config.getProperty(propertyName);
        if (property != null) {
            return property.intValue();
        }
        throw new IllegalArgumentException(propertyName);
    }

    /**
     * Empty "no-op" listener if none registered
     */
    private static final EntityStreamingListener EMPTY_STREAMING_LISTENER = new EntityStreamingListener() {
        @Override
        public void onBeforeStreamingEntity(ClientRequest clientRequest) {
        }
    };

    /**
     * OutputStream used for buffering entity body when "Content-Length" is not known in advance.
     */
    private final class BufferingOutputStream extends OutputStream {
        private final ByteArrayOutputStream outputStream;
        private final HttpURLConnection urlConnection;
        private final ClientRequest clientRequest;
        private final EntityStreamingListener entityStreamingListener;
        private boolean closed;

        private BufferingOutputStream(HttpURLConnection urlConnection, ClientRequest clientRequest,
                EntityStreamingListener entityStreamingListener) {
            this.outputStream = new ByteArrayOutputStream();
            this.urlConnection = urlConnection;
            this.clientRequest = clientRequest;
            this.entityStreamingListener = entityStreamingListener;
        }

        @Override
        public void close() throws IOException {
            outputStream.close();

            if (!closed) {
                closed = true;

                // Give the listener a last change to modify headers now that the content length is known
                setContentLengthHeader(clientRequest, outputStream.size());
                entityStreamingListener.onBeforeStreamingEntity(clientRequest);

                // Write headers, then entity to the http connection.
                setURLConnectionHeaders(clientRequest.getHeaders(), urlConnection);

                // Since we buffered the entity and we know the content size, we might as well
                // use the "fixed length" streaming mode of HttpURLConnection to stream
                // the buffer directly.
                urlConnection.setFixedLengthStreamingMode(outputStream.size());
                OutputStream httpOutputStream = urlConnection.getOutputStream();
                outputStream.writeTo(httpOutputStream);
                httpOutputStream.flush();
                httpOutputStream.close();
            }
        }

        @Override
        public void flush() throws IOException {
            outputStream.flush();
        }

        @Override
        public void write(byte[] b, int off, int len) {
            outputStream.write(b, off, len);
        }

        @Override
        public void write(byte[] b) throws IOException {
            outputStream.write(b);
        }

        @Override
        public void write(int b) {
            outputStream.write(b);
        }
    }

    /**
     * OutputStream used for directly streaming entity to url connection stream. Headers are written just before sending
     * the first bytes to the output stream.
     */
    private final class StreamingOutputStream extends CommittingOutputStream {
        private final HttpURLConnection urlConnection;
        private final ClientRequest clientRequest;

        private StreamingOutputStream(HttpURLConnection urlConnection, ClientRequest clientRequest) {
            this.urlConnection = urlConnection;
            this.clientRequest = clientRequest;
        }

        @Override
        protected OutputStream getOutputStream() throws IOException {
            return urlConnection.getOutputStream();
        }

        @Override
        public void commit() throws IOException {
            setURLConnectionHeaders(clientRequest.getHeaders(), urlConnection);
        }
    }

    /**
     * Simple response implementation around an HttpURLConnection response
     */
    private final class URLConnectionResponse extends ClientResponse {
        private final String method;
        private final HttpURLConnection urlConnection;

        URLConnectionResponse(int status, InBoundHeaders headers, InputStream entity, String method,
                HttpURLConnection urlConnection) {
            super(status, headers, entity, getMessageBodyWorkers());
            this.method = method;
            this.urlConnection = urlConnection;
        }

        @Override
        public boolean hasEntity() {
            if (method.equals("HEAD") || getEntityInputStream() == null)
                return false;

            // Length "-1" means "unknown"
            int length = urlConnection.getContentLength();
            return length > 0 || length == -1;
        }

        @Override
        public String toString() {
            return urlConnection.getRequestMethod() + " " + urlConnection.getURL() + " returned a response status of "
                    + this.getStatus() + " " + this.getClientResponseStatus();
        }
    }

    @Override
    public ClientResponse handle(final ClientRequest ro) throws ClientHandlerException {
        try {
            return doHandle(ro);
        }
        catch (Exception e) {
            throw new ClientHandlerException(e);
        }
    }

    private ClientResponse doHandle(final ClientRequest clientRequest) throws IOException, MalformedURLException,
            ProtocolException {
        final HttpURLConnection urlConnection = (HttpURLConnection) clientRequest.getURI().toURL().openConnection();
        urlConnection.setReadTimeout(readTimeoutMillis);
        urlConnection.setConnectTimeout(connectionTimeoutMillis);

        final EntityStreamingListener entityStreamingListener = getEntityStreamingListener(clientRequest);

        urlConnection.setRequestMethod(clientRequest.getMethod());

        // Write the request headers
        setURLConnectionHeaders(clientRequest.getHeaders(), urlConnection);

        // Write the entity (if any)
        Object entity = clientRequest.getEntity();

        // If no entity and "PUT method, force an empty entity to force the underlying
        // connection to set the "Content-Length" header to 0.
        // This is needed because some web servers require a "Content-Length" field for
        // all PUT method calls (unless chunked encoding is used).
        if (entity == null && "PUT".equals(clientRequest.getMethod())) {
            entity = new byte[0];
            clientRequest.setEntity(entity);
        }

        // Send headers and entity on the wire
        if (entity != null) {
            urlConnection.setDoOutput(true);

            writeRequestEntity(clientRequest, new RequestEntityWriterListener() {
                private boolean inStreamingMode;

                @Override
                public void onRequestEntitySize(long size) {
                    if (size != -1 && size < Integer.MAX_VALUE) {
                        inStreamingMode = true;
                        setContentLengthHeader(clientRequest, (int) size);
                        entityStreamingListener.onBeforeStreamingEntity(clientRequest);

                        urlConnection.setFixedLengthStreamingMode((int) size);
                    }
                    else {
                        Integer chunkedEncodingSize = (Integer) clientRequest.getProperties().get(
                                ClientConfig.PROPERTY_CHUNKED_ENCODING_SIZE);
                        if (chunkedEncodingSize != null) {
                            inStreamingMode = true;
                            entityStreamingListener.onBeforeStreamingEntity(clientRequest);

                            urlConnection.setChunkedStreamingMode(chunkedEncodingSize);
                        }
                    }
                }

                @Override
                public OutputStream onGetOutputStream() throws IOException {
                    if (inStreamingMode)
                        return new StreamingOutputStream(urlConnection, clientRequest);
                    else
                        return new BufferingOutputStream(urlConnection, clientRequest, entityStreamingListener);
                }
            });
        }
        else {
            entityStreamingListener.onBeforeStreamingEntity(clientRequest);
            setURLConnectionHeaders(clientRequest.getHeaders(), urlConnection);
        }

        // Return the in-bound response
        return new URLConnectionResponse(urlConnection.getResponseCode(), getInBoundHeaders(urlConnection),
                getInputStream(urlConnection), clientRequest.getMethod(), urlConnection);
    }

    private EntityStreamingListener getEntityStreamingListener(final ClientRequest clientRequest) {
        EntityStreamingListener result = (EntityStreamingListener) clientRequest.getProperties().get(
                EntityStreamingListener.class.getName());

        if (result != null)
            return result;

        return EMPTY_STREAMING_LISTENER;
    }

    private void setContentLengthHeader(ClientRequest clientRequest, int size) {
        // Skip if already set
        if (clientRequest.getHeaders().getFirst("Content-Length") != null)
            return;

        // Skip if size is unknown
        if (size < 0)
            return;

        clientRequest.getHeaders().putSingle("Content-Length", size);
    }

    private void setURLConnectionHeaders(MultivaluedMap headers, HttpURLConnection urlConnection) {
        for (Map.Entry> e : headers.entrySet()) {
            List vs = e.getValue();
            if (vs.size() == 1) {
                urlConnection.setRequestProperty(e.getKey(), ClientRequest.getHeaderValue(vs.get(0)));
            }
            else {
                CommaStringBuilder sb = new CommaStringBuilder();
                for (Object v : e.getValue()) {
                    sb.add(ClientRequest.getHeaderValue(v));
                }
                urlConnection.setRequestProperty(e.getKey(), sb.toString());
            }
        }
    }

    private InBoundHeaders getInBoundHeaders(HttpURLConnection urlConnection) {
        InBoundHeaders headers = new InBoundHeaders();
        for (Map.Entry> e : urlConnection.getHeaderFields().entrySet()) {
            if (e.getKey() != null)
                headers.put(e.getKey(), e.getValue());
        }
        return headers;
    }

    private InputStream getInputStream(HttpURLConnection urlConnection) throws IOException {
        if (urlConnection.getResponseCode() < 300) {
            return urlConnection.getInputStream();
        }
        else {
            InputStream ein = urlConnection.getErrorStream();
            return (ein != null) ? ein : new ByteArrayInputStream(new byte[0]);
        }
    }
}