text: # Copyright 2013 The Chromium Authors. All rights reserved.
  # Use of this source code is governed by a BSD-style license that can be
  # found in the LICENSE file.
  
  from datetime import datetime, timedelta
  from file_system import FileNotFoundError, ToUnicode
  from future import Future
  from patcher import Patcher
  
  _VERSION_CACHE_MAXAGE = timedelta(seconds=5)
  
  ''' Append @version for keys to distinguish between different patchsets of
  an issue.
  '''
  def _MakeKey(path, version):
    return '%s@%s' % (path, version)
  
  def _ToObjectStoreValue(raw_value, version):
    return dict((_MakeKey(key, version), raw_value[key])
                for key in raw_value)
  
  def _FromObjectStoreValue(raw_value, binary):
    return dict((key[0:key.rfind('@')], _HandleBinary(raw_value[key], binary))
                for key in raw_value)
  
  def _HandleBinary(data, binary):
    return data if binary else ToUnicode(data)
  
  class _AsyncUncachedFuture(object):
    def __init__(self,
                 version,
                 paths,
                 binary,
                 cached_value,
                 missing_paths,
                 fetch_delegate,
                 object_store):
      self._version = version
      self._paths = paths
      self._binary = binary
      self._cached_value = cached_value
      self._missing_paths = missing_paths
      self._fetch_delegate = fetch_delegate
      self._object_store = object_store
  
    def Get(self):
      uncached_raw_value = self._fetch_delegate.Get()
      self._object_store.SetMulti(_ToObjectStoreValue(uncached_raw_value,
                                                      self._version))
  
      for path in self._missing_paths:
        if uncached_raw_value.get(path) is None:
          raise FileNotFoundError('File %s was not found in the patch.' % path)
        self._cached_value[path] = _HandleBinary(uncached_raw_value[path],
                                                 self._binary)
  
      return self._cached_value
  
  class CachingRietveldPatcher(Patcher):
    ''' CachingRietveldPatcher implements a caching layer on top of |patcher|.
    In theory, it can be used with any class that implements Patcher. But this
    class assumes that applying to all patched files at once is more efficient
    than applying to individual files.
    '''
    def __init__(self,
                 rietveld_patcher,
                 object_store_creator,
                 test_datetime=datetime):
      self._patcher = rietveld_patcher
      def create_object_store(category):
        return object_store_creator.Create(
            CachingRietveldPatcher,
            category='%s/%s' % (rietveld_patcher.GetIdentity(), category))
      self._version_object_store = create_object_store('version')
      self._list_object_store = create_object_store('list')
      self._file_object_store = create_object_store('file')
      self._datetime = test_datetime
  
    def GetVersion(self):
      key = 'version'
      value = self._version_object_store.Get(key).Get()
      if value is not None:
        version, time = value
        if self._datetime.now() - time < _VERSION_CACHE_MAXAGE:
          return version
  
      version = self._patcher.GetVersion()
      self._version_object_store.Set(key,
                                  (version, self._datetime.now()))
      return version
  
    def GetPatchedFiles(self, version=None):
      if version is None:
        version = self.GetVersion()
      patched_files = self._list_object_store.Get(version).Get()
      if patched_files is not None:
        return patched_files
  
      patched_files = self._patcher.GetPatchedFiles(version)
      self._list_object_store.Set(version, patched_files)
      return patched_files
  
    def Apply(self, paths, file_system, binary=False, version=None):
      if version is None:
        version = self.GetVersion()
      added, deleted, modified = self.GetPatchedFiles(version)
      cached_value = _FromObjectStoreValue(self._file_object_store.
          GetMulti([_MakeKey(path, version) for path in paths]).Get(), binary)
      missing_paths = list(set(paths) - set(cached_value.keys()))
      if len(missing_paths) == 0:
        return Future(value=cached_value)
  
      # binary is explicitly set to True. Here we are applying the patch to
      # ALL patched files without a way to know whether individual files are
      # binary or not. Therefore all data cached must be binary. When reading
      # from the cache with binary=False, it will be converted to Unicode by
      # _HandleBinary.
      return _AsyncUncachedFuture(version,
                                  paths,
                                  binary,
                                  cached_value,
                                  missing_paths,
                                  self._patcher.Apply(set(added) | set(modified),
                                                      None,
                                                      True,
                                                      version),
                                  self._file_object_store)
  
    def GetIdentity(self):
      return self._patcher.GetIdentity()
  