1#!/usr/bin/env python
2#
3# Copyright 2014 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7import collections
8from datetime import date
9import re
10import optparse
11import os
12from string import Template
13import sys
14import textwrap
15import zipfile
16
17from util import build_utils
18from util import java_cpp_utils
19
20# List of C++ types that are compatible with the Java code generated by this
21# script.
22#
23# This script can parse .idl files however, at present it ignores special
24# rules such as [cpp_enum_prefix_override="ax_attr"].
25ENUM_FIXED_TYPE_ALLOWLIST = [
26    'char', 'unsigned char', 'short', 'unsigned short', 'int', 'int8_t',
27    'int16_t', 'int32_t', 'uint8_t', 'uint16_t'
28]
29
30
31class EnumDefinition(object):
32  def __init__(self, original_enum_name=None, class_name_override=None,
33               enum_package=None, entries=None, comments=None, fixed_type=None):
34    self.original_enum_name = original_enum_name
35    self.class_name_override = class_name_override
36    self.enum_package = enum_package
37    self.entries = collections.OrderedDict(entries or [])
38    self.comments = collections.OrderedDict(comments or [])
39    self.prefix_to_strip = None
40    self.fixed_type = fixed_type
41
42  def AppendEntry(self, key, value):
43    if key in self.entries:
44      raise Exception('Multiple definitions of key %s found.' % key)
45    self.entries[key] = value
46
47  def AppendEntryComment(self, key, value):
48    if key in self.comments:
49      raise Exception('Multiple definitions of key %s found.' % key)
50    self.comments[key] = value
51
52  @property
53  def class_name(self):
54    return self.class_name_override or self.original_enum_name
55
56  def Finalize(self):
57    self._Validate()
58    self._AssignEntryIndices()
59    self._StripPrefix()
60    self._NormalizeNames()
61
62  def _Validate(self):
63    assert self.class_name
64    assert self.enum_package
65    assert self.entries
66    if self.fixed_type and self.fixed_type not in ENUM_FIXED_TYPE_ALLOWLIST:
67      raise Exception('Fixed type %s for enum %s not in allowlist.' %
68                      (self.fixed_type, self.class_name))
69
70  def _AssignEntryIndices(self):
71    # Enums, if given no value, are given the value of the previous enum + 1.
72    if not all(self.entries.values()):
73      prev_enum_value = -1
74      for key, value in self.entries.items():
75        if not value:
76          self.entries[key] = prev_enum_value + 1
77        elif value in self.entries:
78          self.entries[key] = self.entries[value]
79        else:
80          try:
81            self.entries[key] = int(value)
82          except ValueError:
83            raise Exception('Could not interpret integer from enum value "%s" '
84                            'for key %s.' % (value, key))
85        prev_enum_value = self.entries[key]
86
87
88  def _StripPrefix(self):
89    prefix_to_strip = self.prefix_to_strip
90    if not prefix_to_strip:
91      shout_case = self.original_enum_name
92      shout_case = re.sub('(?!^)([A-Z]+)', r'_\1', shout_case).upper()
93      shout_case += '_'
94
95      prefixes = [shout_case, self.original_enum_name,
96                  'k' + self.original_enum_name]
97
98      for prefix in prefixes:
99        if all([w.startswith(prefix) for w in self.entries.keys()]):
100          prefix_to_strip = prefix
101          break
102      else:
103        prefix_to_strip = ''
104
105    def StripEntries(entries):
106      ret = collections.OrderedDict()
107      for k, v in entries.items():
108        stripped_key = k.replace(prefix_to_strip, '', 1)
109        if isinstance(v, str):
110          stripped_value = v.replace(prefix_to_strip, '')
111        else:
112          stripped_value = v
113        ret[stripped_key] = stripped_value
114
115      return ret
116
117    self.entries = StripEntries(self.entries)
118    self.comments = StripEntries(self.comments)
119
120  def _NormalizeNames(self):
121    self.entries = _TransformKeys(self.entries, java_cpp_utils.KCamelToShouty)
122    self.comments = _TransformKeys(self.comments, java_cpp_utils.KCamelToShouty)
123
124
125def _TransformKeys(d, func):
126  """Normalize keys in |d| and update references to old keys in |d| values."""
127  keys_map = {k: func(k) for k in d}
128  ret = collections.OrderedDict()
129  for k, v in d.items():
130    # Need to transform values as well when the entry value was explicitly set
131    # (since it could contain references to other enum entry values).
132    if isinstance(v, str):
133      # First check if a full replacement is available. This avoids issues when
134      # one key is a substring of another.
135      if v in d:
136        v = keys_map[v]
137      else:
138        for old_key, new_key in keys_map.items():
139          v = v.replace(old_key, new_key)
140    ret[keys_map[k]] = v
141  return ret
142
143
144class DirectiveSet(object):
145  class_name_override_key = 'CLASS_NAME_OVERRIDE'
146  enum_package_key = 'ENUM_PACKAGE'
147  prefix_to_strip_key = 'PREFIX_TO_STRIP'
148
149  known_keys = [class_name_override_key, enum_package_key, prefix_to_strip_key]
150
151  def __init__(self):
152    self._directives = {}
153
154  def Update(self, key, value):
155    if key not in DirectiveSet.known_keys:
156      raise Exception("Unknown directive: " + key)
157    self._directives[key] = value
158
159  @property
160  def empty(self):
161    return len(self._directives) == 0
162
163  def UpdateDefinition(self, definition):
164    definition.class_name_override = self._directives.get(
165        DirectiveSet.class_name_override_key, '')
166    definition.enum_package = self._directives.get(
167        DirectiveSet.enum_package_key)
168    definition.prefix_to_strip = self._directives.get(
169        DirectiveSet.prefix_to_strip_key)
170
171
172class HeaderParser(object):
173  single_line_comment_re = re.compile(r'\s*//\s*([^\n]*)')
174  multi_line_comment_start_re = re.compile(r'\s*/\*')
175  enum_line_re = re.compile(r'^\s*(\w+)(\s*\=\s*([^,\n]+))?,?')
176  enum_end_re = re.compile(r'^\s*}\s*;\.*$')
177  generator_error_re = re.compile(r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*$')
178  generator_directive_re = re.compile(
179      r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*([\.\w]+)$')
180  multi_line_generator_directive_start_re = re.compile(
181      r'^\s*//\s+GENERATED_JAVA_(\w+)\s*:\s*\(([\.\w]*)$')
182  multi_line_directive_continuation_re = re.compile(r'^\s*//\s+([\.\w]+)$')
183  multi_line_directive_end_re = re.compile(r'^\s*//\s+([\.\w]*)\)$')
184
185  optional_class_or_struct_re = r'(class|struct)?'
186  enum_name_re = r'(\w+)'
187  optional_fixed_type_re = r'(\:\s*(\w+\s*\w+?))?'
188  enum_start_re = re.compile(r'^\s*(?:\[cpp.*\])?\s*enum\s+' +
189      optional_class_or_struct_re + '\s*' + enum_name_re + '\s*' +
190      optional_fixed_type_re + '\s*{\s*')
191  enum_single_line_re = re.compile(
192      r'^\s*(?:\[cpp.*\])?\s*enum.*{(?P<enum_entries>.*)}.*$')
193
194  def __init__(self, lines, path=''):
195    self._lines = lines
196    self._path = path
197    self._enum_definitions = []
198    self._in_enum = False
199    self._current_definition = None
200    self._current_comments = []
201    self._generator_directives = DirectiveSet()
202    self._multi_line_generator_directive = None
203    self._current_enum_entry = ''
204
205  def _ApplyGeneratorDirectives(self):
206    self._generator_directives.UpdateDefinition(self._current_definition)
207    self._generator_directives = DirectiveSet()
208
209  def ParseDefinitions(self):
210    for line in self._lines:
211      self._ParseLine(line)
212    return self._enum_definitions
213
214  def _ParseLine(self, line):
215    if self._multi_line_generator_directive:
216      self._ParseMultiLineDirectiveLine(line)
217    elif not self._in_enum:
218      self._ParseRegularLine(line)
219    else:
220      self._ParseEnumLine(line)
221
222  def _ParseEnumLine(self, line):
223    if HeaderParser.multi_line_comment_start_re.match(line):
224      raise Exception('Multi-line comments in enums are not supported in ' +
225                      self._path)
226
227    enum_comment = HeaderParser.single_line_comment_re.match(line)
228    if enum_comment:
229      comment = enum_comment.groups()[0]
230      if comment:
231        self._current_comments.append(comment)
232    elif HeaderParser.enum_end_re.match(line):
233      self._FinalizeCurrentEnumDefinition()
234    else:
235      self._AddToCurrentEnumEntry(line)
236      if ',' in line:
237        self._ParseCurrentEnumEntry()
238
239  def _ParseSingleLineEnum(self, line):
240    for entry in line.split(','):
241      self._AddToCurrentEnumEntry(entry)
242      self._ParseCurrentEnumEntry()
243
244    self._FinalizeCurrentEnumDefinition()
245
246  def _ParseCurrentEnumEntry(self):
247    if not self._current_enum_entry:
248      return
249
250    enum_entry = HeaderParser.enum_line_re.match(self._current_enum_entry)
251    if not enum_entry:
252      raise Exception('Unexpected error while attempting to parse %s as enum '
253                      'entry.' % self._current_enum_entry)
254
255    enum_key = enum_entry.groups()[0]
256    enum_value = enum_entry.groups()[2]
257    self._current_definition.AppendEntry(enum_key, enum_value)
258    if self._current_comments:
259      self._current_definition.AppendEntryComment(
260          enum_key, ' '.join(self._current_comments))
261      self._current_comments = []
262    self._current_enum_entry = ''
263
264  def _AddToCurrentEnumEntry(self, line):
265    self._current_enum_entry += ' ' + line.strip()
266
267  def _FinalizeCurrentEnumDefinition(self):
268    if self._current_enum_entry:
269      self._ParseCurrentEnumEntry()
270    self._ApplyGeneratorDirectives()
271    self._current_definition.Finalize()
272    self._enum_definitions.append(self._current_definition)
273    self._current_definition = None
274    self._in_enum = False
275
276  def _ParseMultiLineDirectiveLine(self, line):
277    multi_line_directive_continuation = (
278        HeaderParser.multi_line_directive_continuation_re.match(line))
279    multi_line_directive_end = (
280        HeaderParser.multi_line_directive_end_re.match(line))
281
282    if multi_line_directive_continuation:
283      value_cont = multi_line_directive_continuation.groups()[0]
284      self._multi_line_generator_directive[1].append(value_cont)
285    elif multi_line_directive_end:
286      directive_name = self._multi_line_generator_directive[0]
287      directive_value = "".join(self._multi_line_generator_directive[1])
288      directive_value += multi_line_directive_end.groups()[0]
289      self._multi_line_generator_directive = None
290      self._generator_directives.Update(directive_name, directive_value)
291    else:
292      raise Exception('Malformed multi-line directive declaration in ' +
293                      self._path)
294
295  def _ParseRegularLine(self, line):
296    enum_start = HeaderParser.enum_start_re.match(line)
297    generator_directive_error = HeaderParser.generator_error_re.match(line)
298    generator_directive = HeaderParser.generator_directive_re.match(line)
299    multi_line_generator_directive_start = (
300        HeaderParser.multi_line_generator_directive_start_re.match(line))
301    single_line_enum = HeaderParser.enum_single_line_re.match(line)
302
303    if generator_directive_error:
304      raise Exception('Malformed directive declaration in ' + self._path +
305                      '. Use () for multi-line directives. E.g.\n' +
306                      '// GENERATED_JAVA_ENUM_PACKAGE: (\n' +
307                      '//   foo.package)')
308    elif generator_directive:
309      directive_name = generator_directive.groups()[0]
310      directive_value = generator_directive.groups()[1]
311      self._generator_directives.Update(directive_name, directive_value)
312    elif multi_line_generator_directive_start:
313      directive_name = multi_line_generator_directive_start.groups()[0]
314      directive_value = multi_line_generator_directive_start.groups()[1]
315      self._multi_line_generator_directive = (directive_name, [directive_value])
316    elif enum_start or single_line_enum:
317      if self._generator_directives.empty:
318        return
319      self._current_definition = EnumDefinition(
320          original_enum_name=enum_start.groups()[1],
321          fixed_type=enum_start.groups()[3])
322      self._in_enum = True
323      if single_line_enum:
324        self._ParseSingleLineEnum(single_line_enum.group('enum_entries'))
325
326
327def DoGenerate(source_paths):
328  for source_path in source_paths:
329    enum_definitions = DoParseHeaderFile(source_path)
330    if not enum_definitions:
331      raise Exception('No enums found in %s\n'
332                      'Did you forget prefixing enums with '
333                      '"// GENERATED_JAVA_ENUM_PACKAGE: foo"?' %
334                      source_path)
335    for enum_definition in enum_definitions:
336      output_path = java_cpp_utils.GetJavaFilePath(enum_definition.enum_package,
337                                                   enum_definition.class_name)
338      output = GenerateOutput(source_path, enum_definition)
339      yield output_path, output
340
341
342def DoParseHeaderFile(path):
343  with open(path) as f:
344    return HeaderParser(f.readlines(), path).ParseDefinitions()
345
346
347def GenerateOutput(source_path, enum_definition):
348  template = Template("""
349// Copyright ${YEAR} The Chromium Authors. All rights reserved.
350// Use of this source code is governed by a BSD-style license that can be
351// found in the LICENSE file.
352
353// This file is autogenerated by
354//     ${SCRIPT_NAME}
355// From
356//     ${SOURCE_PATH}
357
358package ${PACKAGE};
359
360import androidx.annotation.IntDef;
361
362import java.lang.annotation.Retention;
363import java.lang.annotation.RetentionPolicy;
364
365@IntDef({
366${INT_DEF}
367})
368@Retention(RetentionPolicy.SOURCE)
369public @interface ${CLASS_NAME} {
370${ENUM_ENTRIES}
371}
372""")
373
374  enum_template = Template('  int ${NAME} = ${VALUE};')
375  enum_entries_string = []
376  enum_names = []
377  for enum_name, enum_value in enum_definition.entries.items():
378    values = {
379        'NAME': enum_name,
380        'VALUE': enum_value,
381    }
382    enum_comments = enum_definition.comments.get(enum_name)
383    if enum_comments:
384      enum_comments_indent = '   * '
385      comments_line_wrapper = textwrap.TextWrapper(
386          initial_indent=enum_comments_indent,
387          subsequent_indent=enum_comments_indent,
388          width=100)
389      enum_entries_string.append('  /**')
390      enum_entries_string.append('\n'.join(
391          comments_line_wrapper.wrap(enum_comments)))
392      enum_entries_string.append('   */')
393    enum_entries_string.append(enum_template.substitute(values))
394    if enum_name != "NUM_ENTRIES":
395      enum_names.append(enum_definition.class_name + '.' + enum_name)
396  enum_entries_string = '\n'.join(enum_entries_string)
397
398  enum_names_indent = ' ' * 4
399  wrapper = textwrap.TextWrapper(initial_indent = enum_names_indent,
400                                 subsequent_indent = enum_names_indent,
401                                 width = 100)
402  enum_names_string = '\n'.join(wrapper.wrap(', '.join(enum_names)))
403
404  values = {
405      'CLASS_NAME': enum_definition.class_name,
406      'ENUM_ENTRIES': enum_entries_string,
407      'PACKAGE': enum_definition.enum_package,
408      'INT_DEF': enum_names_string,
409      'SCRIPT_NAME': java_cpp_utils.GetScriptName(),
410      'SOURCE_PATH': source_path,
411      'YEAR': str(date.today().year)
412  }
413  return template.substitute(values)
414
415
416def DoMain(argv):
417  usage = 'usage: %prog [options] [output_dir] input_file(s)...'
418  parser = optparse.OptionParser(usage=usage)
419
420  parser.add_option('--srcjar',
421                    help='When specified, a .srcjar at the given path is '
422                    'created instead of individual .java files.')
423
424  options, args = parser.parse_args(argv)
425
426  if not args:
427    parser.error('Need to specify at least one input file')
428  input_paths = args
429
430  with build_utils.AtomicOutput(options.srcjar) as f:
431    with zipfile.ZipFile(f, 'w', zipfile.ZIP_STORED) as srcjar:
432      for output_path, data in DoGenerate(input_paths):
433        build_utils.AddToZipHermetic(srcjar, output_path, data=data)
434
435
436if __name__ == '__main__':
437  DoMain(sys.argv[1:])
438