Package com.google.appengine.tools.development

Source Code of com.google.appengine.tools.development.StaticFileUtils

// Copyright 2009 Google Inc. All rights reserved.

package com.google.appengine.tools.development;

import com.google.apphosting.utils.config.AppEngineWebXml;
import com.google.common.annotations.VisibleForTesting;

import org.mortbay.io.WriterOutputStream;
import org.mortbay.jetty.HttpHeaders;
import org.mortbay.jetty.HttpMethods;
import org.mortbay.resource.Resource;
import org.mortbay.util.URIUtil;

import java.io.IOException;
import java.io.OutputStream;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.servlet.ServletException;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* {@code StaticFileUtils} is a collection of utilities shared by
* {@link LocalResourceFileServlet} and {@link StaticFileFilter}.
*
*/
public class StaticFileUtils {
  private static final String DEFAULT_CACHE_CONTROL_VALUE = "public, max-age=600";

  private final ServletContext servletContext;

  public StaticFileUtils(ServletContext servletContext) {
    this.servletContext = servletContext;
  }

  public boolean serveWelcomeFileAsRedirect(String path,
                                            boolean included,
                                            HttpServletRequest request,
                                            HttpServletResponse response)
      throws IOException {
    if (included) {
      return false;
    }

    response.setContentLength(0);
    String q = request.getQueryString();
    if (q != null && q.length() != 0) {
      response.sendRedirect(path + "?" + q);
    } else {
      response.sendRedirect(path);
    }
    return true;
  }

  public boolean serveWelcomeFileAsForward(RequestDispatcher dispatcher,
                                           boolean included,
                                           HttpServletRequest request,
                                           HttpServletResponse response)
      throws IOException, ServletException {
    if (!included && !request.getRequestURI().endsWith(URIUtil.SLASH)) {
      redirectToAddSlash(request, response);
      return true;
    }

    request.setAttribute("com.google.appengine.tools.development.isWelcomeFile", true);
    if (dispatcher != null) {
      if (included) {
        dispatcher.include(request, response);
      } else {
        dispatcher.forward(request, response);
      }
      return true;
    }
    return false;
  }

  public void redirectToAddSlash(HttpServletRequest request, HttpServletResponse response)
      throws IOException {
    StringBuffer buf = request.getRequestURL();
    int param = buf.lastIndexOf(";");
    if (param < 0) {
      buf.append('/');
    } else {
      buf.insert(param, '/');
    }
    String q = request.getQueryString();
    if (q != null && q.length() != 0) {
      buf.append('?');
      buf.append(q);
    }
    response.setContentLength(0);
    response.sendRedirect(response.encodeRedirectURL(buf.toString()));
  }

  /**
   * Check the headers to see if content needs to be sent.
   * @return true if the content should be sent, false otherwise.
   */
  public boolean passConditionalHeaders(HttpServletRequest request,
                                         HttpServletResponse response,
                                         Resource resource) throws IOException {
    if (!request.getMethod().equals(HttpMethods.HEAD)) {
      String ifms = request.getHeader(HttpHeaders.IF_MODIFIED_SINCE);
      if (ifms != null) {
        long ifmsl = -1;
        try {
          ifmsl = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE);
        } catch (IllegalArgumentException e) {
        }
        if (ifmsl != -1) {
          if (resource.lastModified() <= ifmsl) {
            response.reset();
            response.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
            response.flushBuffer();
            return false;
          }
        }
      }

      long date = -1;
      try {
        date = request.getDateHeader(HttpHeaders.IF_UNMODIFIED_SINCE);
      } catch (IllegalArgumentException e) {
      }
      if (date != -1) {
        if (resource.lastModified() > date) {
          response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED);
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Write or include the specified resource.
   */
  public void sendData(HttpServletRequest request,
                        HttpServletResponse response,
                        boolean include,
                        Resource resource) throws IOException {
    long contentLength = resource.length();
    if (!include) {
      writeHeaders(response, request.getRequestURI(), resource, contentLength);
    }

    OutputStream out = null;
    try {
      out = response.getOutputStream();}
    catch (IllegalStateException e) {
      out = new WriterOutputStream(response.getWriter());
    }
    resource.writeTo(out, 0, contentLength);
  }

  /**
   * Write the headers that should accompany the specified resource.
   */
  public void writeHeaders(HttpServletResponse response, String requestPath, Resource resource,
                           long count) {
    if (count != -1) {
      if (count < Integer.MAX_VALUE) {
        response.setContentLength((int) count);
      } else {
        response.setHeader(HttpHeaders.CONTENT_LENGTH, String.valueOf(count));
      }
    }

    Set<String> headersApplied = addUserStaticHeaders(requestPath, response);

    if (!headersApplied.contains("content-type")) {
      String contentType = servletContext.getMimeType(resource.getName());
      if (contentType != null) {
        response.setContentType(contentType);
      }
    }

    if (!headersApplied.contains("last-modified")) {
      response.setDateHeader(HttpHeaders.LAST_MODIFIED, resource.lastModified());
    }

    if (!headersApplied.contains(HttpHeaders.CACHE_CONTROL.toLowerCase())) {
      response.setHeader(HttpHeaders.CACHE_CONTROL, DEFAULT_CACHE_CONTROL_VALUE);
    }
  }

  /**
   * Adds HTTP Response headers that are specified in appengine-web.xml. The user may specify
   * headers explicitly using the {@code http-header} element. Also the user may specify cache
   * expiration headers implicitly using the {@code expiration} attribute. There is no check for
   * consistency between different specified headers.
   *
   * @param localFilePath The path to the static file being served.
   * @param response The HttpResponse object to which headers will be added
   * @return The Set of the names of all headers that were added, canonicalized to lower case.
   */
  @VisibleForTesting
  Set<String> addUserStaticHeaders(String localFilePath,
                                     HttpServletResponse response) {
    AppEngineWebXml appEngineWebXml = (AppEngineWebXml) servletContext.getAttribute(
        "com.google.appengine.tools.development.appEngineWebXml");

    Set<String> headersApplied = new HashSet<String>();
    for (AppEngineWebXml.StaticFileInclude include : appEngineWebXml.getStaticFileIncludes()) {
      Pattern pattern = include.getRegularExpression();
      if (pattern.matcher(localFilePath).matches()) {
        for (Map.Entry<String, String> entry : include.getHttpHeaders().entrySet()) {
          response.addHeader(entry.getKey(), entry.getValue());
          headersApplied.add(entry.getKey().toLowerCase());
        }
        String expirationString = include.getExpiration();
        if (expirationString != null) {
          addCacheControlHeaders(headersApplied, expirationString, response);
        }
        break;
      }
    }
    return headersApplied;
  }

  /**
   * Adds HTTP headers to the response to describe cache expiration behavior, based on the
   * {@code expires} attribute of the {@code includes} element of the {@code static-files} element
   * of appengine-web.xml.
   * <p>
   * We follow the same logic that is used in production App Engine. This includes:
   * <ul>
   * <li>There is no coordination between these headers (implied by the 'expires' attribute) and
   * explicitly specified headers (expressed with the 'http-header' sub-element). If the user
   * specifies contradictory headers then we will include contradictory headers.
   * <li>If the expiration time is zero then we specify that the response should not be cached using
   * three different headers: {@code Pragma: no-cache}, {@code Expires: 0} and
   * {@code Cache-Control: no-cache, must-revalidate}.
   * <li>If the expiration time is positive then we specify that the response should be cached for
   * that many seconds using two different headers: {@code Expires: num-seconds} and
   * {@code Cache-Control: public, max-age=num-seconds}.
   * <li>If the expiration time is not specified then we use a default value of 10 minutes
   * </ul>
   *
   * Note that there is one aspect of the production App Engine logic that is not replicated here.
   * In production App Engine if the url to a static file is protected by a security constraint in
   * web.xml then {@code Cache-Control: private} is used instead of {@code Cache-Control: public}.
   * In the development App Server {@code Cache-Control: public} is always used.
   * <p>
   * Also if the expiration time is specified but cannot be parsed as a non-negative number of
   * seconds then a RuntimeException is thrown.
   *
   * @param headersApplied Set of headers that have been applied, canonicalized to lower-case. Any
   *        new headers applied in this method will be added to the set.
   * @param expiration The expiration String specified in appengine-web.xml
   * @param response The HttpServletResponse into which we will write the HTTP headers.
   */
  private static void addCacheControlHeaders(
      Set<String> headersApplied, String expiration, HttpServletResponse response) {

    int expirationSeconds = parseExpirationSpecifier(expiration);
    if (expirationSeconds == 0) {
      response.addHeader("Pragma", "no-cache");
      response.addHeader(HttpHeaders.CACHE_CONTROL, "no-cache, must-revalidate");
      response.addDateHeader(HttpHeaders.EXPIRES, 0);
      headersApplied.add(HttpHeaders.CACHE_CONTROL.toLowerCase());
      headersApplied.add(HttpHeaders.EXPIRES.toLowerCase());
      headersApplied.add("pragma");
      return;
    }
    if (expirationSeconds > 0) {
      response.addHeader(HttpHeaders.CACHE_CONTROL, "public, max-age=" + expirationSeconds);
      response.addDateHeader(
          HttpHeaders.EXPIRES, System.currentTimeMillis() + expirationSeconds * 1000L);
      headersApplied.add(HttpHeaders.CACHE_CONTROL.toLowerCase());
      headersApplied.add(HttpHeaders.EXPIRES.toLowerCase());
      return;
    }
    throw new RuntimeException("expirationSeconds is negative: " + expirationSeconds);
  }

  /**
   * Parses an expiration specifier String and returns the number of seconds it represents. A valid
   * expiration specifier is a white-space-delimited list of components, each of which is a sequence
   * of digits, optionally followed by a single letter from the set {D, d, H, h, M, m, S, s}. For
   * example {@code 21D 4H 30m} represents the number of seconds in 21 days, 4.5 hours.
   *
   * @param expirationSpecifier The non-null, non-empty expiration specifier String to parse
   * @return The non-negative number of seconds represented by this String.
   */
  @VisibleForTesting
  static int parseExpirationSpecifier(String expirationSpecifier) {
    expirationSpecifier = expirationSpecifier.trim();
    if (expirationSpecifier.isEmpty()) {
      throwExpirationParseException("", expirationSpecifier);
    }
    String[] components = expirationSpecifier.split("(\\s)+");
    int expirationSeconds = 0;
    for (String componentSpecifier : components) {
      expirationSeconds +=
          parseExpirationSpeciferComponent(componentSpecifier, expirationSpecifier);
    }
    return expirationSeconds;
  }

  private static final Pattern EXPIRATION_COMPONENT_PATTERN = Pattern.compile("^(\\d+)([dhms]?)$");

  /**
   * Parses a single component of an expiration specifier, and returns the number of seconds that
   * the component represents. A valid component specifier is a sequence of digits, optionally
   * followed by a single letter from the set {D, d, H, h, M, m, S, s}, indicating days, hours,
   * minutes and seconds. A lack of a trailing letter is interpreted as seconds.
   *
   * @param componentSpecifier The component specifier to parse
   * @param fullSpecifier The full specifier of which {@code componentSpecifier} is a component.
   *        This will be included in an error message if necessary.
   * @return The number of seconds represented by {@code componentSpecifier}
   */
  private static int parseExpirationSpeciferComponent(
      String componentSpecifier, String fullSpecifier) {
    Matcher matcher = EXPIRATION_COMPONENT_PATTERN.matcher(componentSpecifier.toLowerCase());
    if (!matcher.matches()) {
      throwExpirationParseException(componentSpecifier, fullSpecifier);
    }
    String numericString = matcher.group(1);
    int numSeconds = parseExpirationInteger(numericString, componentSpecifier, fullSpecifier);
    String unitString = matcher.group(2);
    if (unitString.length() > 0) {
      switch (unitString.charAt(0)) {
        case 'd':
          numSeconds *= 24 * 60 * 60;
          break;
        case 'h':
          numSeconds *= 60 * 60;
          break;
        case 'm':
          numSeconds *= 60;
          break;
      }
    }
    return numSeconds;
  }

  /**
   * Parses a String from an expiration specifier as a non-negative integer. If successful returns
   * the integer. Otherwise throws an {@link IllegalArgumentException} indicating that the specifier
   * could not be parsed.
   *
   * @param intString String to parse
   * @param componentSpecifier The component of the specifier being parsed
   * @param fullSpecifier The full specifier
   * @return The parsed integer
   */
  private static int parseExpirationInteger(
      String intString, String componentSpecifier, String fullSpecifier) {
    int seconds = 0;
    try {
      seconds = Integer.parseInt(intString);
    } catch (NumberFormatException e) {
      throwExpirationParseException(componentSpecifier, fullSpecifier);
    }
    if (seconds < 0) {
      throwExpirationParseException(componentSpecifier, fullSpecifier);
    }
    return seconds;
  }

  /**
   * Throws an {@link IllegalArgumentException} indicating that an expiration specifier String was
   * not able to be parsed.
   *
   * @param componentSpecifier The component that could not be parsed
   * @param fullSpecifier The full String
   */
  private static void throwExpirationParseException(
      String componentSpecifier, String fullSpecifier) {
    throw new IllegalArgumentException("Unable to parse cache expiration specifier '"
        + fullSpecifier + "' at component '" + componentSpecifier + "'");
  }

}
TOP

Related Classes of com.google.appengine.tools.development.StaticFileUtils

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.