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.
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
17 _SINGLE_FILE_FUNCTIONS = set()
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.
26 Note that this decorator must be listed first in any list of decorators to
29 _SINGLE_FILE_FUNCTIONS.add(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.
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
42 def convert_args(args):
44 args[-1] = ToUnicode(args[-1])
47 return lambda *args: fn(*convert_args(args))
50 class _CacheEntry(object):
51 def __init__(self, cache_data, version):
53 self._cache_data = cache_data
54 self.version = version
57 class CompiledFileSystem(object):
58 '''This class caches FileSystem data that has been processed.
61 class Factory(object):
62 '''A class to build a CompiledFileSystem backed by |file_system|.
65 def __init__(self, object_store_creator):
66 self._object_store_creator = object_store_creator
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|.
73 The namespace for the compiled file system is derived similar to
74 ObjectStoreCreator: from |cls| along with an optional |category|.
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(
90 category='/'.join(full_name + [my_category]),
91 start_empty=compilation_function not in _SINGLE_FILE_FUNCTIONS)
92 return CompiledFileSystem(file_system,
94 create_object_store('file'),
95 create_object_store('list'))
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.
102 return self.Create(file_system,
103 SingleFile(lambda _, data:
104 json_parse.Parse(ToUnicode(data))),
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.
114 return self.Create(file_system,
115 SingleFile(Unicode(schema_util.ProcessSchema)),
117 category='api-schema')
120 def ForTemplates(self, file_system):
121 '''Creates a CompiledFileSystem for parsing templates.
125 SingleFile(lambda path, text: Handlebar(ToUnicode(text), name=path)),
129 def ForUnicode(self, file_system):
130 '''Creates a CompiledFileSystem for Unicode text processing.
134 SingleFile(lambda _, text: ToUnicode(text)),
140 compilation_function,
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
148 def _RecursiveList(self, path):
149 '''Returns a Future containing the recursive directory listing of |path| as
150 a flat list of paths.
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.
158 result[0 if path.endswith('/') else 1].append(path)
161 def add_prefix(prefix, paths):
162 return [prefix + path for path in paths]
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).
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())
173 if not first_layer_dirs:
174 return Future(value=first_layer_files)
176 second_layer_listing = self._file_system.Read(
177 add_prefix(path, first_layer_dirs))
180 def get_from_future_listing(futures):
181 '''Recursively lists files from directory listing |futures|.
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)
196 files += get_from_future_listing(self._file_system.Read(dirs))
199 return first_layer_files + get_from_future_listing(second_layer_listing)
201 return Future(callback=resolve)
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|
212 version = self._file_system.Stat(path).version
213 except FileNotFoundError:
214 return Future(exc_info=sys.exc_info())
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)
220 future_files = self._file_system.ReadSingle(path)
222 cache_data = self._compilation_function(path, future_files.Get())
223 self._file_object_store.Set(path, _CacheEntry(cache_data, version))
225 return Future(callback=resolve)
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.
231 AssertIsDirectory(path)
234 version = self._file_system.Stat(path).version
235 except FileNotFoundError:
236 return Future(exc_info=sys.exc_info())
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)
242 recursive_list_future = self._RecursiveList(path)
244 cache_data = self._compilation_function(path, recursive_list_future.Get())
245 self._list_object_store.Set(path, _CacheEntry(cache_data, version))
247 return Future(callback=resolve)
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
255 def GetFileListingVersion(self, path):
256 if not path.endswith('/'):
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
263 def FileExists(self, path):
264 return self._file_system.Exists(path)
266 def GetIdentity(self):
267 return self._file_system.GetIdentity()