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