/*
 * 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.
 */


#include "net/instaweb/http/public/http_cache.h"

#include <algorithm>

#include "base/logging.h"
#include "net/instaweb/http/public/http_cache_failure.h"
#include "net/instaweb/http/public/http_value.h"
#include "net/instaweb/http/public/inflating_fetch.h"
#include "pagespeed/kernel/base/basictypes.h"
#include "pagespeed/kernel/base/hasher.h"
#include "pagespeed/kernel/base/message_handler.h"
#include "pagespeed/kernel/base/scoped_ptr.h"
#include "pagespeed/kernel/base/statistics.h"
#include "pagespeed/kernel/base/string.h"
#include "pagespeed/kernel/base/string_util.h"
#include "pagespeed/kernel/base/timer.h"
#include "pagespeed/kernel/cache/cache_interface.h"
#include "pagespeed/kernel/http/content_type.h"
#include "pagespeed/kernel/http/google_url.h"
#include "pagespeed/kernel/http/http_names.h"
#include "pagespeed/kernel/http/response_headers.h"
#include "pagespeed/opt/logging/request_timing_info.h"

namespace net_instaweb {

namespace {

// Increment this value to flush HTTP cache.
// Similar to RewriteOptions::kOptionVersion which can be used to flush the
// metadata cache.
const int kHttpCacheVersion = 3;

// Maximum size of response content in bytes. -1 indicates that there is no size
// limit.
const int64 kCacheSizeUnlimited = -1;

}  // namespace

const char HTTPCache::kCacheTimeUs[] = "cache_time_us";
const char HTTPCache::kCacheHits[] = "cache_hits";
const char HTTPCache::kCacheMisses[] = "cache_misses";
const char HTTPCache::kCacheBackendHits[] = "cache_backend_hits";
const char HTTPCache::kCacheBackendMisses[] = "cache_backend_misses";
const char HTTPCache::kCacheFallbacks[] = "cache_fallbacks";
const char HTTPCache::kCacheExpirations[] = "cache_expirations";
const char HTTPCache::kCacheInserts[] = "cache_inserts";
const char HTTPCache::kCacheDeletes[] = "cache_deletes";

// This used for doing prefix match for etag in fetcher code.
const char HTTPCache::kEtagPrefix[] = "W/\"PSA-";

HTTPCache::HTTPCache(CacheInterface* cache, Timer* timer, Hasher* hasher,
                     Statistics* stats)
    : cache_(cache),
      timer_(timer),
      hasher_(hasher),
      force_caching_(false),
      disable_html_caching_on_https_(false),
      cache_levels_(1),
      compression_level_(0),
      cache_time_us_(stats->GetVariable(kCacheTimeUs)),
      cache_hits_(stats->GetVariable(kCacheHits)),
      cache_misses_(stats->GetVariable(kCacheMisses)),
      cache_backend_hits_(stats->GetVariable(kCacheBackendHits)),
      cache_backend_misses_(stats->GetVariable(kCacheBackendMisses)),
      cache_fallbacks_(stats->GetVariable(kCacheFallbacks)),
      cache_expirations_(stats->GetVariable(kCacheExpirations)),
      cache_inserts_(stats->GetVariable(kCacheInserts)),
      cache_deletes_(stats->GetVariable(kCacheDeletes)),
      name_(FormatName(cache->Name())) {
  max_cacheable_response_content_length_ = kCacheSizeUnlimited;
  SetVersion(kHttpCacheVersion);
}

void HTTPCache::SetVersion(int version_number) {
  version_prefix_ = StrCat("v", IntegerToString(version_number), "/");
}

HTTPCache::~HTTPCache() {}

GoogleString HTTPCache::FormatName(StringPiece cache) {
  return StrCat("HTTPCache(", cache, ")");
}

void HTTPCache::SetIgnoreFailurePuts() {
  ignore_failure_puts_.set_value(true);
}

bool HTTPCache::IsExpired(const ResponseHeaders& headers, int64 now_ms) {
  if (force_caching_) {
    return false;
  }

  if (headers.CacheExpirationTimeMs() > now_ms) {
    return false;
  }
  return true;
}

bool HTTPCache::IsExpired(const ResponseHeaders& headers) {
  return IsExpired(headers, timer_->NowMs());
}

class HTTPCacheCallback : public CacheInterface::Callback {
 public:
  HTTPCacheCallback(const GoogleString& key,
                    const GoogleString& fragment,
                    MessageHandler* handler,
                    HTTPCache::Callback* callback, HTTPCache* http_cache)
      : key_(key),
        fragment_(fragment),
        handler_(handler),
        callback_(callback),
        http_cache_(http_cache),
        result_(HTTPCache::kNotFound, kFetchStatusNotSet),
        cache_level_(0) {
    start_us_ = http_cache_->timer()->NowUs();
    start_ms_ = start_us_ / 1000;
  }

  virtual bool ValidateCandidate(const GoogleString& key,
                                 CacheInterface::KeyState backend_state) {
    ++cache_level_;
    int64 now_us = http_cache_->timer()->NowUs();
    int64 now_ms = now_us / 1000;
    ResponseHeaders* headers = callback_->response_headers();
    bool is_expired = false;
    if ((backend_state == CacheInterface::kAvailable) &&
        callback_->http_value()->Link(value(), headers, handler_) &&
        (http_cache_->force_caching_ ||
         headers->IsProxyCacheable(callback_->req_properties(),
                                   callback_->RespectVaryOnResources(),
                                   ResponseHeaders::kHasValidator)) &&
        callback_->IsCacheValid(key_, *headers) &&
        // To resolve Issue 664 we sanitize 'Connection' headers on
        // HTTPCache::Put, but cache entries written before the bug
        // was fixed may have Connection or Transfer-Encoding so treat
        // unsanitary headers as a MISS.  Note that we could at this
        // point actually write the sanitized headers back into the
        // HTTPValue, or better still the HTTPCache, but the former
        // would be slow, and the latter might be complex for
        // write-throughs.  Simply responding with a MISS will let us
        // correct our caches permanently, without having to do a one
        // time full-flush that would impact clean cache entries.
        // Once the caches are all clean, the Sanitize call will be a
        // relatively fast check.
        !headers->Sanitize()) {
      // While stale responses can potentially be used in case of fetch
      // failures, responses invalidated via a cache flush should never be
      // returned under any scenario.
      // TODO(sriharis) : Should we keep statistic for number of invalidated
      // lookups, i.e., #times IsCacheValid returned false?
      // Check that the response is currently valid and is fresh as per the
      // caller's definition of freshness. For example, if we are using a
      // two-level HTTPCache, while freshening a resource, this ensures that we
      // check both caches and don't return a valid response from the L1 cache
      // that is about to expire soon. We instead also check the L2 cache which
      // could have a fresher response. We don't need to pass request_headers
      // here, as we shouldn't have put things in here that required
      // Authorization in the first place.
      int64 override_cache_ttl_ms = callback_->OverrideCacheTtlMs(key_);
      if (override_cache_ttl_ms > 0) {
        // Use the OverrideCacheTtlMs if specified.
        headers->ForceCaching(override_cache_ttl_ms);
      }
      // Is the response still valid?
      is_expired = http_cache_->IsExpired(*headers, now_ms);
      bool is_valid_and_fresh = !is_expired && callback_->IsFresh(*headers);
      HttpStatus::Code http_status =
          static_cast<HttpStatus::Code>(headers->status_code());

      if (HttpCacheFailure::IsFailureCachingStatus(http_status)) {
        // If the response was stored as uncacheable and a 200, it may since
        // have been added to the override caching group, hence, if
        // override_cache_ttl_ms > 0 we have to disregard the cached failure.
        if (override_cache_ttl_ms > 0 &&
            http_status == HttpStatus::kRememberNotCacheableAnd200StatusCode) {
          is_valid_and_fresh = false;
        }
        if (is_valid_and_fresh) {  // is the failure caching still valid?
          int64 remaining_cache_failure_time_ms =
              headers->CacheExpirationTimeMs() - start_ms_;
          result_ = HTTPCache::FindResult(
              HTTPCache::kRecentFailure,
              HttpCacheFailure::DecodeFailureCachingStatus(http_status));
          if (handler_ != NULL) {
            handler_->Message(kInfo,
                              "HTTPCache key=%s fragment=%s: remembering "
                              "recent failure for %ld seconds.",
                              key_.c_str(), fragment_.c_str(),
                              static_cast<long>(  // NOLINT
                                  remaining_cache_failure_time_ms / 1000));
          }
        }
      } else {
        if (is_valid_and_fresh) {
          result_ = HTTPCache::FindResult(HTTPCache::kFound, kFetchStatusOK);
          callback_->fallback_http_value()->Clear();
          if (headers->UpdateCacheHeadersIfForceCached()) {
            // If the cache headers were updated as a result of it being force
            // cached, we need to reconstruct the HTTPValue with the new
            // headers.
            StringPiece content;
            callback_->http_value()->ExtractContents(&content);
            callback_->http_value()->Clear();
            callback_->http_value()->Write(content, handler_);
            callback_->http_value()->SetHeaders(headers);
          }
        } else {
          if (http_cache_->force_caching_ ||
              headers->IsProxyCacheable(callback_->req_properties(),
                                        callback_->RespectVaryOnResources(),
                                        ResponseHeaders::kHasValidator)) {
            ResponseHeaders fallback_headers;
            if (callback_->request_context()->accepts_gzip() ||
                !callback_->http_value()->ExtractHeaders(&fallback_headers,
                                                         handler_) ||
                !InflatingFetch::UnGzipValueIfCompressed(
                    *callback_->http_value(), &fallback_headers,
                    callback_->fallback_http_value(), handler_)) {
              // If we don't need to unzip, or can't unzip, then just
              // link the value and fallback together.
              callback_->fallback_http_value()->Link(callback_->http_value());
            }
          }
        }
      }
    }

    // TODO(gee): Perhaps all of this belongs in TimingInfo.
    int64 elapsed_us = std::max(static_cast<int64>(0), now_us - start_us_);
    http_cache_->cache_time_us()->Add(elapsed_us);
    callback_->ReportLatencyMs(elapsed_us/1000);
    if (cache_level_ == http_cache_->cache_levels() ||
        result_.status == HTTPCache::kFound) {
      http_cache_->UpdateStats(key_, fragment_, backend_state, result_,
                               !callback_->fallback_http_value()->Empty(),
                               is_expired, handler_);
    }

    if (result_.status != HTTPCache::kFound) {
      headers->Clear();
      callback_->http_value()->Clear();
    } else if (!callback_->request_context()->accepts_gzip() &&
               headers->IsGzipped()) {
      HTTPValue new_value;
      GoogleString inflated;
      if (InflatingFetch::UnGzipValueIfCompressed(
              *callback_->http_value(), headers, &new_value, handler_)) {
        callback_->http_value()->Link(&new_value);
      }
    }
    start_ms_ = now_ms;
    start_us_ = now_us;
    return result_.status == HTTPCache::kFound;
  }

  virtual void Done(CacheInterface::KeyState backend_state) {
    callback_->Done(result_);
    delete this;
  }

 private:
  GoogleString key_;
  GoogleString fragment_;
  RequestHeaders::Properties req_properties_;
  MessageHandler* handler_;
  HTTPCache::Callback* callback_;
  HTTPCache* http_cache_;
  HTTPCache::FindResult result_;
  int64 start_us_;
  int64 start_ms_;
  int cache_level_;

  DISALLOW_COPY_AND_ASSIGN(HTTPCacheCallback);
};

void HTTPCache::Find(const GoogleString& key, const GoogleString& fragment,
                     MessageHandler* handler, Callback* callback) {
  HTTPCacheCallback* cb = new HTTPCacheCallback(
      key, fragment, handler, callback, this);
  cache_->Get(CompositeKey(key, fragment), cb);
}

void HTTPCache::UpdateStats(
    const GoogleString& key, const GoogleString& fragment,
    CacheInterface::KeyState backend_state, FindResult result,
    bool has_fallback, bool is_expired, MessageHandler* handler) {
  if (backend_state == CacheInterface::kAvailable) {
    cache_backend_hits_->Add(1);
  } else {
    cache_backend_misses_->Add(1);
  }
  if (result.status == kFound) {
    cache_hits_->Add(1);
    DCHECK(!has_fallback);
  } else {
    cache_misses_->Add(1);
    if (has_fallback) {
      cache_fallbacks_->Add(1);
    }
    if (is_expired) {
      handler->Message(kInfo, "Cache entry is expired: %s (fragment=%s)",
                       key.c_str(), fragment.c_str());
      cache_expirations_->Add(1);
    }
  }
}

void HTTPCache::set_max_cacheable_response_content_length(int64 value) {
  DCHECK(value >= kCacheSizeUnlimited);
  if (value >= kCacheSizeUnlimited) {
    max_cacheable_response_content_length_ = value;
  }
}

void HTTPCache::RememberFailure(
    const GoogleString& key,
    const GoogleString& fragment,
    FetchResponseStatus failure_status,
    MessageHandler* handler) {
  HttpStatus::Code code = HttpCacheFailure::EncodeFailureCachingStatus(
      failure_status);
  int64 ttl_sec = remember_failure_policy_.ttl_sec_for_status[failure_status];
  ResponseHeaders headers;
  headers.set_status_code(code);
  int64 now_ms = timer_->NowMs();
  headers.SetDateAndCaching(now_ms, ttl_sec * 1000);
  headers.ComputeCaching();
  Put(key, fragment, RequestHeaders::Properties(),
      ResponseHeaders::kRespectVaryOnResources,
      &headers, "", handler);
}

HTTPValue* HTTPCache::ApplyHeaderChangesForPut(
    int64 start_us, const StringPiece* content, ResponseHeaders* headers,
    HTTPValue* value, MessageHandler* handler) {
  if ((headers->status_code() != HttpStatus::kOK) &&
      ignore_failure_puts_.value()) {
    return NULL;
  }
  DCHECK(value != NULL || content != NULL);

  // Clear out Set-Cookie headers before storing the response into cache.
  bool headers_mutated = headers->Sanitize();
  // TODO(sriharis): Modify date headers.
  // TODO(nikhilmadan): Set etags from hash of content.

  StringPiece new_content;

  // Add an Etag if the original response didn't have any.
  if (headers->Lookup1(HttpAttributes::kEtag) == NULL) {
    GoogleString hash;
    if (content == NULL) {
      bool success = value->ExtractContents(&new_content);
      DCHECK(success);
      content = &new_content;
    }
    hash = hasher_->Hash(*content);
    headers->Add(HttpAttributes::kEtag, FormatEtag(hash));
    headers_mutated = true;
  }

  if (headers_mutated || value == NULL) {
    HTTPValue* new_value = new HTTPValue;  // Will be deleted by calling Put.
    new_value->SetHeaders(headers);
    if (content == NULL) {
      bool success = value->ExtractContents(&new_content);
      DCHECK(success);
      new_value->Write(new_content, handler);
    } else {
      new_value->Write(*content, handler);
    }
    return new_value;
  }
  return value;
}

void HTTPCache::PutInternal(bool preserve_response_headers,
                            const GoogleString& key,
                            const GoogleString& fragment, int64 start_us,
                            HTTPValue* value, ResponseHeaders* response_headers,
                            MessageHandler* handler) {
  HTTPValue working_value;

  // Check to see if the HTTPValue is worth gzipping.
  // TODO(jcrowell): investigate switching to mod_gzip from mod_deflate so that
  // we can set some heuristic on minimum size where compressing the data no
  // longer makes filesize smaller. For now, we pay the penalty of compression
  // from mod_deflate if we don't precompress everything, so just compress
  // everything.
  if (!value->Empty() && compression_level_ != 0) {
    const ContentType* type = response_headers->DetermineContentType();
    if ((type != NULL) && type->IsCompressible() &&
        !response_headers->IsGzipped()) {
      ResponseHeaders* headers_to_gzip = response_headers;
      ResponseHeaders headers_copy;
      if (preserve_response_headers) {
        headers_copy.CopyFrom(*response_headers);
        headers_to_gzip = &headers_copy;
      }

      // Canonicalize header order so x-original-content-length is always
      // last.  This helps tests act more consistently.
      const char* orig_content_length = headers_to_gzip->Lookup1(
          HttpAttributes::kXOriginalContentLength);
      if (orig_content_length != NULL) {
        GoogleString save_content_length = orig_content_length;
        headers_to_gzip->RemoveAll(HttpAttributes::kXOriginalContentLength);
        headers_to_gzip->Add(HttpAttributes::kXOriginalContentLength,
                             save_content_length);
      }
      headers_to_gzip->ComputeCaching();

      if (InflatingFetch::GzipValue(compression_level_, *value,
                                    &working_value, headers_to_gzip,
                                    handler)) {
        // The resource is text (js, css, html, svg, etc.), and not previously
        // compressed, so we'll compress it and stick the new compressed version
        // in the cache.
        value = &working_value;
      }
    }
  } else if ((compression_level_ == 0) && response_headers->IsGzipped()) {
    ResponseHeaders* headers_to_unzip = response_headers;
    ResponseHeaders headers_copy;
    if (preserve_response_headers) {
      headers_copy.CopyFrom(*response_headers);
      headers_to_unzip = &headers_copy;
    }

    if (InflatingFetch::UnGzipValueIfCompressed(
            *value, headers_to_unzip, &working_value, handler)) {
      value = &working_value;
    }
  }
  // TODO(jcrowell): prevent the unzip-rezip flow when sending compressed data
  // directly to a client through InflatingFetch.
  cache_->Put(CompositeKey(key, fragment), value->share());
  if (cache_time_us_ != NULL) {
    int64 delta_us = timer_->NowUs() - start_us;
    cache_time_us_->Add(delta_us);
  }
}

// We do not check cache invalidation in Put. It is assumed that the date header
// will be greater than the cache_invalidation_timestamp, if any, in domain
// config.
void HTTPCache::Put(const GoogleString& key, const GoogleString& fragment,
                    RequestHeaders::Properties req_properties,
                    const HttpOptions& http_options,
                    HTTPValue* value, MessageHandler* handler) {
  int64 start_us = timer_->NowUs();
  // Extract headers and contents.
  ResponseHeaders headers(http_options);
  bool success = value->ExtractHeaders(&headers, handler);
  DCHECK(success);
  if (!MayCacheUrl(key, headers)) {
    return;
  }
  if (!force_caching_ &&
      !(headers.IsProxyCacheable(
          req_properties,
          ResponseHeaders::GetVaryOption(http_options.respect_vary),
          ResponseHeaders::kHasValidator) &&
        IsCacheableBodySize(value->contents_size()))) {
    LOG(DFATAL) << "trying to Put uncacheable data for key=" << key
                << " fragment=" << fragment;
    return;
  }
  // Apply header changes.
  HTTPValue* new_value = ApplyHeaderChangesForPut(
      start_us, NULL, &headers, value, handler);
  // Put into underlying cache.
  if (new_value != NULL) {
    PutInternal(false /* preserve_response_headers */,
                key, fragment, start_us, new_value, &headers, handler);
    if (cache_inserts_ != NULL) {
      cache_inserts_->Add(1);
    }

    // Delete new_value if it is newly allocated.
    if (new_value != value) {
      delete new_value;
    }
  }
}

void HTTPCache::Put(const GoogleString& key, const GoogleString& fragment,
                    RequestHeaders::Properties req_properties,
                    ResponseHeaders::VaryOption respect_vary_on_resources,
                    ResponseHeaders* headers,
                    const StringPiece& content, MessageHandler* handler) {
  if (!MayCacheUrl(key, *headers)) {
    return;
  }
  int64 start_us = timer_->NowUs();
  int64 now_ms = start_us / 1000;
  if ((IsExpired(*headers, now_ms) ||
       !headers->IsProxyCacheable(req_properties, respect_vary_on_resources,
                                  ResponseHeaders::kHasValidator) ||
       !IsCacheableBodySize(content.size())) &&
      !force_caching_) {
    return;
  }
  // Apply header changes.
  // Takes ownership of the returned HTTPValue, which is guaranteed to have been
  // allocated by ApplyHeaderChangesForPut.
  scoped_ptr<HTTPValue> value(
      ApplyHeaderChangesForPut(start_us, &content, headers, NULL, handler));
  // Put into underlying cache.
  if (value.get() != NULL) {
    PutInternal(true /* preserve_response_headers */,
                key, fragment, start_us, value.get(), headers, handler);
    if (cache_inserts_ != NULL) {
      cache_inserts_->Add(1);
    }
  }
}

bool HTTPCache::IsCacheableContentLength(ResponseHeaders* headers) const {
  int64 content_length;
  bool content_length_found = headers->FindContentLength(&content_length);
  return (!content_length_found || IsCacheableBodySize(content_length));
}

bool HTTPCache::IsCacheableBodySize(int64 body_size) const {
  return (max_cacheable_response_content_length_ == -1 ||
          body_size <= max_cacheable_response_content_length_);
}

bool HTTPCache::MayCacheUrl(const GoogleString& url,
                            const ResponseHeaders& headers) {
  GoogleUrl gurl(url);
  // TODO(sligocki): Should we restrict this to IsWebValid()?
  // That would break google_font_service_input_resource which uses gfnt:
  if (!gurl.IsAnyValid()) {
    return false;
  }
  if (disable_html_caching_on_https_ && gurl.SchemeIs("https")) {
    return !headers.IsHtmlLike();
  }
  return true;
}

void HTTPCache::Delete(const GoogleString& key, const GoogleString& fragment) {
  cache_deletes_->Add(1);
  DeleteInternal(CompositeKey(key, fragment));
}

void HTTPCache::DeleteInternal(const GoogleString& key_fragment) {
  cache_->Delete(key_fragment);
}

void HTTPCache::InitStats(Statistics* statistics) {
  statistics->AddVariable(kCacheTimeUs);
  statistics->AddVariable(kCacheHits);
  statistics->AddVariable(kCacheMisses);
  statistics->AddVariable(kCacheBackendHits);
  statistics->AddVariable(kCacheBackendMisses);
  statistics->AddVariable(kCacheFallbacks);
  statistics->AddVariable(kCacheExpirations);
  statistics->AddVariable(kCacheInserts);
  statistics->AddVariable(kCacheDeletes);
}

GoogleString HTTPCache::FormatEtag(StringPiece hash) {
  return StrCat(kEtagPrefix, hash, "\"");
}

HTTPCache::Callback::~Callback() {
  if (owns_response_headers_) {
    delete response_headers_;
  }
}

void HTTPCache::Callback::ReportLatencyMs(int64 latency_ms) {
  if (is_background_) {
    return;
  }

  if (request_context().get() == NULL) {
    LOG(DFATAL) << "NOTREACHED";
    return;
  }

  ++cache_level_;
  if (cache_level_ == 1) {
    request_context()->mutable_timing_info()->SetHTTPCacheLatencyMs(latency_ms);
  } else if (cache_level_ == 2) {
    request_context()->mutable_timing_info()->SetL2HTTPCacheLatencyMs(
        latency_ms);
  }
}

}  // namespace net_instaweb
