org.jclouds.http.internal.JavaUrlHttpCommandExecutorService.java Source code

Java tutorial

Introduction

Here is the source code for org.jclouds.http.internal.JavaUrlHttpCommandExecutorService.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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.jclouds.http.internal;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Throwables.propagate;
import static com.google.common.io.ByteStreams.toByteArray;
import static com.google.common.io.Closeables.closeQuietly;
import static com.google.common.net.HttpHeaders.CONTENT_LENGTH;
import static com.google.common.net.HttpHeaders.HOST;
import static com.google.common.net.HttpHeaders.USER_AGENT;
import static org.jclouds.http.HttpUtils.filterOutContentHeaders;
import static org.jclouds.io.Payloads.newInputStreamPayload;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.Proxy;
import java.net.URI;
import java.net.URL;
import java.util.List;
import java.util.Map;

import javax.inject.Named;
import javax.inject.Singleton;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;

import org.jclouds.Constants;
import org.jclouds.JcloudsVersion;
import org.jclouds.http.HttpCommandExecutorService;
import org.jclouds.http.HttpRequest;
import org.jclouds.http.HttpResponse;
import org.jclouds.http.HttpUtils;
import org.jclouds.http.IOExceptionRetryHandler;
import org.jclouds.http.handlers.DelegatingErrorHandler;
import org.jclouds.http.handlers.DelegatingRetryHandler;
import org.jclouds.io.ContentMetadataCodec;
import org.jclouds.io.MutableContentMetadata;
import org.jclouds.io.Payload;

import com.google.common.base.Function;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableMultimap.Builder;
import com.google.common.io.CountingOutputStream;
import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.inject.Inject;

/**
 * Basic implementation of a {@link HttpCommandExecutorService}.
 * 
 * @author Adrian Cole
 */
@Singleton
public class JavaUrlHttpCommandExecutorService extends BaseHttpCommandExecutorService<HttpURLConnection> {

    public static final String DEFAULT_USER_AGENT = String.format("jclouds/%s java/%s", JcloudsVersion.get(),
            System.getProperty("java.version"));

    private final Supplier<SSLContext> untrustedSSLContextProvider;
    private final Function<URI, Proxy> proxyForURI;
    private final HostnameVerifier verifier;
    private final Field methodField;
    @Inject(optional = true)
    Supplier<SSLContext> sslContextSupplier;

    @Inject
    public JavaUrlHttpCommandExecutorService(HttpUtils utils, ContentMetadataCodec contentMetadataCodec,
            @Named(Constants.PROPERTY_IO_WORKER_THREADS) ListeningExecutorService ioExecutor,
            DelegatingRetryHandler retryHandler, IOExceptionRetryHandler ioRetryHandler,
            DelegatingErrorHandler errorHandler, HttpWire wire, @Named("untrusted") HostnameVerifier verifier,
            @Named("untrusted") Supplier<SSLContext> untrustedSSLContextProvider, Function<URI, Proxy> proxyForURI)
            throws SecurityException, NoSuchFieldException {
        super(utils, contentMetadataCodec, ioExecutor, retryHandler, ioRetryHandler, errorHandler, wire);
        if (utils.getMaxConnections() > 0)
            System.setProperty("http.maxConnections",
                    String.valueOf(checkNotNull(utils, "utils").getMaxConnections()));
        this.untrustedSSLContextProvider = checkNotNull(untrustedSSLContextProvider, "untrustedSSLContextProvider");
        this.verifier = checkNotNull(verifier, "verifier");
        this.proxyForURI = checkNotNull(proxyForURI, "proxyForURI");
        this.methodField = HttpURLConnection.class.getDeclaredField("method");
        this.methodField.setAccessible(true);
    }

    @Override
    protected HttpResponse invoke(HttpURLConnection connection) throws IOException, InterruptedException {
        HttpResponse.Builder<?> builder = HttpResponse.builder();
        InputStream in = null;
        try {
            in = consumeOnClose(connection.getInputStream());
        } catch (IOException e) {
            in = bufferAndCloseStream(connection.getErrorStream());
        } catch (RuntimeException e) {
            closeQuietly(in);
            throw propagate(e);
        }

        int responseCode = connection.getResponseCode();
        if (responseCode == 204) {
            closeQuietly(in);
            in = null;
        }
        builder.statusCode(responseCode);
        builder.message(connection.getResponseMessage());

        Builder<String, String> headerBuilder = ImmutableMultimap.builder();
        for (Map.Entry<String, List<String>> entry : connection.getHeaderFields().entrySet()) {
            String header = entry.getKey();
            // HTTP message comes back as a header without a key
            if (header != null)
                headerBuilder.putAll(header, entry.getValue());
        }
        ImmutableMultimap<String, String> headers = headerBuilder.build();
        if (in != null) {
            Payload payload = newInputStreamPayload(in);
            contentMetadataCodec.fromHeaders(payload.getContentMetadata(), headers);
            builder.payload(payload);
        }
        builder.headers(filterOutContentHeaders(headers));
        return builder.build();
    }

    private InputStream bufferAndCloseStream(InputStream inputStream) throws IOException {
        InputStream in = null;
        try {
            if (inputStream != null) {
                in = new ByteArrayInputStream(toByteArray(inputStream));
            }
        } finally {
            closeQuietly(inputStream);
        }
        return in;
    }

    @Override
    protected HttpURLConnection convert(HttpRequest request) throws IOException, InterruptedException {
        boolean chunked = "chunked".equals(request.getFirstHeaderOrNull("Transfer-Encoding"));
        URL url = request.getEndpoint().toURL();

        HttpURLConnection connection = (HttpURLConnection) url
                .openConnection(proxyForURI.apply(request.getEndpoint()));
        if (connection instanceof HttpsURLConnection) {
            HttpsURLConnection sslCon = (HttpsURLConnection) connection;
            if (utils.relaxHostname())
                sslCon.setHostnameVerifier(verifier);
            if (sslContextSupplier != null) {
                // used for providers which e.g. use certs for authentication (like FGCP)
                // Provider provides SSLContext impl (which inits context with key manager)
                sslCon.setSSLSocketFactory(sslContextSupplier.get().getSocketFactory());
            } else if (utils.trustAllCerts()) {
                sslCon.setSSLSocketFactory(untrustedSSLContextProvider.get().getSocketFactory());
            }
        }
        connection.setConnectTimeout(utils.getConnectionTimeout());
        connection.setReadTimeout(utils.getSocketOpenTimeout());
        connection.setAllowUserInteraction(false);
        // do not follow redirects since https redirects don't work properly
        // ex. Caused by: java.io.IOException: HTTPS hostname wrong: should be
        // <adriancole.s3int0.s3-external-3.amazonaws.com>
        connection.setInstanceFollowRedirects(false);
        try {
            connection.setRequestMethod(request.getMethod());
        } catch (ProtocolException e) {
            try {
                methodField.set(connection, request.getMethod());
            } catch (IllegalAccessException e1) {
                logger.error(e, "could not set request method: ", request.getMethod());
                propagate(e1);
            }
        }

        for (Map.Entry<String, String> entry : request.getHeaders().entries()) {
            connection.setRequestProperty(entry.getKey(), entry.getValue());
        }

        String host = request.getEndpoint().getHost();
        if (request.getEndpoint().getPort() != -1) {
            host += ":" + request.getEndpoint().getPort();
        }
        connection.setRequestProperty(HOST, host);
        if (connection.getRequestProperty(USER_AGENT) == null) {
            connection.setRequestProperty(USER_AGENT, DEFAULT_USER_AGENT);
        }
        Payload payload = request.getPayload();
        if (payload != null) {
            MutableContentMetadata md = payload.getContentMetadata();
            for (Map.Entry<String, String> entry : contentMetadataCodec.toHeaders(md).entries()) {
                connection.setRequestProperty(entry.getKey(), entry.getValue());
            }
            if (chunked) {
                connection.setChunkedStreamingMode(8196);
                writePayloadToConnection(payload, "streaming", connection);
            } else {
                Long length = checkNotNull(md.getContentLength(), "payload.getContentLength");
                // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6755625
                checkArgument(length < Integer.MAX_VALUE,
                        "JDK 1.6 does not support >2GB chunks. Use chunked encoding, if possible.");
                if (length > 0) {
                    connection.setRequestProperty(CONTENT_LENGTH, length.toString());
                    connection.setFixedLengthStreamingMode(length.intValue());
                    writePayloadToConnection(payload, length, connection);
                } else {
                    writeNothing(connection);
                }
            }
        } else {
            writeNothing(connection);
        }
        return connection;
    }

    protected void writeNothing(HttpURLConnection connection) {
        if (!HttpRequest.NON_PAYLOAD_METHODS.contains(connection.getRequestMethod())) {
            connection.setRequestProperty(CONTENT_LENGTH, "0");
            // HttpUrlConnection strips Content-Length: 0 without setDoOutput(true)
            String method = connection.getRequestMethod();
            if ("POST".equals(method) || "PUT".equals(method)) {
                connection.setFixedLengthStreamingMode(0);
                connection.setDoOutput(true);
            }
        }
    }

    /**
     * @param payload
     *           payload to write
     * @param lengthDesc
     *           what to use in error log when an IOException occurs
     * @param connection
     *           connection to write to
     */
    void writePayloadToConnection(Payload payload, Object lengthDesc, HttpURLConnection connection)
            throws IOException {
        connection.setDoOutput(true);
        CountingOutputStream out = new CountingOutputStream(connection.getOutputStream());
        try {
            payload.writeTo(out);
        } catch (IOException e) {
            logger.error(e, "error after writing %d/%s bytes to %s", out.getCount(), lengthDesc,
                    connection.getURL());
            throw e;
        }
    }

    @Override
    protected void cleanup(HttpURLConnection connection) {
        if (connection != null)
            connection.disconnect();
    }
}