e9b43ebace900da61e7325026c1df5e984d35402
[platform/framework/web/crosswalk.git] / src / chrome / common / extensions / docs / server2 / compiled_file_system.py
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()