/* Copyright (c) 2002, Oracle Corporation. All rights reserved. */

package oracle.portal.provider.v2.cache.esi;

import java.security.InvalidKeyException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import oracle.portal.provider.v2.PortletConstants;
import oracle.portal.provider.v2.render.PortletRenderRequest;
import oracle.portal.provider.v2.http.HttpCommonConstants;
import oracle.portal.utils.NameValue;
import oracle.webdb.utils.WebCacheUtils;
import oracle.webdb.utils.SimpleStringBuffer;
import oracle.webdb.utils.HmacAuthenticater;

// Logging
import oracle.portal.log.LogManager;

/**
 * Contains utility methods and constants for including and caching 
 * ESI fragments.
 */
public class ESIUtils
{
    /**
     * Names of the headers sent with the ESI fragment HTTP request
     * when calling generatePortalESIInclude and returned with the
     * ESI fragment servlet response when calling setPortalESIResponseHeaders.
     */
    private static final String ORACLE_PROVIDER_ESI_LANG_HEADER = 
        "X-ORACLE-PROVIDER-ESI-LANG";
    private static final String ORACLE_PROVIDER_ESI_SUBID_HEADER = 
        "X-ORACLE-PROVIDER-ESI-SUBID";
    private static final String ORACLE_PROVIDER_ESI_DEVICE_HEADER = 
        "X-ORACLE-PROVIDER-ESI-DEVICE";
    private static final String ORACLE_PROVIDER_ESI_USER_HEADER = 
        "X-ORACLE-PROVIDER-ESI-USER";
    private static final String ORACLE_PROVIDER_ESI_AUTH_HEADER = 
        "X-ORACLE-PROVIDER-ESI-AUTH";
    private static final String ORACLE_PROVIDER_ESI_ENCRYPT_HEADER = 
        "X-ORACLE-PROVIDER-ESI-ENCRYPT";

    private static final int NUM_ORACLE_PROVIDER_ESI_HEADERS = 6;

    /**
     * The vary directive returned as a servlet response header when calling 
     * generatePortalESIInclude.
     */
    private static final String ORACLE_PROVIDER_ESI_VARY_DIRECTIVE = 
      "vary=headers(" + 
      ORACLE_PROVIDER_ESI_USER_HEADER    + 
          WebCacheUtils.SURROGATE_CONTROL_FORCE_FLAG_WHITE_SPACE +
      ORACLE_PROVIDER_ESI_SUBID_HEADER   + 
          WebCacheUtils.SURROGATE_CONTROL_FORCE_FLAG_WHITE_SPACE +
      ORACLE_PROVIDER_ESI_AUTH_HEADER    + 
          WebCacheUtils.SURROGATE_CONTROL_FORCE_FLAG_WHITE_SPACE +
      ORACLE_PROVIDER_ESI_DEVICE_HEADER  + 
          WebCacheUtils.SURROGATE_CONTROL_FORCE_FLAG_WHITE_SPACE +
      ORACLE_PROVIDER_ESI_ENCRYPT_HEADER + 
          WebCacheUtils.SURROGATE_CONTROL_FORCE_FLAG_WHITE_SPACE +
      ORACLE_PROVIDER_ESI_LANG_HEADER + "/f)";

    /**
     * Generates an ESI include directive that can be inserted into
     * a portlet's rendered HTML.
     *
     * <p>This method allows for complete control of all the attributes
     * that make up an ESI include directive.
     *
     * <p>The ESI include directive is placed within an ESI try block and will
     * have one of the two following formats:
     * <ul>
     * <li> 
     * <pre>
     * &lt;esi:try&gt;
     *   &lt;esi:attempt&gt;
     *     &lt;esi:include src="URL_fragment" 
     *       [max-age="expiration_time [+ removal_time]]" [method="GET|POST"] 
     *       [onerror="continue"] [timeout="fetch_time"]/&gt;
     *   &lt;/esi:attempt&gt;
     *   &lt;esi:except&gt;
     *     Time Out Message
     *   &lt;/esi:except&gt;
     * &lt;/esi:try&gt;
     * </pre>
     * </li>
     * <li>
     * <pre>
     * &lt;esi:try&gt;
     *   &lt;esi:attempt&gt;
     *     &lt;esi:include src="URL_fragment" 
     *       [max-age="expiration_time [+ removal_time]"] [method="GET|POST"] 
     *       [onerror="continue"] [timeout="fetch_time"]&gt;
     *       [&lt;esi:request_header name="request_header" value="value"/&gt;
     *       [&lt;esi:request_body value="value"/&gt;
     *     &lt;/esi:include&gt;
     *   &lt;/esi:attempt&gt;
     *   &lt;esi:except&gt;
     *     Time Out Message
     *   &lt;/esi:except&gt;
     * &lt;/esi:try&gt;
     * </pre>
     * </li>
     * </ul>
     *
     * <p>See the <i>
     * OracleAS Web Cache Administration and Deployment Guide</i> for a more
     * in-depth discussion of ESI and the ESI include directive.
     *
     * @param url the URL of the ESI fragment
     * @param expirationTimeInSeconds time, in seconds, to expire the fragment.
     *        The maxage attribute is added to the ESI include directive only
     *        if the expirition time is greater than 0.
     *        If not added then the expiration time specified in the ESI
     *        response will be used.
     *        Submit a positive expiration time if the including page has a 
     *        higher tolerance for stale fragments than specified by the 
     *        expiration time parameters in fragment responses. 
     *        If max-age is set in both the ESI request
     *        and the ESI response then Webcache uses the greater of the two.
     *        Oracle Corporation recommends setting the max-age attribute in
     *        the ESI request to a longer time than the max-age control
     *        directive in the ESI response.
     * @param removalTimeInSeconds optional, the time, in seconds, to remove the
     *        fragment after the expiration time. 
     *        A removal time less than or equal to 0 will not be added to the
     *        ESI request.
     *        Webcache uses a heuristic to remove a cached entry some time 
     *        after the end of the expiration period and before the end of the 
     *        removal period.
     * @param method the HTTP method used - either "GET" or "POST".
     *        If method is not set, i.e. is null or of 0 length, then GET 
     *        is assumed. However, if a valid queryString (non-null and of 
     *        non-zero length) is sent, then POST is assumed.
     * @param queryString the body of the HTTP request that fetches the ESI
     *        fragment. Ignored if null or length is 0.
     * @param onErrorContinue if true then the onerror="continue" attribute
     *        is added to the ESI include directive which directs Webcache, 
     *        in the case of a failed fetch on the ESI fragment, 
     *        to ignore the ESI tag and serve the page.
     * @param timeoutInSeconds if the fragment has not been fetched within this 
     *        timeout interval, then the fetch is aborted.
     *        Timeout is added to the ESI include only if greater than 0.
     * @param timeoutMessage the message rendered if the fetch times out.
     * @param esiRequestHeaders the headers sent with the HTTP request that
     *        fetches the ESI fragment.
     * @return a string representation of the ESI include directive.
     */
    public static String generateESIInclude(
        String               url, 
        int                  expirationTimeInSeconds,
        int                  removalTimeInSeconds,
        String               method,
        String               queryString,
        boolean              onErrorContinue, 
        int                  timeoutInSeconds, 
        String               timeoutMessage,
        NameValue            esiRequestHeaders[])
    {
      // Call the shared ESI include directive generator.
      return WebCacheUtils.generateESIInclude(url, 
          expirationTimeInSeconds, removalTimeInSeconds, method, queryString,
          onErrorContinue, timeoutInSeconds, timeoutMessage, esiRequestHeaders);
    }

    /**
     * Generates an ESI include directive which uses Portal portlet
     * render request headers to cache the ESI fragment at either the 
     * Portal user or Portal system level.
     *
     * <p>This method allows for complete control of all the attributes
     * that make up an ESI include directive except for the ESI request
     * headers. By specifying a cache level using the <b>cachelevel</b> 
     * parameter, Portal request headers will sent with the ESI fragment 
     * HTTP request resulting in the ESI fragment being cached at either
     * the Portal user level or the Portal system level.
     *
     * <p>The ESI include directive is placed within an ESI try block and will
     * have one of the two following formats:
     * <ul>
     * <li> 
     * <pre>
     * &lt;esi:try&gt;
     *   &lt;esi:attempt&gt;
     *     &lt;esi:include src="URL_fragment" 
     *       [max-age="expiration_time [+ removal_time]]" [method="GET|POST"] 
     *       [onerror="continue"] [timeout="fetch_time"]/&gt;
     *     &lt;/esi:attempt&gt;
     *   &lt;esi:except&gt;
     *     Time Out Message
     *   &lt;/esi:except&gt;
     * &lt;/esi:try&gt;
     * </pre>
     * </li>
     * <li>
     * <pre>
     * &lt;esi:try&gt;
     *   &lt;esi:attempt&gt;
     *     &lt;esi:include src="URL_fragment" 
     *       [max-age="expiration_time [+ removal_time]"] [method="GET|POST"] 
     *       [onerror="continue"] [timeout="fetch_time"]&gt;
     *       [&lt;esi:request_header name="request_header" value="value"/&gt;
     *       [&lt;esi:request_body value="value"/&gt;
     *     &lt;/esi:include&gt;
     *   &lt;/esi:attempt&gt;
     *   &lt;esi:except&gt;
     *       Time Out Message
     *   &lt;/esi:except&gt;
     * &lt;/esi:try&gt;
     * </pre>
     * </li>
     * </ul>
     *
     * <p>See the <i> 
     * OracleAS Web Cache Administration and Deployment Guide</i> for a more
     * in-depth discussion of ESI and the ESI include directive.
     *
     * @param url the URL of the ESI fragment
     * @param expirationTimeInSeconds time, in seconds, to expire the fragment.
     *        The maxage attribute is added to the ESI include directive only
     *        if the expirition time is greater than 0.
     *        If not added then the expiration time specified in the ESI
     *        response will be used.
     *        Submit a positive expiration time if the including page has a 
     *        higher tolerance for stale fragments than specified by the 
     *        expiration time parameters in fragment responses. 
     *        If max-age is set in both the ESI request
     *        and the ESI response then Webcache uses the greater of the two.
     *        Oracle Corporation recommends setting the max-age attribute in
     *        the ESI request to a longer time than the max-age control
     *        directive in the ESI response.
     * @param removalTimeInSeconds optional, the time, in seconds, to remove the
     *        fragment after the expiration time. 
     *        A removal time less than or equal to 0 will not be added to the
     *        ESI request.
     *        Webcache uses a heuristic to remove a cached entry some time 
     *        after the end of the expiration period and before the end of the 
     *        removal period.
     * @param method the HTTP method used - either "GET" or "POST".
     *        If method is not set, i.e. is null or of 0 length, then GET 
     *        is assumed. However, if a valid queryString (non-null and of 
     *        non-zero length) is sent, then POST is assumed.
     * @param queryString the body of the HTTP request that fetches the ESI
     *        fragment. Ignored if null or length is 0.
     * @param onErrorContinue if true then the onerror="continue" attribute
     *        is added to the ESI include directive which directs Webcache, 
     *        in the case of a failed fetch on the ESI fragment, 
     *        to ignore the ESI tag and serve the page.
     * @param timeoutInSeconds if the fragment has not been fetched within this 
     *        timeout interval, then the fetch is aborted.
     *        Timeout is added to the ESI include only if greater than 0.
     * @param timeoutMessage the message rendered if the fetch times out.
     * @param esiRequestHeaders the headers sent with the HTTP request that
     *        fetches the ESI fragment.
     * @param cacheEncryptionKey a string of a least 20 bytes in length that
     *        is used to encrypt the Portal cache key header
     *        (a composite of several non-encrypted Portal headers).
     *        If not supplied or if not valid, i.e. less than 20 bytes in 
     *        length, then the cache key is not encrypted.
     * @return a string representation of the ESI include directive.
     */
    public static String generatePortalESIInclude(
        PortletRenderRequest prr, 
        String               url, 
        int                  expirationTimeInSeconds,
        int                  removalTimeInSeconds,
        String               method,
        String               queryString,
        boolean              onErrorContinue, 
        int                  timeoutInSeconds, 
        String               timeoutMessage,
        int                  cacheLevel,
        String               cacheEncryptionKey)
    {
      // Get the servlet request object.
      HttpServletRequest hsr = (HttpServletRequest)
          prr.getAttribute(HttpCommonConstants.SERVLET_REQUEST);

      // Get the headers that are independent of system or user level caching.
      String language = hsr.getHeader(WebCacheUtils.ORACLE_CACHE_KEY_LANG);
      String subID = hsr.getHeader(WebCacheUtils.ORACLE_CACHE_KEY_SUBID);
      String device = hsr.getHeader(WebCacheUtils.ORACLE_CACHE_KEY_DEVICE);

      // Construct an array containing Oracle Web Provider V2 ESI names and
      // their corresponding Oracle Portal header values.
      NameValue[] esiRequestHeaders = 
          new NameValue[NUM_ORACLE_PROVIDER_ESI_HEADERS];

      // Add the language, subscriber ID, and device headers that were passed
      // with the portlet render request except use a different header name
      // for the ESI fragment request.
      esiRequestHeaders[0] = 
          new NameValue(ORACLE_PROVIDER_ESI_LANG_HEADER, language);
      esiRequestHeaders[1] = 
          new NameValue(ORACLE_PROVIDER_ESI_SUBID_HEADER, subID);
      esiRequestHeaders[2] = 
          new NameValue(ORACLE_PROVIDER_ESI_DEVICE_HEADER, device);

      if (cacheLevel == PortletConstants.CACHE_LEVEL_SYSTEM)
      {
        // The ESI fragment is to be cached at the Portal system level so pass
        // the public user and public authentication level with the 
        // ESI fragment request.
        esiRequestHeaders[3] = new NameValue(ORACLE_PROVIDER_ESI_USER_HEADER, 
            WebCacheUtils.ORACLE_CACHE_VALUE_PUBLIC_USER);
        esiRequestHeaders[4] = new NameValue(ORACLE_PROVIDER_ESI_AUTH_HEADER, 
            WebCacheUtils.ORACLE_CACHE_VALUE_PUBLIC_AUTH);

        // Construct an encrypted cache key based on the public
        // user information.
        SimpleStringBuffer unencryptedKey = new SimpleStringBuffer(200);
        unencryptedKey.append(WebCacheUtils.ORACLE_CACHE_VALUE_PUBLIC_USER)
                      .append(subID)
                      .append(WebCacheUtils.ORACLE_CACHE_VALUE_PUBLIC_AUTH)
                      .append(device)
                      .append(language);

        // Try to encrypt the cache key.
        String encryptedKey;

        // Get the cache encryption key. If not given then don't encrypt.
        if (cacheEncryptionKey == null || cacheEncryptionKey.length() == 0)
        {
          encryptedKey = unencryptedKey.toString();
        }
        else
        {
          try
          {
            encryptedKey = HmacAuthenticater.getChecksum(cacheEncryptionKey,
                new String[]{unencryptedKey.toString()});
          }
          catch (InvalidKeyException ike)
          {
            // If the encryption fails use a non-encrypted key.
            LogManager.warning("Invalid encryption key, returning non-encrypted cache key");
            encryptedKey = unencryptedKey.toString();
          }
          catch (Exception e)
          {
            // If the encryption fails use a non-encrypted key.
            LogManager.warning(
              "Unknown Error with Hmac, using non-encrypted cache key");
            encryptedKey = unencryptedKey.toString();
          }
        }

        // Add the cache key to the set of headers sent with the ESI
        // fragment request.
        esiRequestHeaders[5] = new NameValue(ORACLE_PROVIDER_ESI_ENCRYPT_HEADER,
            encryptedKey);
      }
      else
      {
        // The ESI fragment is to be cached at the Portal user level so pass
        // with the ESI fragment request the user, authentication level, 
        // and encrypted key sent with the portlet render request.
        esiRequestHeaders[3] = new NameValue(ORACLE_PROVIDER_ESI_USER_HEADER, 
            hsr.getHeader(WebCacheUtils.ORACLE_CACHE_KEY_USER));
        esiRequestHeaders[4] = new NameValue(ORACLE_PROVIDER_ESI_AUTH_HEADER, 
            hsr.getHeader(WebCacheUtils.ORACLE_CACHE_KEY_AUTH));
        esiRequestHeaders[5] = new NameValue(ORACLE_PROVIDER_ESI_ENCRYPT_HEADER,
            hsr.getHeader(WebCacheUtils.ORACLE_CACHE_KEY_ENCRYPT));
      }

      // Call the shared ESI include directive generator.
      return WebCacheUtils.generateESIInclude(url, 
          expirationTimeInSeconds, removalTimeInSeconds, method, queryString,
          onErrorContinue, timeoutInSeconds, timeoutMessage, esiRequestHeaders);
    }

    /**
     * Sets the servlet reponse headers for an ESI fragment using
     * Portal specific headers to cache the ESI fragment at either the Portal
     * user or Portal system level.
     *
     * <p>Use this method to have an ESI fragment cached at the Portal user or
     * Portal system level. Even though the including page may be cached at the
     * same level as the ESI fragment it may have a different expiry time.
     *
     * <p>See the <i>
     * OracleAS Web Cache Administration and Deployment Guide</i> for a more
     * in-depth discussion of ESI and the ESI include directive.
     *
     * @param response the servlet response object
     * @param expirationTimeInSeconds time, in seconds, to expire the fragment.
     *        The maxage attribute is added to the ESI response directive only
     *        if the expirition time is greater than 0.
     *        If not added then the expiration time specified in the ESI
     *        request will be used.
     *        Submit a positive expiration time if the including page has a 
     *        higher tolerance for stale fragments than specified by the 
     *        expiration time parameters in fragment responses. 
     *        If max-age is set in both the ESI request
     *        and the ESI response then Webcache uses the greater of the two.
     *        Oracle Corporation recommends setting the max-age attribute in
     *        the ESI request to a longer time than the max-age control
     *        directive in the ESI response.
     * @param removalTimeInSeconds optional, the time, in seconds, to remove the
     *        fragment after the expiration time. 
     *        A removal time less than or equal to 0 will not be added to the
     *        ESI response.
     * @param varyHeaders specifies the headers to be used for caching
     *        the ESI fragment
     */
    public static void setESIResponseHeaders(
        HttpServletResponse response,
        int expirationTimeInSeconds, 
        int removalTimeInSeconds, 
        String[] varyHeaders)
    {
        String maxAgeDirective = WebCacheUtils.getResponseMaxAgeDirective(
            expirationTimeInSeconds, removalTimeInSeconds);

        SimpleStringBuffer responseHeader = 
            new SimpleStringBuffer(maxAgeDirective);

        responseHeader.append(WebCacheUtils.COMMA_WHITE_SPACE)
                     .append(WebCacheUtils.SURROGATE_CONTROL_CONTENT_DIRECTIVE);

        if (varyHeaders!=null && varyHeaders.length > 0)
        {
          responseHeader.append(WebCacheUtils.COMMA_WHITE_SPACE)
                        .append(", vary=headers(");

          for (int i = 0; i < varyHeaders.length; i++)
          {
            responseHeader.append(varyHeaders[i]);
            if (i < varyHeaders.length - 1)
            {
              responseHeader.append(
                  WebCacheUtils.SURROGATE_CONTROL_FORCE_FLAG_WHITE_SPACE);
            }
          }

          responseHeader.append(
              WebCacheUtils.SURROGATE_CONTROL_FORCE_FLAG_CLOSE_PAREN);
        }

        response.setHeader(WebCacheUtils.SURROGATE_CONTROL_HDR, 
            responseHeader.toString());
    }

    /**
     * Sets the servlet reponse headers for an ESI fragment using
     * Portal specific headers to cache the ESI fragment at either the Portal
     * user or Portal system level.
     *
     * <p>Use this method to have an ESI fragment cached at the Portal user or
     * Portal system level. Even though the including page may be cached at the
     * same level as the ESI fragment it may have a different expiry time.
     *
     * <p>See the <a href="http://iasdocs.us.oracle.com/iasdl/doc/caching.902/a95404/esi.htm#620714">
     * OracleAS Web Cache Administration and Deployment Guide</a> for a more
     * in-depth discussion of ESI and the ESI include directive.
     *
     * @param response the servlet response object
     * @param expirationTimeInSeconds time, in seconds, to expire the fragment.
     *        The maxage attribute is added to the ESI response directive only
     *        if the expirition time is greater than 0.
     *        If not added then the expiration time specified in the ESI
     *        request will be used.
     *        Submit a positive expiration time if the including page has a 
     *        higher tolerance for stale fragments than specified by the 
     *        expiration time parameters in fragment responses. 
     *        If max-age is set in both the ESI request
     *        and the ESI response then Webcache uses the greater of the two.
     *        Oracle Corporation recommends setting the max-age attribute in
     *        the ESI request to a longer time than the max-age control
     *        directive in the ESI response.
     * @param removalTimeInSeconds optional, the time, in seconds, to remove the
     *        fragment after the expiration time. 
     *        A removal time less than or equal to 0 will not be added to the
     *        ESI response.
     */
    public static void setPortalESIResponseHeaders(
        HttpServletResponse response, 
        int expirationTimeInSeconds, 
        int removalTimeInSeconds)
    {
      // Get the max-age directive.
      String maxAgeDirective = WebCacheUtils.getResponseMaxAgeDirective(
          expirationTimeInSeconds, removalTimeInSeconds);

      // Start construct the response header value by adding max-age directive.
      SimpleStringBuffer responseHeader = 
          new SimpleStringBuffer(maxAgeDirective);

      responseHeader.append(", ")
                    .append(ORACLE_PROVIDER_ESI_VARY_DIRECTIVE);

      // Set the Surrogate-control header in the servlet response.
      response.setHeader(WebCacheUtils.SURROGATE_CONTROL_HDR, 
          responseHeader.toString());
    }

}
