org.opendatakit.http.conn.GaeManagedClientConnection.java Source code

Java tutorial

Introduction

Here is the source code for org.opendatakit.http.conn.GaeManagedClientConnection.java

Source

/*
 * Copyright (C) 2011 University of Washington
 *
 * 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.opendatakit.http.conn;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.URL;
import java.util.concurrent.TimeUnit;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.Header;
import org.apache.http.HttpConnectionMetrics;
import org.apache.http.HttpEntity;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.ProtocolVersion;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpHead;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ManagedClientConnection;
import org.apache.http.conn.OperatedClientConnection;
import org.apache.http.conn.routing.HttpRoute;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.impl.client.EntityEnclosingRequestWrapper;
import org.apache.http.impl.client.RequestWrapper;
import org.apache.http.impl.conn.ConnectionShutdownException;
import org.apache.http.message.BasicHeader;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;

import com.google.appengine.api.urlfetch.HTTPMethod;
import com.google.appengine.api.urlfetch.URLFetchService;
import com.google.appengine.api.urlfetch.URLFetchServiceFactory;

/**
 * Implementation of a ManagedClientConnection that uses Google's
 * URLFetchService under the covers to send and receive the message.
 * Handles all message types (DELETE, GET, HEAD, PUT, POST).
 * 
 * @author mitchellsundt@gmail.com
 *
 */
public class GaeManagedClientConnection implements ManagedClientConnection {

    private static final Log logger = LogFactory.getLog(GaeManagedClientConnection.class);

    /** The state object associated with this connection */
    private Object state;

    /** The route of this connection. */
    private HttpRoute route;

    /** The context for the open() request (unused) */
    @SuppressWarnings("unused")
    private HttpContext context = null;

    /** The parameters on the open() request */
    private HttpParams params = null;

    /** The target host of this connection. */
    private HttpHost targetHost;

    /** The request to send */
    private HttpRequest request = null;

    /** The expect-continue headers (because we strip them from request */
    private Header[] expectContinueHeaders = null;

    /** The returned response */
    private com.google.appengine.api.urlfetch.HTTPResponse response = null;

    /** default is to not set this */
    private int timeoutMilliseconds = -1;

    /** The communications are in a reusable state (i.e., not open) */
    private boolean reusable = true;
    private boolean broken = false;

    /**
     * Reset the state of this object (for reuse)
     */
    private void reset() {
        request = null;
        expectContinueHeaders = null;
        response = null;
        reusable = true;
        broken = false;
    }

    GaeManagedClientConnection(HttpRoute route, Object state) {
        this.route = route;
        if (route != null) {
            this.targetHost = route.getTargetHost();
        } else {
            this.targetHost = null;
        }
        this.state = state;
    }

    /**
     * @deprecated use {@link #assertValid(OperatedClientConnection)}
     *
     * @throws IllegalStateException    if this manager is shut down
     */
    protected final void assertNotOpen() throws IllegalStateException {
        if (!reusable)
            throw new IllegalStateException("Connection is already open.");
        if (broken)
            throw new IllegalStateException("Connection is not cleanly closed.");
    }

    /**
     * Asserts that there is a wrapped connection to delegate to.
     * @since 4.1
     * @return value of released flag
     */
    protected boolean isReleased() {
        return reusable && !broken;
    }

    /**
     * Asserts that there is a valid wrapped connection to delegate to.
     *
     * @throws ConnectionShutdownException if there is no wrapped connection
     *                                  or connection has been aborted
     */
    protected final void assertValid(final OperatedClientConnection wrappedConn)
            throws ConnectionShutdownException {
        if (isReleased() || wrappedConn == null) {
            throw new ConnectionShutdownException();
        }
    }

    public final HttpHost getTargetHost() {
        return this.targetHost;
    }

    @Override
    public HttpRoute getRoute() {
        return route;
    }

    @Override
    public javax.net.ssl.SSLSession getSSLSession() {
        return null;
    }

    @Override
    public Object getState() {
        return state;
    }

    @Override
    public boolean isMarkedReusable() {
        return reusable && !broken;
    }

    @Override
    public boolean isSecure() {
        return route.isSecure();
    }

    @Override
    public void layerProtocol(HttpContext context, HttpParams params) throws IOException {
        // handled by URLFetch layer
        throw new IllegalStateException("not supported");
    }

    @Override
    public void markReusable() {
        reset();
    }

    @Override
    public void open(HttpRoute route, HttpContext context, HttpParams params) throws IOException {
        assertNotOpen();
        // mark as non-reusable (we are open...)
        reusable = false;
        if (route != null) {
            this.route = route;
            this.targetHost = route.getTargetHost();
        }
        this.context = context;
        this.params = params;
    }

    @Override
    public void setIdleDuration(long duration, TimeUnit unit) {
        // don't care -- connection is never reused
    }

    @Override
    public void setState(Object state) {
        this.state = state;
    }

    @Override
    public void tunnelProxy(HttpHost next, boolean secure, HttpParams params) throws IOException {
        // handled by URLFetch layer
        throw new IllegalStateException("not supported");
    }

    @Override
    public void tunnelTarget(boolean secure, HttpParams params) throws IOException {
        // handled by URLFetch layer
        throw new IllegalStateException("not supported");
    }

    @Override
    public void unmarkReusable() {
        reusable = false;
        broken = false;
    }

    @Override
    public void flush() throws IOException {
        // flush is always called by 
        // org.apache.http.protocol.HttpRequestExecutor.doSendRequest

        // Build and issue the URLFetch request here.
        URLFetchService service = URLFetchServiceFactory.getURLFetchService();

        boolean redirect = HttpClientParams.isRedirecting(params);
        @SuppressWarnings("unused")
        boolean authenticate = HttpClientParams.isAuthenticating(params);
        // TODO: verify that authentication is handled by URLFetchService...

        // default is to throw an exception on a overly-large request
        // follow redirects (e.g., to https), and to validate server
        // certificates.
        com.google.appengine.api.urlfetch.FetchOptions f = com.google.appengine.api.urlfetch.FetchOptions.Builder
                .withDefaults();
        f.disallowTruncate();
        f.validateCertificate();
        if (redirect) {
            f.followRedirects();
        } else {
            f.doNotFollowRedirects();
        }

        // set a deadline if we have a wait-for-continue limit
        // in an expectContinue situation 
        // or a timeout value set on the connection.
        HttpParams params = request.getParams();
        int deadline = 0;
        int msWaitForContinue = params.getIntParameter(CoreProtocolPNames.WAIT_FOR_CONTINUE, 2000);
        if (expectContinueHeaders == null) {
            msWaitForContinue = 0;
        }
        int soTimeout = org.apache.http.params.HttpConnectionParams.getSoTimeout(params);
        int connTimeout = org.apache.http.params.HttpConnectionParams.getConnectionTimeout(params);
        if (soTimeout <= 0 || connTimeout <= 0) {
            deadline = 0; // wait forever...
        } else {
            int maxDelay = Math.max(Math.max(timeoutMilliseconds, msWaitForContinue), connTimeout);
            deadline = soTimeout + maxDelay;
        }

        if (deadline > 0) {
            logger.info("URLFetch timeout (socket + connection) (ms): " + deadline);
            f.setDeadline(new Double(0.001 * (double) deadline));
        }
        f.validateCertificate();

        com.google.appengine.api.urlfetch.HTTPMethod method;
        if (request instanceof HttpGet) {
            method = HTTPMethod.GET;
        } else if (request instanceof HttpPut) {
            method = HTTPMethod.PUT;
        } else if (request instanceof HttpPost) {
            method = HTTPMethod.POST;
        } else if (request instanceof HttpHead) {
            method = HTTPMethod.HEAD;
        } else if (request instanceof HttpDelete) {
            method = HTTPMethod.DELETE;
        } else if (request instanceof EntityEnclosingRequestWrapper) {
            String name = ((EntityEnclosingRequestWrapper) request).getMethod();
            method = HTTPMethod.valueOf(name);
        } else if (request instanceof RequestWrapper) {
            String name = ((RequestWrapper) request).getMethod();
            method = HTTPMethod.valueOf(name);
        } else {
            throw new IllegalStateException("Unrecognized Http request method");
        }

        // we need to construct the URL for the request
        // to the target host.  The request line, for, e.g., 
        // a get, needs to be added to the URL.
        URL url = new URL(targetHost.getSchemeName(), targetHost.getHostName(), targetHost.getPort(),
                request.getRequestLine().getUri());

        com.google.appengine.api.urlfetch.HTTPRequest req = new com.google.appengine.api.urlfetch.HTTPRequest(url,
                method, f);

        Header[] headers = request.getAllHeaders();
        for (Header h : headers) {
            req.addHeader(new com.google.appengine.api.urlfetch.HTTPHeader(h.getName(), h.getValue()));
        }
        // restore the expect-continue header
        if (expectContinueHeaders != null) {
            for (Header h : expectContinueHeaders) {
                req.addHeader(new com.google.appengine.api.urlfetch.HTTPHeader(h.getName(), h.getValue()));
            }
        }

        // see if we need to copy entity body over...
        if (request instanceof HttpEntityEnclosingRequest) {
            HttpEntity entity = ((HttpEntityEnclosingRequest) request).getEntity();
            if (entity != null) {
                ByteArrayOutputStream blobStream = new ByteArrayOutputStream();
                entity.writeTo(blobStream);
                req.setPayload(blobStream.toByteArray());
            }
        }

        response = service.fetch(req);
    }

    @Override
    public boolean isResponseAvailable(int arg0) throws IOException {
        return !reusable && !broken && (response != null);
    }

    @Override
    public void receiveResponseEntity(HttpResponse resp) throws HttpException, IOException {
        if (resp == null) {
            throw new IllegalArgumentException("HttpResponse cannot be null");
        }
        if (response == null) {
            throw new IllegalStateException("no response avaliable");
        }

        byte[] byteArray = response.getContent();
        if (byteArray != null) {
            ByteArrayEntity entity = new ByteArrayEntity(response.getContent());
            entity.setContentType(resp.getFirstHeader(HTTP.CONTENT_TYPE));
            resp.setEntity(entity);
        }
    }

    @Override
    public HttpResponse receiveResponseHeader() throws HttpException, IOException {
        if (response == null) {
            throw new IllegalStateException("no response avaliable");
        }
        // we don't have access to the protocol version, so assume it is Http 1.1
        HttpResponse resp = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), response.getResponseCode(),
                null);

        for (com.google.appengine.api.urlfetch.HTTPHeader h : response.getHeaders()) {
            resp.addHeader(new BasicHeader(h.getName(), h.getValue()));
        }

        return resp;
    }

    @Override
    public void sendRequestEntity(HttpEntityEnclosingRequest request) throws HttpException, IOException {
        if (reusable && !broken) {
            throw new IllegalStateException("Connection has not yet been opened");
        }

        if (this.request != request) {
            throw new IllegalStateException("Connection already sending a different request");
        }
    }

    @Override
    public void sendRequestHeader(HttpRequest request) throws HttpException, IOException {
        if (reusable && !broken) {
            throw new IllegalStateException("Connection has not yet been opened");
        }
        this.request = request;
        if (request instanceof HttpEntityEnclosingRequest) {
            HttpEntityEnclosingRequest req = (HttpEntityEnclosingRequest) request;
            expectContinueHeaders = req.getHeaders(HTTP.EXPECT_DIRECTIVE);
            req.removeHeaders(HTTP.EXPECT_DIRECTIVE);
        }
    }

    @Override
    public void close() throws IOException {
        reset();
    }

    @Override
    public HttpConnectionMetrics getMetrics() {
        // none available...
        return null;
    }

    @Override
    public int getSocketTimeout() {
        return timeoutMilliseconds;
    }

    @Override
    public boolean isOpen() {
        return !reusable;
    }

    @Override
    public boolean isStale() {
        // assume it isn't...
        return false;
    }

    @Override
    public void setSocketTimeout(int milliseconds) {
        this.timeoutMilliseconds = milliseconds;
    }

    @Override
    public void shutdown() throws IOException {
        reusable = false;
        broken = true;
    }

    @Override
    public InetAddress getLocalAddress() {
        return route.getLocalAddress();
    }

    @Override
    public int getLocalPort() {
        // not available...
        return 0;
    }

    @Override
    public InetAddress getRemoteAddress() {
        // not available...
        return null;
    }

    @Override
    public int getRemotePort() {
        HttpHost host = route.getTargetHost();
        int port = host.getPort();
        if (port == -1) {
            if (host.getSchemeName().equalsIgnoreCase("http")) {
                return 80;
            } else {
                return 443;
            }
        }
        return host.getPort();
    }

    @Override
    public void abortConnection() throws IOException {
        reset();
        reusable = false;
        broken = true;
    }

    @Override
    public void releaseConnection() throws IOException {
        reset();
    }
}