Home | History | Annotate | Download | only in server2
      1 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 import sys
      6 
      7 import schema_util
      8 from docs_server_utils import ToUnicode
      9 from file_system import FileNotFoundError
     10 from future import Future
     11 from path_util import AssertIsDirectory, AssertIsFile
     12 from third_party.handlebar import Handlebar
     13 from third_party.json_schema_compiler import json_parse
     14 from third_party.json_schema_compiler.memoize import memoize
     15 
     16 
     17 _SINGLE_FILE_FUNCTIONS = set()
     18 
     19 
     20 def SingleFile(fn):
     21   '''A decorator which can be optionally applied to the compilation function
     22   passed to CompiledFileSystem.Create, indicating that the function only
     23   needs access to the file which is given in the function's callback. When
     24   this is the case some optimisations can be done.
     25 
     26   Note that this decorator must be listed first in any list of decorators to
     27   have any effect.
     28   '''
     29   _SINGLE_FILE_FUNCTIONS.add(fn)
     30   return fn
     31 
     32 
     33 def Unicode(fn):
     34   '''A decorator which can be optionally applied to the compilation function
     35   passed to CompiledFileSystem.Create, indicating that the function processes
     36   the file's data as Unicode text.
     37   '''
     38 
     39   # The arguments passed to fn can be (self, path, data) or (path, data). In
     40   # either case the last argument is |data|, which should be converted to
     41   # Unicode.
     42   def convert_args(args):
     43     args = list(args)
     44     args[-1] = ToUnicode(args[-1])
     45     return args
     46 
     47   return lambda *args: fn(*convert_args(args))
     48 
     49 
     50 class _CacheEntry(object):
     51   def __init__(self, cache_data, version):
     52 
     53     self._cache_data = cache_data
     54     self.version = version
     55 
     56 
     57 class CompiledFileSystem(object):
     58   '''This class caches FileSystem data that has been processed.
     59   '''
     60 
     61   class Factory(object):
     62     '''A class to build a CompiledFileSystem backed by |file_system|.
     63     '''
     64 
     65     def __init__(self, object_store_creator):
     66       self._object_store_creator = object_store_creator
     67 
     68     def Create(self, file_system, compilation_function, cls, category=None):
     69       '''Creates a CompiledFileSystem view over |file_system| that populates
     70       its cache by calling |compilation_function| with (path, data), where
     71       |data| is the data that was fetched from |path| in |file_system|.
     72 
     73       The namespace for the compiled file system is derived similar to
     74       ObjectStoreCreator: from |cls| along with an optional |category|.
     75       '''
     76       assert isinstance(cls, type)
     77       assert not cls.__name__[0].islower()  # guard against non-class types
     78       full_name = [cls.__name__, file_system.GetIdentity()]
     79       if category is not None:
     80         full_name.append(category)
     81       def create_object_store(my_category):
     82         # The read caches can start populated (start_empty=False) because file
     83         # updates are picked up by the stat - but only if the compilation
     84         # function is affected by a single file. If the compilation function is
     85         # affected by other files (e.g. compiling a list of APIs available to
     86         # extensions may be affected by both a features file and the list of
     87         # files in the API directory) then this optimisation won't work.
     88         return self._object_store_creator.Create(
     89             CompiledFileSystem,
     90             category='/'.join(full_name + [my_category]),
     91             start_empty=compilation_function not in _SINGLE_FILE_FUNCTIONS)
     92       return CompiledFileSystem(file_system,
     93                                 compilation_function,
     94                                 create_object_store('file'),
     95                                 create_object_store('list'))
     96 
     97     @memoize
     98     def ForJson(self, file_system):
     99       '''A CompiledFileSystem specifically for parsing JSON configuration data.
    100       These are memoized over file systems tied to different branches.
    101       '''
    102       return self.Create(file_system,
    103                          SingleFile(lambda _, data:
    104                              json_parse.Parse(ToUnicode(data))),
    105                          CompiledFileSystem,
    106                          category='json')
    107 
    108     @memoize
    109     def ForAPISchema(self, file_system):
    110       '''Creates a CompiledFileSystem for parsing raw JSON or IDL API schema
    111       data and formatting it so that it can be used by other classes, such
    112       as Model and APISchemaGraph.
    113       '''
    114       return self.Create(file_system,
    115                          SingleFile(Unicode(schema_util.ProcessSchema)),
    116                          CompiledFileSystem,
    117                          category='api-schema')
    118 
    119     @memoize
    120     def ForTemplates(self, file_system):
    121       '''Creates a CompiledFileSystem for parsing templates.
    122       '''
    123       return self.Create(
    124           file_system,
    125           SingleFile(lambda path, text: Handlebar(ToUnicode(text), name=path)),
    126           CompiledFileSystem)
    127 
    128     @memoize
    129     def ForUnicode(self, file_system):
    130       '''Creates a CompiledFileSystem for Unicode text processing.
    131       '''
    132       return self.Create(
    133         file_system,
    134         SingleFile(lambda _, text: ToUnicode(text)),
    135         CompiledFileSystem,
    136         category='text')
    137 
    138   def __init__(self,
    139                file_system,
    140                compilation_function,
    141                file_object_store,
    142                list_object_store):
    143     self._file_system = file_system
    144     self._compilation_function = compilation_function
    145     self._file_object_store = file_object_store
    146     self._list_object_store = list_object_store
    147 
    148   def _RecursiveList(self, path):
    149     '''Returns a Future containing the recursive directory listing of |path| as
    150     a flat list of paths.
    151     '''
    152     def split_dirs_from_files(paths):
    153       '''Returns a tuple (dirs, files) where |dirs| contains the directory
    154       names in |paths| and |files| contains the files.
    155       '''
    156       result = [], []
    157       for path in paths:
    158         result[0 if path.endswith('/') else 1].append(path)
    159       return result
    160 
    161     def add_prefix(prefix, paths):
    162       return [prefix + path for path in paths]
    163 
    164     # Read in the initial list of files. Do this eagerly (i.e. not part of the
    165     # asynchronous Future contract) because there's a greater chance to
    166     # parallelise fetching with the second layer (can fetch multiple paths).
    167     try:
    168       first_layer_dirs, first_layer_files = split_dirs_from_files(
    169           self._file_system.ReadSingle(path).Get())
    170     except FileNotFoundError:
    171       return Future(exc_info=sys.exc_info())
    172 
    173     if not first_layer_dirs:
    174       return Future(value=first_layer_files)
    175 
    176     second_layer_listing = self._file_system.Read(
    177         add_prefix(path, first_layer_dirs))
    178 
    179     def resolve():
    180       def get_from_future_listing(futures):
    181         '''Recursively lists files from directory listing |futures|.
    182         '''
    183         dirs, files = [], []
    184         for dir_name, listing in futures.Get().iteritems():
    185           new_dirs, new_files = split_dirs_from_files(listing)
    186           # |dirs| are paths for reading. Add the full prefix relative to
    187           # |path| so that |file_system| can find the files.
    188           dirs += add_prefix(dir_name, new_dirs)
    189           # |files| are not for reading, they are for returning to the caller.
    190           # This entire function set (i.e. GetFromFileListing) is defined to
    191           # not include the fetched-path in the result, however, |dir_name|
    192           # will be prefixed with |path|. Strip it.
    193           assert dir_name.startswith(path)
    194           files += add_prefix(dir_name[len(path):], new_files)
    195         if dirs:
    196           files += get_from_future_listing(self._file_system.Read(dirs))
    197         return files
    198 
    199       return first_layer_files + get_from_future_listing(second_layer_listing)
    200 
    201     return Future(callback=resolve)
    202 
    203   def GetFromFile(self, path):
    204     '''Calls |compilation_function| on the contents of the file at |path|.  If
    205     |binary| is True then the file will be read as binary - but this will only
    206     apply for the first time the file is fetched; if already cached, |binary|
    207     will be ignored.
    208     '''
    209     AssertIsFile(path)
    210 
    211     try:
    212       version = self._file_system.Stat(path).version
    213     except FileNotFoundError:
    214       return Future(exc_info=sys.exc_info())
    215 
    216     cache_entry = self._file_object_store.Get(path).Get()
    217     if (cache_entry is not None) and (version == cache_entry.version):
    218       return Future(value=cache_entry._cache_data)
    219 
    220     future_files = self._file_system.ReadSingle(path)
    221     def resolve():
    222       cache_data = self._compilation_function(path, future_files.Get())
    223       self._file_object_store.Set(path, _CacheEntry(cache_data, version))
    224       return cache_data
    225     return Future(callback=resolve)
    226 
    227   def GetFromFileListing(self, path):
    228     '''Calls |compilation_function| on the listing of the files at |path|.
    229     Assumes that the path given is to a directory.
    230     '''
    231     AssertIsDirectory(path)
    232 
    233     try:
    234       version = self._file_system.Stat(path).version
    235     except FileNotFoundError:
    236       return Future(exc_info=sys.exc_info())
    237 
    238     cache_entry = self._list_object_store.Get(path).Get()
    239     if (cache_entry is not None) and (version == cache_entry.version):
    240       return Future(value=cache_entry._cache_data)
    241 
    242     recursive_list_future = self._RecursiveList(path)
    243     def resolve():
    244       cache_data = self._compilation_function(path, recursive_list_future.Get())
    245       self._list_object_store.Set(path, _CacheEntry(cache_data, version))
    246       return cache_data
    247     return Future(callback=resolve)
    248 
    249   def GetFileVersion(self, path):
    250     cache_entry = self._file_object_store.Get(path).Get()
    251     if cache_entry is not None:
    252       return cache_entry.version
    253     return self._file_system.Stat(path).version
    254 
    255   def GetFileListingVersion(self, path):
    256     if not path.endswith('/'):
    257       path += '/'
    258     cache_entry = self._list_object_store.Get(path).Get()
    259     if cache_entry is not None:
    260       return cache_entry.version
    261     return self._file_system.Stat(path).version
    262 
    263   def FileExists(self, path):
    264     return self._file_system.Exists(path)
    265 
    266   def GetIdentity(self):
    267     return self._file_system.GetIdentity()
    268