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

org.apache.shindig.gadgets.http.HttpResponse Maven / Gradle / Ivy

Go to download

Renders gadgets, provides the gadget metadata service, and serves all javascript required by the OpenSocial specification.

The newest version!
/*
 * 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.apache.shindig.gadgets.http;

import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.MapMaker;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.inject.Inject;
import com.google.inject.name.Named;

import org.apache.commons.lang3.StringUtils;
import org.apache.shindig.common.servlet.HttpUtil;
import org.apache.shindig.common.util.DateUtil;
import org.apache.shindig.common.util.TimeSource;
import org.apache.shindig.gadgets.encoding.EncodingDetector;

import java.io.ByteArrayInputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

/**
 * Represents the results of an HTTP content retrieval operation.
 *
 * HttpResponse objects are immutable in order to allow them to be safely used in concurrent
 * caches and by multiple threads without worrying about concurrent modification.
 */
public final class HttpResponse implements Externalizable {
  private static final long serialVersionUID = 7526471155622776147L;

  public static final int SC_CONTINUE = 100;
  public static final int SC_SWITCHING_PROTOCOLS = 101;

  public static final int SC_OK = 200;
  public static final int SC_CREATED = 201;
  public static final int SC_ACCEPTED = 202;
  public static final int SC_NON_AUTHORITATIVE_INFORMATION = 203;
  public static final int SC_NO_CONTENT = 204;
  public static final int SC_RESET_CONTENT = 205;
  public static final int SC_PARTIAL_CONTENT = 206;

  public static final int SC_MULTIPLE_CHOICES = 300;
  public static final int SC_MOVED_PERMANENTLY = 301;
  public static final int SC_FOUND = 302;
  public static final int SC_SEE_OTHER = 303;
  public static final int SC_NOT_MODIFIED = 304;
  public static final int SC_USE_PROXY = 305;
  public static final int SC_TEMPORARY_REDIRECT = 307;

  public static final int SC_BAD_REQUEST = 400;
  public static final int SC_UNAUTHORIZED = 401;
  public static final int SC_PAYMENT_REQUIRED = 402;
  public static final int SC_FORBIDDEN = 403;
  public static final int SC_NOT_FOUND = 404;
  public static final int SC_METHOD_NOT_ALLOWED = 405;
  public static final int SC_NOT_ACCEPTABLE = 406;
  public static final int SC_PROXY_AUTHENTICATION_REQUIRED = 407;
  public static final int SC_REQUEST_TIMEOUT = 408;
  public static final int SC_CONFLICT = 409;
  public static final int SC_GONE = 410;
  public static final int SC_LENGTH_REQUIRED = 411;
  public static final int SC_PRECONDITION_FAILED = 412;
  public static final int SC_REQUEST_ENTITY_TOO_LARGE = 413;
  public static final int SC_REQUEST_URI_TOO_LONG = 414;
  public static final int SC_UNSUPPORTED_MEDIA_TYPE = 415;
  public static final int SC_REQUESTED_RANGE_NOT_SATISFIABLE = 416;
  public static final int SC_EXPECTATION_FAILED = 417;

  public static final int SC_INTERNAL_SERVER_ERROR = 500;
  public static final int SC_NOT_IMPLEMENTED = 501;
  public static final int SC_BAD_GATEWAY = 502;
  public static final int SC_SERVICE_UNAVAILABLE = 503;
  public static final int SC_GATEWAY_TIMEOUT = 504;
  public static final int SC_HTTP_VERSION_NOT_SUPPORTED = 505;

  // These content types can always skip encoding detection.
  private static final Set BINARY_CONTENT_TYPES = ImmutableSet.of(
      "image/jpeg", "image/png", "image/gif", "image/jpg", "application/x-shockwave-flash",
      "application/octet-stream", "application/ogg", "application/zip", "audio/mpeg",
      "audio/x-ms-wma", "audio/vnd.rn-realaudio", "audio/x-wav", "video/mpeg", "video/mp4",
      "video/quicktime", "video/x-ms-wmv", "video/x-flv", "video/flv",
      "video/x-ms-asf", "application/pdf", "image/x-icon"
  );

  // These HTTP status codes should always honor the HTTP status returned by the remote host. All
  // other error codes are treated as errors and will use the negativeCacheTtl value.
  private static final Set NEGATIVE_CACHING_EXEMPT_STATUS
      = ImmutableSet.of(SC_UNAUTHORIZED, SC_FORBIDDEN);

  // TTL to use when an error response is fetched. This should be non-zero to
  // avoid high rates of requests to bad urls in high-traffic situations.
  static final long DEFAULT_NEGATIVE_CACHE_TTL = 30 * 1000;

  // Default TTL for an entry in the cache that does not have any cache control headers.
  static final long DEFAULT_TTL = 5L * 60L * 1000L;

  static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8");

  @Inject(optional = true) @Named("shindig.cache.http.negativeCacheTtl")
  private static long negativeCacheTtl = DEFAULT_NEGATIVE_CACHE_TTL;

  // Default TTL for resources that are public and has no explicit Cache-Control max-age
  // and expires headers. Resources without cache-control are considered public by default.
  @Inject(optional = true) @Named("shindig.cache.http.defaultTtl")
  public static long defaultTtl = DEFAULT_TTL;

  @Inject(optional = true) @Named("shindig.http.fast-encoding-detection")
  private static boolean fastEncodingDetection = true;

  // Support injection of smarter encoding detection
  @Inject(optional = true)
  private static EncodingDetector.FallbackEncodingDetector customEncodingDetector =
      new EncodingDetector.FallbackEncodingDetector();

  public static void setTimeSource(TimeSource timeSource) {
    HttpUtil.setTimeSource(timeSource);
  }

  public static TimeSource getTimeSource() {
    return HttpUtil.getTimeSource();
  }

  // Holds character sets for fast conversion
  private static final Map encodingToCharset = new MapMaker().makeMap();

  private String responseString;
  private long date;
  private Charset encoding;
  private Map metadata;

  private int httpStatusCode;
  private Multimap headers;
  private byte[] responseBytes;

  private long refetchStrictNoCacheAfterMs;

  /**
   * Needed for serialization. Do not use this for any other purpose.
   */
  public HttpResponse() {}

  /**
   * Construct an HttpResponse from a builder (called by HttpResponseBuilder.create).
   * @param builder a valid builder
   */
  HttpResponse(HttpResponseBuilder builder) {
    httpStatusCode = builder.getHttpStatusCode();
    Multimap headerCopy = HttpResponse.newHeaderMultimap();

    // Always safe, HttpResponseBuilder won't modify the body.
    responseBytes = builder.getResponse();

    // Copy headers after builder.getResponse(), since that can modify Content-Type.
    headerCopy.putAll(builder.getHeaders());

    metadata = ImmutableMap.copyOf(builder.getMetadata());

    // We want to modify the headers to ensure that the proper Content-Type and Date headers
    // have been set. This allows us to avoid these expensive calculations from the cache.
    date = getAndUpdateDate(headerCopy);
    encoding = getAndUpdateEncoding(headerCopy, responseBytes);
    headers = Multimaps.unmodifiableMultimap(headerCopy);
    refetchStrictNoCacheAfterMs = builder.getRefetchStrictNoCacheAfterMs();
  }

  private HttpResponse(int httpStatusCode, String body) {
    this(new HttpResponseBuilder()
      .setHttpStatusCode(httpStatusCode)
      .setResponseString(body));
  }

  public HttpResponse(String body) {
    this(SC_OK, body);
  }

  public static HttpResponse error() {
    return new HttpResponse(SC_INTERNAL_SERVER_ERROR, "");
  }

  public static HttpResponse badrequest(String msg) {
    return new HttpResponse(SC_BAD_REQUEST, msg);
  }

  public static HttpResponse timeout() {
    return new HttpResponse(SC_GATEWAY_TIMEOUT, "");
  }

  public static HttpResponse notFound() {
    return new HttpResponse(SC_NOT_FOUND, "");
  }

  public int getHttpStatusCode() {
    return httpStatusCode;
  }

  /**
   * @return True if the status code is considered to be an error.
   */
  public boolean isError() {
    return httpStatusCode >= 400;
  }

  /**
   * @return The encoding of the response body, if we're able to determine it.
   */
  public String getEncoding() {
    return encoding.name();
  }

  /**
   * @return The Charset of the response body's encoding, if we were able to determine it.
   */
  public Charset getEncodingCharset() {
    return encoding;
  }

  /**
   * @return the content length
   */
  public int getContentLength() {
    return responseBytes.length;
  }

  /**
   * @return An input stream suitable for reading the entirety of the response.
   */
  public InputStream getResponse() {
    return new ByteArrayInputStream(responseBytes);
  }

  /**
   * Attempts to convert the response body to a string using the Content-Type header. If no
   * Content-Type header is specified (or it doesn't include an encoding), we will assume it is
   * DEFAULT_ENCODING.
   *
   * @return The body as a string.
   */
  public String getResponseAsString() {
    if (responseString == null) {
      responseString = encoding.decode(ByteBuffer.wrap(responseBytes)).toString();

      // Strip BOM if present.
      if (responseString.length() > 0 && responseString.codePointAt(0) == 0xFEFF) {
        responseString = responseString.substring(1);
      }
    }
    return responseString;
  }

  /**
   * @return All headers for this object.
   */
  public Multimap getHeaders() {
    return headers;
  }

  /**
   * @return All headers with the given name. If no headers are set for the given name, an empty
   * collection will be returned.
   */
  public Collection getHeaders(String name) {
    return headers.get(name);
  }

  /**
   * @return The first set header with the given name or null if not set. If you need multiple
   *         values for the header, use getHeaders().
   */
  public String getHeader(String name) {
    Collection headerList = getHeaders(name);
    if (headerList.isEmpty()) {
      return null;
    } else {
      return headerList.iterator().next();
    }
  }

  /**
   * @return additional data to embed in responses sent from the JSON proxy.
   */
  public Map getMetadata() {
    return metadata;
  }

  /**
   * Calculate the Cache Expiration for this response.
   *
   *
   * For errors (rc >=400) we intentionally ignore cache-control headers for most HTTP error responses, because if
   * we don't we end up hammering sites that have gone down with lots of requests. Certain classes
   * of client errors (authentication) have more severe behavioral implications if we cache them.
   *
   * For errors if the server provides a Retry-After header we use that.
   *
   * We technically shouldn't be caching certain 300 class status codes either, such as 302, but
   * in practice this is a better option for performance.
   *
   * @return consolidated cache expiration time or -1
   */
  public long getCacheExpiration() {
    if (isError() && !NEGATIVE_CACHING_EXEMPT_STATUS.contains(httpStatusCode)) {
      // If the server provides a Retry-After header use that as the cacheTtl
      String retryAfter = this.getHeader("Retry-After");
      if (retryAfter != null) {
        if (StringUtils.isNumeric(retryAfter)) {
          return date + Integer.valueOf(retryAfter) * 1000L;
        } else {
          Date expiresDate = DateUtil.parseRfc1123Date(retryAfter);
          if (expiresDate != null)
            return expiresDate.getTime();
        }
      }
      // default value
      return date + negativeCacheTtl;
    }

    if (isStrictNoCache()) {
      return -1;
    }
    long maxAge = getCacheControlMaxAge();
    if (maxAge != -1) {
      return date + maxAge;
    }
    long expiration = getExpiresTime();
    if (expiration != -1) {
      return expiration;
    }
    return date + defaultTtl;
  }

  public long getRefetchStrictNoCacheAfterMs() {
    return refetchStrictNoCacheAfterMs;
  }

  public boolean shouldRefetch() {
    // Time after which resource should be refetched.
    long refetchExpiration = isStrictNoCache() ?
        date + getRefetchStrictNoCacheAfterMs() : getCacheExpiration();
    return refetchExpiration <= getTimeSource().currentTimeMillis();
  }

  /**
   * @return Consolidated ttl in milliseconds or -1.
   */
  public long getCacheTtl() {
    long expiration = getCacheExpiration();
    if (expiration != -1) {
      return expiration - getTimeSource().currentTimeMillis();
    }
    return -1;
  }

  /**
   * @return True if this result is stale.
   */
  public boolean isStale() {
    return getCacheTtl() <= 0;
  }

  /**
   * @return true if a strict no-cache header is set in Cache-Control or Pragma
   */
  public boolean isStrictNoCache() {
    if (isError() && !NEGATIVE_CACHING_EXEMPT_STATUS.contains(httpStatusCode)) {
      return false;
    }
    String cacheControl = getHeader("Cache-Control");
    if (cacheControl != null) {
      String[] directives = StringUtils.split(cacheControl, ',');
      for (String directive : directives) {
        directive = directive.trim();
        if (directive.equalsIgnoreCase("no-cache")
            || directive.equalsIgnoreCase("no-store")
            || directive.equalsIgnoreCase("private")) {
          return true;
        }
      }
    }

    for (String pragma : getHeaders("Pragma")) {
      if ("no-cache".equalsIgnoreCase(pragma)) {
        return true;
      }
    }
    return false;
  }

  /**
   * @return the expiration time from the Expires header or -1 if not set
   */
  private long getExpiresTime() {
    String expires = getHeader("Expires");
    if (expires != null) {
      Date expiresDate = DateUtil.parseRfc1123Date(expires);
      if (expiresDate != null) {
        return expiresDate.getTime();
      } else {
        // Per RFC2616, 14.21 (http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.21):
        // "HTTP/1.1 clients and caches MUST treat other invalid date formats,
        // especially including the value "0", as in the past (i.e., "already
        // expired")."
        return 0;
      }
    }
    return -1;
  }

  /**
   * @return max-age value or -1 if invalid or not set
   */
  public long getCacheControlMaxAge() {
    String cacheControl = getHeader("Cache-Control");
    if (cacheControl != null) {
      String[] directives = StringUtils.split(cacheControl, ',');
      for (String directive : directives) {
        directive = directive.trim();
        if (directive.startsWith("max-age")) {
          String[] parts = StringUtils.split(directive, '=');
          if (parts.length == 2) {
            try {
              return Long.valueOf(parts[1]) * 1000;
            } catch (NumberFormatException ignore) {
              return -1;
            }
          }
        }
      }
    }
    return -1;
  }

  /**
   * Tries to find a valid date from the input headers.
   *
   * @return The value of the date header, in milliseconds, or -1 if no Date could be determined.
   */
  private static long getAndUpdateDate(Multimap headers) {
    // Validate the Date header. Must conform to the HTTP date format.
    long timestamp = -1;
    long currentTime = getTimeSource().currentTimeMillis();
    Collection dates = headers.get("Date");

    if (!dates.isEmpty()) {
      Date d = DateUtil.parseRfc1123Date(dates.iterator().next());
      if (d != null) {
        timestamp = d.getTime();
      }
    }
    if (timestamp == -1) {
      timestamp = currentTime;
      headers.replaceValues("Date", ImmutableList.of(DateUtil.formatRfc1123Date(timestamp)));
    }
    return timestamp;
  }

  /**
   * returns the default TTL for responses.  Used mainly by tests because Guice static injects TTL values.
   *
   * @return milliseconds of the ttl
   */
  public long getDefaultTtl() {
    return defaultTtl;
  }

  /**
   * Attempts to determine the encoding of the body. If it can't be determined, we use
   * DEFAULT_ENCODING instead.
   *
   * @return The detected encoding or DEFAULT_ENCODING.
   */
  private static Charset getAndUpdateEncoding(Multimap headers, byte[] body) {
    if (body == null || body.length == 0) {
      return DEFAULT_ENCODING;
    }

    Collection values = headers.get("Content-Type");
    if (!values.isEmpty()) {
      String contentType = values.iterator().next();
      String[] parts = StringUtils.split(contentType, ';');
      if (parts == null
          || parts.length == 0
          || BINARY_CONTENT_TYPES.contains(parts[0])) {
        return DEFAULT_ENCODING;
      }
      if (parts.length == 2) {
        int offset = parts[1].toLowerCase().indexOf("charset=");
        if (offset != -1) {
          String charset = parts[1].substring(offset + 8).toUpperCase();
          // Some servers include quotes around the charset:
          //   Content-Type: text/html; charset="UTF-8"
          if (charset.length() >= 2 && charset.startsWith("\"") && charset.endsWith("\"")) {
            charset = charset.substring(1, charset.length() - 1);
          }

          try {
            return charsetForName(charset);
          } catch (IllegalArgumentException e) {
            // fall through to detection
          }
        }
      }
      Charset encoding = EncodingDetector.detectEncoding(body, fastEncodingDetection,
          customEncodingDetector);
      // Record the charset in the content-type header so that its value can be cached
      // and re-used. This is a BIG performance win.
      values.clear();
      values.add(contentType + "; charset=" + encoding.name());

      return encoding;
    } else {
      // If no content type was specified, we'll assume an unknown binary type.
      return DEFAULT_ENCODING;
    }
  }

  /**
   * Cover for Charset.forName() that caches results.
   * @return the charset
   * @throws IllegalArgumentException if the encoding is invalid
   */
  private static Charset charsetForName(String encoding) {
    Charset charset = encodingToCharset.get(encoding);
    if (charset == null) {
      charset = Charset.forName(encoding);
      encodingToCharset.put(encoding, charset);
    }

    return charset;
  }

  @Override
  public int hashCode() {
    return httpStatusCode
      ^ headers.hashCode()
      ^ Arrays.hashCode(responseBytes);
  }

  @Override
  public boolean equals(Object obj) {
    if (obj == this) { return true; }
    if (!(obj instanceof HttpResponse)) { return false; }

    HttpResponse response = (HttpResponse)obj;

    return httpStatusCode == response.httpStatusCode &&
           headers.equals(response.headers) &&
           Arrays.equals(responseBytes, response.responseBytes);
  }

  @Override
  public String toString() {
    StringBuilder buf = new StringBuilder("HTTP/1.1 ").append(httpStatusCode).append("\r\n\r\n");
    for (Map.Entry entry : headers.entries()) {
      buf.append(entry.getKey()).append(": ").append(entry.getValue()).append("\r\n");
    }
    buf.append("\r\n").append(getResponseAsString()).append("\r\n");
    return buf.toString();
  }

  /**
   * @return The response as a byte array. Only visible to the package to avoid copying when
   * making a new HttpResponseBuilder.
   */
  byte[] getResponseAsBytes() {
    return responseBytes;
  }

  /**
   * Expected layout:
   *
   * int - status code
   * Map> - headers
   * int - length of body
   * byte array - body, of previously specified length
   */
  @SuppressWarnings("unchecked")
  public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
    httpStatusCode = in.readInt();

    // We store the multimap as a Map> to insulate us from google-collections API churn
    // And to remain backwards compatible

    Map> headerCopyMap = (Map>)in.readObject();
    Multimap headerCopy = newHeaderMultimap();

    for (Map.Entry> entry : headerCopyMap.entrySet()) {
      headerCopy.putAll(entry.getKey(), entry.getValue());
    }

    int bodyLength = in.readInt();
    responseBytes = new byte[bodyLength];
    int cnt, offset = 0;
    while ((cnt = in.read(responseBytes, offset, bodyLength)) > 0) {
      offset += cnt;
      bodyLength -= cnt;
    }
    if (offset != responseBytes.length) {
      throw new IOException("Invalid body! Expected length = " + responseBytes.length + ", bytes readed = " + offset + '.');
    }

    date = getAndUpdateDate(headerCopy);
    encoding = getAndUpdateEncoding(headerCopy, responseBytes);
    headers = Multimaps.unmodifiableMultimap(headerCopy);
    metadata = Collections.emptyMap();
  }

  public void writeExternal(ObjectOutput out) throws IOException {
    out.writeInt(httpStatusCode);
    // Write out multimap as a map (see above)
    Map> map = Maps.newHashMap();
    for (String key : headers.keySet()) {
      map.put(key, Lists.newArrayList(headers.get(key)));
    }
    out.writeObject(Maps.newHashMap(map));
    out.writeInt(responseBytes.length);
    out.write(responseBytes);
  }


  private static final Supplier> HEADER_COLLECTION_SUPPLIER = new HeaderCollectionSupplier();

  private static class HeaderCollectionSupplier implements Supplier> {
    public Collection get() {
      return new LinkedList();  //To change body of implemented methods use File | Settings | File Templates.
    }
  }
  public static Multimap newHeaderMultimap() {
    TreeMap> map = new TreeMap>(String.CASE_INSENSITIVE_ORDER);
    return Multimaps.newMultimap(map, HEADER_COLLECTION_SUPPLIER);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy