Package com.google.gdata.client.http

Source Code of com.google.gdata.client.http.HttpGDataRequest

/* Copyright (c) 2008 Google Inc.
*
* 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.google.gdata.client.http;

import com.google.gdata.util.common.xml.XmlWriter;
import com.google.gdata.client.AuthTokenFactory;
import com.google.gdata.client.GDataProtocol;
import com.google.gdata.client.Query;
import com.google.gdata.client.GDataProtocol.Header;
import com.google.gdata.client.Service.GDataRequest;
import com.google.gdata.client.Service.GDataRequestFactory;
import com.google.gdata.client.authn.oauthproxy.OAuthProxyProtocol;
import com.google.gdata.data.DateTime;
import com.google.gdata.data.ParseSource;
import com.google.gdata.util.AuthenticationException;
import com.google.gdata.util.ContentType;
import com.google.gdata.util.EntityTooLargeException;
import com.google.gdata.util.InvalidEntryException;
import com.google.gdata.util.LoggableInputStream;
import com.google.gdata.util.LoggableOutputStream;
import com.google.gdata.util.NoLongerAvailableException;
import com.google.gdata.util.NotAcceptableException;
import com.google.gdata.util.NotImplementedException;
import com.google.gdata.util.NotModifiedException;
import com.google.gdata.util.OAuthProxyException;
import com.google.gdata.util.PreconditionFailedException;
import com.google.gdata.util.ResourceNotFoundException;
import com.google.gdata.util.ServiceException;
import com.google.gdata.util.ServiceForbiddenException;
import com.google.gdata.util.VersionConflictException;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.ProtocolException;
import java.net.URL;
import java.net.URLConnection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.GZIPInputStream;

/**
* The HttpGDataRequest class provides a basic implementation of the
* <code>GDataRequest</code> interface over HTTP.
*
* @see GDataRequest
*/
public class HttpGDataRequest implements GDataRequest {

  static final Logger logger =
      Logger.getLogger(HttpGDataRequest.class.getName());

  /**
   * If this system property is set to <code>true</code>, the GData HTTP
   * client library will use POST to send data to the associated GData service
   * and will specify the actual method using the {@code METHOD_OVERRIDE_HEADER}
   * HTTP header. This can be used as a workaround for HTTP proxies or gateways
   * that do not handle PUT or DELETE HTTP methods properly. If the system
   * property is <code>false</code>, the regular PUT and DELETE HTTP verbs
   * will be used.
   */
  public static final String METHOD_OVERRIDE_PROPERTY =
      "com.google.gdata.UseMethodOverride";


  /**
   * Name of HTTP header containing the method name that overrides the normal
   * HTTP method.
   *
   * @deprecated Use {@link GDataProtocol.Header#METHOD_OVERRIDE} instead
   */
  @Deprecated
  public static final String METHOD_OVERRIDE_HEADER =
      GDataProtocol.Header.METHOD_OVERRIDE;


  /**
   * The HttpGDataRequest.Factory class is a factory class for constructing
   * new HttpGDataRequest instances.
   */
  public static class Factory implements GDataRequestFactory {

    protected HttpAuthToken authToken;
    protected Map<String, String> headerMap
        = new LinkedHashMap<String, String>();
    protected Map<String, String> privateHeaderMap
        = new LinkedHashMap<String, String>();
    protected boolean useSsl = false;
    protected HttpUrlConnectionSource connectionSource =
        JdkHttpUrlConnectionSource.INSTANCE;

    public void setAuthToken(AuthTokenFactory.AuthToken authToken) {
      if (authToken != null && !(authToken instanceof HttpAuthToken)) {
        throw new IllegalArgumentException("Invalid token type");
      }
      setAuthToken((HttpAuthToken) authToken);
    }

    public void setAuthToken(HttpAuthToken authToken) {
      this.authToken = authToken;
    }

    public void useSsl() {
      this.useSsl = true;
    }

    private void extendHeaderMap(Map<String, String> headerMap,
                                 String header, String value) {
      if (value == null) {
        headerMap.remove(header);
      } else {
        headerMap.put(header, value);
      }
    }

    public void setHeader(String header, String value) {
      extendHeaderMap(this.headerMap, header, value);
    }

    public void setPrivateHeader(String header, String value) {
      extendHeaderMap(this.privateHeaderMap, header, value);
    }

    /**
     * Sets a specific {@link HttpUrlConnectionSource} instance to create
     * backing {@link URLConnection} instance.
     */
    public void setConnectionSource(HttpUrlConnectionSource connectionSource) {
      if (connectionSource == null) {
        throw new NullPointerException("connectionSource");
      }
      this.connectionSource = connectionSource;
    }

    @SuppressWarnings("unused")
    public GDataRequest getRequest(RequestType type,
                                   URL requestUrl,
                                   ContentType contentType)
        throws IOException, ServiceException {
      if (this.useSsl && !requestUrl.getProtocol().startsWith("https")) {
        requestUrl = new URL(
            requestUrl.toString().replaceFirst("http", "https"));
      }
      return createRequest(type, requestUrl, contentType);
    }

    @SuppressWarnings("unused")
    public GDataRequest getRequest(Query query, ContentType contentType)
        throws IOException, ServiceException {
      return getRequest(RequestType.QUERY, query.getUrl(), contentType);
    }

    /**
     * Creates a {@link GDataRequest} instance.
     *
     * <p>This method is called from {@link #getRequest} after any changes to
     * the parameters have been applied.
     *
     * <p>Subclasses should overwrite this method and not {@link #getRequest}
     */
    protected GDataRequest createRequest(RequestType type,
        URL requestUrl, ContentType contentType)
        throws IOException, ServiceException {
      return new HttpGDataRequest(type, requestUrl, contentType,
          authToken, headerMap, privateHeaderMap, connectionSource);
    }
  }

  /**
   * Source of {@link HttpURLConnection} instances.
   */
  protected final HttpUrlConnectionSource connectionSource;

  /**
   * Underlying HTTP connection to the GData service.
   */
  protected HttpURLConnection httpConn;

  /**
   * The request URL provided by the client.
   */
  protected URL requestUrl;

  /**
   * The GData request type.
   */
  protected RequestType type;


  /**
   * Indicates whether request execution has taken place. Set to
   * <code>true</code> if executed, <code>false</code> otherwise.
   */
  protected boolean executed = false;


  /**
   * True if the request type expects input from the client.
   */
  protected boolean expectsInput;

  /**
   * Contains the content type of the request data
   */
  protected ContentType inputType;

  /**
   * True if the request type returns output to the client.
   */
  protected boolean hasOutput;


  /**
   * The connection timeout for this request. A value of -1 means no value has
   * been configured (use JDK default timeout behavior).
   */
  protected int connectTimeout = -1;


  /**
   * The read timeout for this request. A value of -1 means no value has been
   * configured (use JDK default timeout behavior).
   */
  protected int readTimeout = -1;

  /**
   * The auth token used for this request, may be {@code null}.
   */
  protected final HttpAuthToken authToken;

  /**
   * The input stream from which HTTP response data may be read or {@code null}
   * if no response stream or not opened yet via {@link #getResponseStream()}.
   */
  private InputStream inputStream = null;

  /**
   * Constructs a new HttpGDataRequest instance of the specified RequestType,
   * targeting the specified URL.
   *
   * @param type type of GDataRequest.
   * @param requestUrl request target URL.
   * @param inputType the content type of request data (or {@code null}).
   * @param authToken the auth token used for this request (or {@code null}).
   * @param headerMap a set of headers to be included in each request
   * @param privateHeaderMap a set of headers to be included in each request
   * @param connectionSource source of {@link HttpURLConnection}s
   * @throws IOException on error initializating service connection.
   */
  protected HttpGDataRequest(RequestType type, URL requestUrl,
      ContentType inputType, HttpAuthToken authToken,
      Map<String, String> headerMap, Map<String, String> privateHeaderMap,
      HttpUrlConnectionSource connectionSource)
      throws IOException {

    this.connectionSource = connectionSource;
    this.type = type;
    this.inputType = inputType;
    this.requestUrl = requestUrl;
    this.httpConn = getRequestConnection(requestUrl);
    this.authToken = authToken;

    switch (type) {

      case QUERY:
        hasOutput = true;
        break;

      case INSERT:
      case BATCH:
        expectsInput = true;
        hasOutput = true;
        setMethod("POST");
        setHeader("Content-Type", inputType.toString());
        break;

      case UPDATE:
        expectsInput = true;
        hasOutput = true;
        if (Boolean.getBoolean(METHOD_OVERRIDE_PROPERTY)) {
          setMethod("POST");
          setHeader(Header.METHOD_OVERRIDE, "PUT");
        } else {
          setMethod("PUT");
        }
        setHeader("Content-Type", inputType.toString());
        break;

      case PATCH:
        expectsInput = true;
        hasOutput = true;

        // HTTPUrlConnection does not accept unrecognized methods, so always use
        // the POST override model for PATCH requests
        setMethod("POST");
        setHeader(Header.METHOD_OVERRIDE, "PATCH");
        setHeader("Content-Type", inputType.toString());
        break;

      case DELETE:
        if (Boolean.getBoolean(METHOD_OVERRIDE_PROPERTY)) {
          setMethod("POST");
          setHeader(Header.METHOD_OVERRIDE, "DELETE");
        } else {
          setMethod("DELETE");
        }
        setHeader("Content-Length", "0"); // no data to POST
        break;

      default:
        throw new UnsupportedOperationException("Unknown request type:" + type);
    }

    if (authToken != null) {
      // NOTE: Do not use setHeader() here, authorization should never be
      // logged.
      String authHeader =
          authToken.getAuthorizationHeader(requestUrl, httpConn
              .getRequestMethod());
      setPrivateHeader("Authorization", authHeader);
    }

    if (headerMap != null) {
      for (Map.Entry<String, String> e : headerMap.entrySet()) {
        setHeader(e.getKey(), e.getValue());
      }
    }

    if (privateHeaderMap != null) {
      for (Map.Entry<String, String> e : privateHeaderMap.entrySet()) {
        setPrivateHeader(e.getKey(), e.getValue());
      }
    }

    // Request compressed response format
    setHeader("Accept-Encoding", "gzip");

    httpConn.setDoOutput(expectsInput);
  }

  /**
   * Protected default constructor for testing.
   */
  protected HttpGDataRequest() {
    connectionSource = JdkHttpUrlConnectionSource.INSTANCE;
    this.authToken = null;
  }

  public URL getRequestUrl() {
    return requestUrl;
  }

  public ContentType getRequestContentType() {
    return inputType;
  }

  /**
   * Obtains a connection to the GData service.
   */
  protected HttpURLConnection getRequestConnection(URL requestUrl)
      throws IOException {

    HttpURLConnection uc;
    try {
      uc = connectionSource.openConnection(requestUrl);
    } catch (IllegalArgumentException e) {
      throw new UnsupportedOperationException("Unsupported scheme:"
          + requestUrl.getProtocol());
    }

    // Should never cache GData requests/responses
    uc.setUseCaches(false);

    // Always follow redirects
    uc.setInstanceFollowRedirects(true);

    return uc;
  }

  public void setConnectTimeout(int timeout) {
    if (timeout < 0) {
      throw new IllegalArgumentException("Timeout cannot be negative");
    }
    connectTimeout = timeout;
  }

  public void setReadTimeout(int timeout) {
    if (timeout < 0) {
      throw new IllegalArgumentException("Timeout cannot be negative");
    }
    readTimeout = timeout;
  }

  public void setIfModifiedSince(DateTime conditionDate) {
    if (conditionDate == null) {
      return;
    }

    if (type == RequestType.QUERY) {
      setHeader(GDataProtocol.Header.IF_MODIFIED_SINCE,
          conditionDate.toStringRfc822());
    } else {
      throw new IllegalStateException(
          "Date conditions not supported for this request type");
    }
  }

  public void setEtag(String etag) {

    if (etag == null) {
      return;
    }

    switch (type) {
      case QUERY:
        if (etag != null) {
          setHeader(GDataProtocol.Header.IF_NONE_MATCH, etag);
        }
        break;
      case PATCH:
      case UPDATE:
      case DELETE:
        if (etag != null) {
          setHeader(GDataProtocol.Header.IF_MATCH, etag);
        }
        break;
      default:
        throw new IllegalStateException(
            "Etag conditions not supported for this request type");
    }
  }

  public OutputStream getRequestStream() throws IOException {

    if (!expectsInput) {
      throw new IllegalStateException("Request doesn't accept input");
    }
    if (logger.isLoggable(Level.FINEST)){
      return new LoggableOutputStream(logger, httpConn.getOutputStream());
    }
    return httpConn.getOutputStream();
  }


  public XmlWriter getRequestWriter() throws IOException {
    OutputStream requestStream = getRequestStream();
    Writer writer = new OutputStreamWriter(requestStream, "utf-8");
    return new XmlWriter(writer);
  }

  public void setMethod(String method) throws ProtocolException {
    httpConn.setRequestMethod(method);
    if (logger.isLoggable(Level.FINE)) {
      logger.fine(method + " " + httpConn.getURL().toExternalForm());
    }
  }

  public void setHeader(String name, String value) {
    httpConn.setRequestProperty(name, value);
    logger.finer(name + ": " + value);
  }


  public void setPrivateHeader(String name, String value) {
    httpConn.setRequestProperty(name, value);
    logger.finer(name + ": <Not Logged>");
  }

  public void execute() throws IOException, ServiceException {

    if (connectTimeout >= 0) {
      httpConn.setConnectTimeout(connectTimeout);
    }

    if (readTimeout >= 0) {
      httpConn.setReadTimeout(readTimeout);
    }

    // Set the http.strictPostRedirect property to prevent redirected
    // POST/PUT/DELETE from being mapped to a GET. This
    // system property was a hack to fix a jdk bug w/out changing back
    // compat behavior. It's bogus that this is a system (and not a
    // per-connection) property, so we just change it for the duration
    // of the connection.
    // See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4023866
    String httpStrictPostRedirect =
        System.getProperty("http.strictPostRedirect");
    try {
      System.setProperty("http.strictPostRedirect", "true");
      httpConn.connect();

      if (logger.isLoggable(Level.FINE)) {

        // Avoid calling URL.equals() unless an object equivalence test fails,
        // because URL.equals() requires DNS resolution. This test will
        // fail on the first check for any URLConnection implementation
        // that derives from java.net.URLConnection. The 2nd check would
        // work on an alternate impl that clones the URL.
        if (httpConn.getURL() != requestUrl
            && !httpConn.getURL().toExternalForm().equals(
                requestUrl.toExternalForm())) {
          logger.fine("Redirected to:" + httpConn.getURL().toExternalForm());
        }

        // Log response information here, if enabled
        logger.fine(httpConn.getResponseCode() + " "
            + httpConn.getResponseMessage());
        if (logger.isLoggable(Level.FINER)) {
          for (Map.Entry<String, List<String>> headerField : httpConn
              .getHeaderFields().entrySet()) {
            for (String value : headerField.getValue()) {
              logger.finer(headerField.getKey() + ": " + value);
            }
          }
        }
      }
      checkResponse(); // will flush any request data

    } finally {
      if (httpStrictPostRedirect == null) {
        System.clearProperty("http.strictPostRedirect");
      } else {
        System.setProperty("http.strictPostRedirect", httpStrictPostRedirect);
      }
    }

    executed = true;
  }

  /**
   * Called after a request is executed to process the response and generate an
   * appropriate exception (on failure).
   */
  protected void checkResponse() throws IOException, ServiceException {

    if (isOAuthProxyErrorResponse()) {
      handleOAuthProxyErrorResponse();
    } else if (httpConn.getResponseCode() >= 300) {
      handleErrorResponse();
    }
  }

  /** Whether or not the http response comes from the OAuth Proxy. */
  private boolean isOAuthProxyErrorResponse() throws IOException {
    Set<String> headers = httpConn.getHeaderFields().keySet();
    boolean isOAuthRedirectToApproval =
        headers.contains(OAuthProxyProtocol.Header.X_OAUTH_APPROVAL_URL);
    boolean isOtherOAuthError =
        httpConn.getResponseCode() == HttpURLConnection.HTTP_OK
        && (headers.contains(OAuthProxyProtocol.Header.X_OAUTH_ERROR)
        || headers.contains(OAuthProxyProtocol.Header.X_OAUTH_ERROR_TEXT));
    return isOAuthRedirectToApproval || isOtherOAuthError;
  }

  /**
   * Sets the appropriate parameters used by the OAuth Proxy and throws an
   * {@link OAuthProxyException}.
   */
  private void handleOAuthProxyErrorResponse() throws IOException,
      ServiceException {
    throw new OAuthProxyException(httpConn);
  }

  /**
   * Handles an error response received while executing a GData service request.
   * Throws a {@link ServiceException} or one of its subclasses, depending on
   * the failure conditions.
   *
   * @throws ServiceException exception describing the failure.
   * @throws IOException error reading the error response from the GData
   *         service.
   */
  protected void handleErrorResponse() throws ServiceException, IOException {

    switch (httpConn.getResponseCode()) {

      case HttpURLConnection.HTTP_NOT_FOUND:
        throw new ResourceNotFoundException(httpConn);

      case HttpURLConnection.HTTP_BAD_REQUEST:
        throw new InvalidEntryException(httpConn);

      case HttpURLConnection.HTTP_FORBIDDEN:
        throw new ServiceForbiddenException(httpConn);

      case HttpURLConnection.HTTP_UNAUTHORIZED:
        throw new AuthenticationException(httpConn);

      case HttpURLConnection.HTTP_NOT_MODIFIED:
        throw new NotModifiedException(httpConn);

      case HttpURLConnection.HTTP_PRECON_FAILED:
        throw new PreconditionFailedException(httpConn);

      case HttpURLConnection.HTTP_NOT_IMPLEMENTED:
        throw new NotImplementedException(httpConn);

      case HttpURLConnection.HTTP_CONFLICT:
        throw new VersionConflictException(httpConn);

      case HttpURLConnection.HTTP_ENTITY_TOO_LARGE:
        throw new EntityTooLargeException(httpConn);

      case HttpURLConnection.HTTP_NOT_ACCEPTABLE:
        throw new NotAcceptableException(httpConn);

      case HttpURLConnection.HTTP_GONE:
        throw new NoLongerAvailableException(httpConn);

      default:
        throw new ServiceException(httpConn);
    }
  }

  public ContentType getResponseContentType() {

    if (!executed) {
      throw new IllegalStateException(
          "Must call execute() before attempting to read response");
    }
    String value = httpConn.getHeaderField("Content-Type");
    if (value == null) {
      return null;
    }
    return new ContentType(value);
  }

  public String getResponseHeader(String headerName) {
    return httpConn.getHeaderField(headerName);
  }

  public DateTime getResponseDateHeader(String headerName) {
    long dateValue = httpConn.getHeaderFieldDate(headerName, -1);
    return (dateValue >= 0) ? new DateTime(dateValue) : null;
  }

  public InputStream getResponseStream() throws IOException {

    if (!executed) {
      throw new IllegalStateException(
          "Must call execute() before attempting to read response");
    }

    if (!hasOutput) {
      throw new IllegalStateException("Request doesn't have response data");
    }

    if (inputStream != null) {
      return inputStream;
    }

    inputStream = httpConn.getInputStream();
    if ("gzip".equalsIgnoreCase(httpConn.getContentEncoding())) {
      inputStream = new GZIPInputStream(inputStream);
    }
    if (logger.isLoggable(Level.FINEST)){
      return new LoggableInputStream(logger, inputStream);
    }
    return inputStream;
  }

  public ParseSource getParseSource() throws IOException {
    return new ParseSource(getResponseStream());
  }

  /**
   * Returns the URLConnection instance that represents the underlying
   * connection to the GData service that will be used by this request.
   *
   * @return connection to GData service.
   */
  public HttpURLConnection getConnection() {
    return httpConn;
  }

  public void end() {
    try {
      if (inputStream != null) {
        inputStream.close();
      }
    } catch (IOException ioe) {
      logger.log(Level.WARNING, "Error closing response stream", ioe);
    }
  }
}
TOP

Related Classes of com.google.gdata.client.http.HttpGDataRequest

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.