1# Copyright (c) 2014 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"""ResourceFinder is a helper class for finding resources given their name."""
6
7import codecs
8import os
9
10from tvcm import module
11from tvcm import style_sheet as style_sheet_module
12from tvcm import resource as resource_module
13from tvcm import html_module
14from tvcm import strip_js_comments
15
16
17class ResourceLoader(object):
18  """Manges loading modules and their dependencies from files.
19
20  Modules handle parsing and the construction of their individual dependency
21  pointers. The loader deals with bookkeeping of what has been loaded, and
22  mapping names to file resources.
23  """
24  def __init__(self, project):
25    self.project = project
26    self.stripped_js_by_filename = {}
27    self.loaded_modules = {}
28    self.loaded_raw_scripts = {}
29    self.loaded_style_sheets = {}
30    self.loaded_images = {}
31
32  @property
33  def source_paths(self):
34    """A list of base directories to search for modules under."""
35    return self.project.source_paths
36
37  def FindResource(self, some_path, binary=False):
38    """Finds a Resource for the given path.
39
40    Args:
41      some_path: A relative or absolute path to a file.
42
43    Returns:
44      A Resource or None.
45    """
46    if os.path.isabs(some_path):
47      return self.FindResourceGivenAbsolutePath(some_path, binary)
48    else:
49      return self.FindResourceGivenRelativePath(some_path, binary)
50
51  def FindResourceGivenAbsolutePath(self, absolute_path, binary=False):
52    """Returns a Resource for the given absolute path."""
53    candidate_paths = []
54    for source_path in self.source_paths:
55      if absolute_path.startswith(source_path):
56        candidate_paths.append(source_path)
57    if len(candidate_paths) == 0:
58      return None
59
60    # Sort by length. Longest match wins.
61    candidate_paths.sort(lambda x, y: len(x) - len(y))
62    longest_candidate = candidate_paths[-1]
63    return resource_module.Resource(longest_candidate, absolute_path, binary)
64
65  def FindResourceGivenRelativePath(self, relative_path, binary=False):
66    """Returns a Resource for the given relative path."""
67    absolute_path = None
68    for script_path in self.source_paths:
69      absolute_path = os.path.join(script_path, relative_path)
70      if os.path.exists(absolute_path):
71        return resource_module.Resource(script_path, absolute_path, binary)
72    return None
73
74  def _FindResourceGivenNameAndSuffix(
75      self, requested_name, extension, return_resource=False):
76    """Searches for a file and reads its contents.
77
78    Args:
79      requested_name: The name of the resource that was requested.
80      extension: The extension for this requested resource.
81
82    Returns:
83      A (path, contents) pair.
84    """
85    pathy_name = requested_name.replace('.', os.sep)
86    filename = pathy_name + extension
87
88    resource = self.FindResourceGivenRelativePath(filename)
89    if return_resource:
90      return resource
91    if not resource:
92      return None, None
93    return _read_file(resource.absolute_path)
94
95  def FindModuleResource(self, requested_module_name):
96    """Finds a module javascript file and returns a Resource, or none."""
97    js_resource = self._FindResourceGivenNameAndSuffix(
98        requested_module_name, '.js', return_resource=True)
99    html_resource = self._FindResourceGivenNameAndSuffix(
100        requested_module_name, '.html', return_resource=True)
101    if js_resource and html_resource:
102      if html_module.IsHTMLResourceTheModuleGivenConflictingResourceNames(
103          js_resource, html_resource):
104        return html_resource
105      return js_resource
106    elif js_resource:
107      return js_resource
108    return html_resource
109
110  def LoadModule(self, module_name=None, module_filename=None):
111    assert bool(module_name) ^ bool(module_filename), (
112        'Must provide either module_name or module_filename.')
113    if module_filename:
114      resource = self.FindResource(module_filename)
115      if not resource:
116        raise Exception('Could not find %s in %s' % (
117            module_filename, repr(self.source_paths)))
118      module_name = resource.name
119    else:
120      resource = None  # Will be set if we end up needing to load.
121
122    if module_name in self.loaded_modules:
123      assert self.loaded_modules[module_name].contents
124      return self.loaded_modules[module_name]
125
126    if not resource:  # happens when module_name was given
127      resource = self.FindModuleResource(module_name)
128      if not resource:
129        raise module.DepsException('No resource for module "%s"' % module_name)
130
131    m = html_module.HTMLModule(self, module_name, resource)
132    self.loaded_modules[module_name] = m
133
134    # Fake it, this is probably either polymer.min.js or platform.js which are
135    # actually .js files....
136    if resource.absolute_path.endswith('.js'):
137      return m
138
139    m.Parse()
140    m.Load()
141    return m
142
143  def LoadRawScript(self, relative_raw_script_path):
144    resource = None
145    for source_path in self.source_paths:
146      possible_absolute_path = os.path.join(
147          source_path, os.path.normpath(relative_raw_script_path))
148      if os.path.exists(possible_absolute_path):
149        resource = resource_module.Resource(source_path, possible_absolute_path)
150        break
151    if not resource:
152      raise module.DepsException(
153          'Could not find a file for raw script %s in %s' %
154          (relative_raw_script_path, self.source_paths))
155    assert relative_raw_script_path == resource.unix_style_relative_path, (
156        'Expected %s == %s' % (relative_raw_script_path,
157                               resource.unix_style_relative_path))
158
159    if resource.absolute_path in self.loaded_raw_scripts:
160      return self.loaded_raw_scripts[resource.absolute_path]
161
162    raw_script = module.RawScript(resource)
163    self.loaded_raw_scripts[resource.absolute_path] = raw_script
164    return raw_script
165
166  def LoadStyleSheet(self, name):
167    if name in self.loaded_style_sheets:
168      return self.loaded_style_sheets[name]
169
170    resource = self._FindResourceGivenNameAndSuffix(
171        name, '.css', return_resource=True)
172    if not resource:
173      raise module.DepsException(
174          'Could not find a file for stylesheet %s' % name)
175
176    style_sheet = style_sheet_module.StyleSheet(self, name, resource)
177    style_sheet.load()
178    self.loaded_style_sheets[name] = style_sheet
179    return style_sheet
180
181  def LoadImage(self, abs_path):
182    if abs_path in self.loaded_images:
183      return self.loaded_images[abs_path]
184
185    if not os.path.exists(abs_path):
186      raise module.DepsException("url('%s') did not exist" % abs_path)
187
188    res = self.FindResourceGivenAbsolutePath(abs_path, binary=True)
189    if res is None:
190      raise module.DepsException("url('%s') was not in search path" % abs_path)
191
192    image = style_sheet_module.Image(res)
193    self.loaded_images[abs_path] = image
194    return image
195
196  def GetStrippedJSForFilename(self, filename, early_out_if_no_tvcm):
197    if filename in self.stripped_js_by_filename:
198      return self.stripped_js_by_filename[filename]
199
200    with open(filename, 'r') as f:
201      contents = f.read(4096)
202    if early_out_if_no_tvcm and ('tvcm' not in contents):
203      return None
204
205    s = strip_js_comments.StripJSComments(contents)
206    self.stripped_js_by_filename[filename] = s
207    return s
208
209
210def _read_file(absolute_path):
211  """Reads a file and returns a (path, contents) pair.
212
213  Args:
214    absolute_path: Absolute path to a file.
215
216  Raises:
217    Exception: The given file doesn't exist.
218    IOError: There was a problem opening or reading the file.
219  """
220  if not os.path.exists(absolute_path):
221    raise Exception('%s not found.' % absolute_path)
222  f = codecs.open(absolute_path, mode='r', encoding='utf-8')
223  contents = f.read()
224  f.close()
225  return absolute_path, contents
226