• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright 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"""Generates java source files from a mojom.Module."""
6
7import argparse
8import ast
9import contextlib
10import os
11import re
12import shutil
13import sys
14import tempfile
15
16from jinja2 import contextfilter
17
18import mojom.fileutil as fileutil
19import mojom.generate.generator as generator
20import mojom.generate.module as mojom
21from mojom.generate.template_expander import UseJinja
22
23sys.path.append(os.path.join(os.path.dirname(__file__), os.pardir,
24                             os.pardir, os.pardir, os.pardir, os.pardir,
25                             'build', 'android', 'gyp'))
26from util import build_utils
27
28
29GENERATOR_PREFIX = 'java'
30
31_spec_to_java_type = {
32  mojom.BOOL.spec: 'boolean',
33  mojom.DCPIPE.spec: 'org.chromium.mojo.system.DataPipe.ConsumerHandle',
34  mojom.DOUBLE.spec: 'double',
35  mojom.DPPIPE.spec: 'org.chromium.mojo.system.DataPipe.ProducerHandle',
36  mojom.FLOAT.spec: 'float',
37  mojom.HANDLE.spec: 'org.chromium.mojo.system.UntypedHandle',
38  mojom.INT16.spec: 'short',
39  mojom.INT32.spec: 'int',
40  mojom.INT64.spec: 'long',
41  mojom.INT8.spec: 'byte',
42  mojom.MSGPIPE.spec: 'org.chromium.mojo.system.MessagePipeHandle',
43  mojom.NULLABLE_DCPIPE.spec:
44      'org.chromium.mojo.system.DataPipe.ConsumerHandle',
45  mojom.NULLABLE_DPPIPE.spec:
46      'org.chromium.mojo.system.DataPipe.ProducerHandle',
47  mojom.NULLABLE_HANDLE.spec: 'org.chromium.mojo.system.UntypedHandle',
48  mojom.NULLABLE_MSGPIPE.spec: 'org.chromium.mojo.system.MessagePipeHandle',
49  mojom.NULLABLE_SHAREDBUFFER.spec:
50      'org.chromium.mojo.system.SharedBufferHandle',
51  mojom.NULLABLE_STRING.spec: 'String',
52  mojom.SHAREDBUFFER.spec: 'org.chromium.mojo.system.SharedBufferHandle',
53  mojom.STRING.spec: 'String',
54  mojom.UINT16.spec: 'short',
55  mojom.UINT32.spec: 'int',
56  mojom.UINT64.spec: 'long',
57  mojom.UINT8.spec: 'byte',
58}
59
60_spec_to_decode_method = {
61  mojom.BOOL.spec:                  'readBoolean',
62  mojom.DCPIPE.spec:                'readConsumerHandle',
63  mojom.DOUBLE.spec:                'readDouble',
64  mojom.DPPIPE.spec:                'readProducerHandle',
65  mojom.FLOAT.spec:                 'readFloat',
66  mojom.HANDLE.spec:                'readUntypedHandle',
67  mojom.INT16.spec:                 'readShort',
68  mojom.INT32.spec:                 'readInt',
69  mojom.INT64.spec:                 'readLong',
70  mojom.INT8.spec:                  'readByte',
71  mojom.MSGPIPE.spec:               'readMessagePipeHandle',
72  mojom.NULLABLE_DCPIPE.spec:       'readConsumerHandle',
73  mojom.NULLABLE_DPPIPE.spec:       'readProducerHandle',
74  mojom.NULLABLE_HANDLE.spec:       'readUntypedHandle',
75  mojom.NULLABLE_MSGPIPE.spec:      'readMessagePipeHandle',
76  mojom.NULLABLE_SHAREDBUFFER.spec: 'readSharedBufferHandle',
77  mojom.NULLABLE_STRING.spec:       'readString',
78  mojom.SHAREDBUFFER.spec:          'readSharedBufferHandle',
79  mojom.STRING.spec:                'readString',
80  mojom.UINT16.spec:                'readShort',
81  mojom.UINT32.spec:                'readInt',
82  mojom.UINT64.spec:                'readLong',
83  mojom.UINT8.spec:                 'readByte',
84}
85
86_java_primitive_to_boxed_type = {
87  'boolean': 'Boolean',
88  'byte':    'Byte',
89  'double':  'Double',
90  'float':   'Float',
91  'int':     'Integer',
92  'long':    'Long',
93  'short':   'Short',
94}
95
96_java_reserved_types = [
97  # These two may clash with commonly used classes on Android.
98  'Manifest',
99  'R'
100]
101
102def NameToComponent(name):
103  """ Returns a list of lowercase words corresponding to a given name. """
104  # Add underscores after uppercase letters when appropriate. An uppercase
105  # letter is considered the end of a word if it is followed by an upper and a
106  # lower. E.g. URLLoaderFactory -> URL_LoaderFactory
107  name = re.sub('([A-Z][0-9]*)(?=[A-Z][0-9]*[a-z])', r'\1_', name)
108  # Add underscores after lowercase letters when appropriate. A lowercase letter
109  # is considered the end of a word if it is followed by an upper.
110  # E.g. URLLoaderFactory -> URLLoader_Factory
111  name = re.sub('([a-z][0-9]*)(?=[A-Z])', r'\1_', name)
112  return [x.lower() for x in name.split('_')]
113
114def UpperCamelCase(name):
115  return ''.join([x.capitalize() for x in NameToComponent(name)])
116
117def CamelCase(name):
118  uccc = UpperCamelCase(name)
119  return uccc[0].lower() + uccc[1:]
120
121def ConstantStyle(name):
122  components = NameToComponent(name)
123  if components[0] == 'k' and len(components) > 1:
124    components = components[1:]
125  # variable cannot starts with a digit.
126  if components[0][0].isdigit():
127    components[0] = '_' + components[0]
128  return '_'.join([x.upper() for x in components])
129
130def GetNameForElement(element):
131  if (mojom.IsEnumKind(element) or mojom.IsInterfaceKind(element) or
132      mojom.IsStructKind(element) or mojom.IsUnionKind(element)):
133    name = UpperCamelCase(element.name)
134    if name in _java_reserved_types:
135      return name + '_'
136    return name
137  if mojom.IsInterfaceRequestKind(element) or mojom.IsAssociatedKind(element):
138    return GetNameForElement(element.kind)
139  if isinstance(element, (mojom.Method,
140                          mojom.Parameter,
141                          mojom.Field)):
142    return CamelCase(element.name)
143  if isinstance(element,  mojom.EnumValue):
144    return (GetNameForElement(element.enum) + '.' +
145            ConstantStyle(element.name))
146  if isinstance(element, (mojom.NamedValue,
147                          mojom.Constant,
148                          mojom.EnumField)):
149    return ConstantStyle(element.name)
150  raise Exception('Unexpected element: %s' % element)
151
152def GetInterfaceResponseName(method):
153  return UpperCamelCase(method.name + 'Response')
154
155def ParseStringAttribute(attribute):
156  assert isinstance(attribute, basestring)
157  return attribute
158
159def GetJavaTrueFalse(value):
160  return 'true' if value else 'false'
161
162def GetArrayNullabilityFlags(kind):
163    """Returns nullability flags for an array type, see Decoder.java.
164
165    As we have dedicated decoding functions for arrays, we have to pass
166    nullability information about both the array itself, as well as the array
167    element type there.
168    """
169    assert mojom.IsArrayKind(kind)
170    ARRAY_NULLABLE   = \
171        'org.chromium.mojo.bindings.BindingsHelper.ARRAY_NULLABLE'
172    ELEMENT_NULLABLE = \
173        'org.chromium.mojo.bindings.BindingsHelper.ELEMENT_NULLABLE'
174    NOTHING_NULLABLE = \
175        'org.chromium.mojo.bindings.BindingsHelper.NOTHING_NULLABLE'
176
177    flags_to_set = []
178    if mojom.IsNullableKind(kind):
179        flags_to_set.append(ARRAY_NULLABLE)
180    if mojom.IsNullableKind(kind.kind):
181        flags_to_set.append(ELEMENT_NULLABLE)
182
183    if not flags_to_set:
184        flags_to_set = [NOTHING_NULLABLE]
185    return ' | '.join(flags_to_set)
186
187
188def AppendEncodeDecodeParams(initial_params, context, kind, bit):
189  """ Appends standard parameters shared between encode and decode calls. """
190  params = list(initial_params)
191  if (kind == mojom.BOOL):
192    params.append(str(bit))
193  if mojom.IsReferenceKind(kind):
194    if mojom.IsArrayKind(kind):
195      params.append(GetArrayNullabilityFlags(kind))
196    else:
197      params.append(GetJavaTrueFalse(mojom.IsNullableKind(kind)))
198  if mojom.IsArrayKind(kind):
199    params.append(GetArrayExpectedLength(kind))
200  if mojom.IsInterfaceKind(kind):
201    params.append('%s.MANAGER' % GetJavaType(context, kind))
202  if mojom.IsArrayKind(kind) and mojom.IsInterfaceKind(kind.kind):
203    params.append('%s.MANAGER' % GetJavaType(context, kind.kind))
204  return params
205
206
207@contextfilter
208def DecodeMethod(context, kind, offset, bit):
209  def _DecodeMethodName(kind):
210    if mojom.IsArrayKind(kind):
211      return _DecodeMethodName(kind.kind) + 's'
212    if mojom.IsEnumKind(kind):
213      return _DecodeMethodName(mojom.INT32)
214    if mojom.IsInterfaceRequestKind(kind):
215      return 'readInterfaceRequest'
216    if mojom.IsInterfaceKind(kind):
217      return 'readServiceInterface'
218    if mojom.IsAssociatedInterfaceRequestKind(kind):
219      return 'readAssociatedInterfaceRequestNotSupported'
220    if mojom.IsAssociatedInterfaceKind(kind):
221      return 'readAssociatedServiceInterfaceNotSupported'
222    return _spec_to_decode_method[kind.spec]
223  methodName = _DecodeMethodName(kind)
224  params = AppendEncodeDecodeParams([ str(offset) ], context, kind, bit)
225  return '%s(%s)' % (methodName, ', '.join(params))
226
227@contextfilter
228def EncodeMethod(context, kind, variable, offset, bit):
229  params = AppendEncodeDecodeParams(
230      [ variable, str(offset) ], context, kind, bit)
231  return 'encode(%s)' % ', '.join(params)
232
233def GetPackage(module):
234  if module.attributes and 'JavaPackage' in module.attributes:
235    return ParseStringAttribute(module.attributes['JavaPackage'])
236  # Default package.
237  if module.namespace:
238    return 'org.chromium.' + module.namespace
239  return 'org.chromium'
240
241def GetNameForKind(context, kind):
242  def _GetNameHierachy(kind):
243    hierachy = []
244    if kind.parent_kind:
245      hierachy = _GetNameHierachy(kind.parent_kind)
246    hierachy.append(GetNameForElement(kind))
247    return hierachy
248
249  module = context.resolve('module')
250  elements = []
251  if GetPackage(module) != GetPackage(kind.module):
252    elements += [GetPackage(kind.module)]
253  elements += _GetNameHierachy(kind)
254  return '.'.join(elements)
255
256@contextfilter
257def GetJavaClassForEnum(context, kind):
258  return GetNameForKind(context, kind)
259
260def GetBoxedJavaType(context, kind, with_generics=True):
261  unboxed_type = GetJavaType(context, kind, False, with_generics)
262  if unboxed_type in _java_primitive_to_boxed_type:
263    return _java_primitive_to_boxed_type[unboxed_type]
264  return unboxed_type
265
266@contextfilter
267def GetJavaType(context, kind, boxed=False, with_generics=True):
268  if boxed:
269    return GetBoxedJavaType(context, kind)
270  if (mojom.IsStructKind(kind) or
271      mojom.IsInterfaceKind(kind) or
272      mojom.IsUnionKind(kind)):
273    return GetNameForKind(context, kind)
274  if mojom.IsInterfaceRequestKind(kind):
275    return ('org.chromium.mojo.bindings.InterfaceRequest<%s>' %
276            GetNameForKind(context, kind.kind))
277  if mojom.IsAssociatedInterfaceKind(kind):
278    return 'org.chromium.mojo.bindings.AssociatedInterfaceNotSupported'
279  if mojom.IsAssociatedInterfaceRequestKind(kind):
280    return 'org.chromium.mojo.bindings.AssociatedInterfaceRequestNotSupported'
281  if mojom.IsMapKind(kind):
282    if with_generics:
283      return 'java.util.Map<%s, %s>' % (
284          GetBoxedJavaType(context, kind.key_kind),
285          GetBoxedJavaType(context, kind.value_kind))
286    else:
287      return 'java.util.Map'
288  if mojom.IsArrayKind(kind):
289    return '%s[]' % GetJavaType(context, kind.kind, boxed, with_generics)
290  if mojom.IsEnumKind(kind):
291    return 'int'
292  return _spec_to_java_type[kind.spec]
293
294@contextfilter
295def DefaultValue(context, field):
296  assert field.default
297  if isinstance(field.kind, mojom.Struct):
298    assert field.default == 'default'
299    return 'new %s()' % GetJavaType(context, field.kind)
300  return '(%s) %s' % (
301      GetJavaType(context, field.kind),
302      ExpressionToText(context, field.default, kind_spec=field.kind.spec))
303
304@contextfilter
305def ConstantValue(context, constant):
306  return '(%s) %s' % (
307      GetJavaType(context, constant.kind),
308      ExpressionToText(context, constant.value, kind_spec=constant.kind.spec))
309
310@contextfilter
311def NewArray(context, kind, size):
312  if mojom.IsArrayKind(kind.kind):
313    return NewArray(context, kind.kind, size) + '[]'
314  return 'new %s[%s]' % (
315      GetJavaType(context, kind.kind, boxed=False, with_generics=False), size)
316
317@contextfilter
318def ExpressionToText(context, token, kind_spec=''):
319  def _TranslateNamedValue(named_value):
320    entity_name = GetNameForElement(named_value)
321    if named_value.parent_kind:
322      return GetJavaType(context, named_value.parent_kind) + '.' + entity_name
323    # Handle the case where named_value is a module level constant:
324    if not isinstance(named_value, mojom.EnumValue):
325      entity_name = (GetConstantsMainEntityName(named_value.module) + '.' +
326                      entity_name)
327    if GetPackage(named_value.module) == GetPackage(context.resolve('module')):
328      return entity_name
329    return GetPackage(named_value.module) + '.' + entity_name
330
331  if isinstance(token, mojom.NamedValue):
332    return _TranslateNamedValue(token)
333  if kind_spec.startswith('i') or kind_spec.startswith('u'):
334    # Add Long suffix to all integer literals.
335    number = ast.literal_eval(token.lstrip('+ '))
336    if not isinstance(number, (int, long)):
337      raise ValueError('got unexpected type %r for int literal %r' % (
338          type(number), token))
339    # If the literal is too large to fit a signed long, convert it to the
340    # equivalent signed long.
341    if number >= 2 ** 63:
342      number -= 2 ** 64
343    return '%dL' % number
344  if isinstance(token, mojom.BuiltinValue):
345    if token.value == 'double.INFINITY':
346      return 'java.lang.Double.POSITIVE_INFINITY'
347    if token.value == 'double.NEGATIVE_INFINITY':
348      return 'java.lang.Double.NEGATIVE_INFINITY'
349    if token.value == 'double.NAN':
350      return 'java.lang.Double.NaN'
351    if token.value == 'float.INFINITY':
352      return 'java.lang.Float.POSITIVE_INFINITY'
353    if token.value == 'float.NEGATIVE_INFINITY':
354      return 'java.lang.Float.NEGATIVE_INFINITY'
355    if token.value == 'float.NAN':
356      return 'java.lang.Float.NaN'
357  return token
358
359def GetArrayKind(kind, size = None):
360  if size is None:
361    return mojom.Array(kind)
362  else:
363    array = mojom.Array(kind, 0)
364    array.java_map_size = size
365    return array
366
367def GetArrayExpectedLength(kind):
368  if mojom.IsArrayKind(kind) and kind.length is not None:
369    return getattr(kind, 'java_map_size', str(kind.length))
370  else:
371    return 'org.chromium.mojo.bindings.BindingsHelper.UNSPECIFIED_ARRAY_LENGTH'
372
373def IsPointerArrayKind(kind):
374  if not mojom.IsArrayKind(kind):
375    return False
376  sub_kind = kind.kind
377  return mojom.IsObjectKind(sub_kind) and not mojom.IsUnionKind(sub_kind)
378
379def IsUnionArrayKind(kind):
380  if not mojom.IsArrayKind(kind):
381    return False
382  sub_kind = kind.kind
383  return mojom.IsUnionKind(sub_kind)
384
385def GetConstantsMainEntityName(module):
386  if module.attributes and 'JavaConstantsClassName' in module.attributes:
387    return ParseStringAttribute(module.attributes['JavaConstantsClassName'])
388  # This constructs the name of the embedding classes for module level constants
389  # by extracting the mojom's filename and prepending it to Constants.
390  return (UpperCamelCase(module.path.split('/')[-1].rsplit('.', 1)[0]) +
391          'Constants')
392
393def GetMethodOrdinalName(method):
394  return ConstantStyle(method.name) + '_ORDINAL'
395
396def HasMethodWithResponse(interface):
397  for method in interface.methods:
398    if method.response_parameters is not None:
399      return True
400  return False
401
402def HasMethodWithoutResponse(interface):
403  for method in interface.methods:
404    if method.response_parameters is None:
405      return True
406  return False
407
408@contextlib.contextmanager
409def TempDir():
410  dirname = tempfile.mkdtemp()
411  try:
412    yield dirname
413  finally:
414    shutil.rmtree(dirname)
415
416class Generator(generator.Generator):
417  def _GetJinjaExports(self):
418    return {
419      'package': GetPackage(self.module),
420    }
421
422  @staticmethod
423  def GetTemplatePrefix():
424    return "java_templates"
425
426  def GetFilters(self):
427    java_filters = {
428      'array_expected_length': GetArrayExpectedLength,
429      'array': GetArrayKind,
430      'constant_value': ConstantValue,
431      'decode_method': DecodeMethod,
432      'default_value': DefaultValue,
433      'encode_method': EncodeMethod,
434      'expression_to_text': ExpressionToText,
435      'has_method_without_response': HasMethodWithoutResponse,
436      'has_method_with_response': HasMethodWithResponse,
437      'interface_response_name': GetInterfaceResponseName,
438      'is_array_kind': mojom.IsArrayKind,
439      'is_any_handle_kind': mojom.IsAnyHandleKind,
440      "is_enum_kind": mojom.IsEnumKind,
441      'is_interface_request_kind': mojom.IsInterfaceRequestKind,
442      'is_map_kind': mojom.IsMapKind,
443      'is_nullable_kind': mojom.IsNullableKind,
444      'is_pointer_array_kind': IsPointerArrayKind,
445      'is_reference_kind': mojom.IsReferenceKind,
446      'is_struct_kind': mojom.IsStructKind,
447      'is_union_array_kind': IsUnionArrayKind,
448      'is_union_kind': mojom.IsUnionKind,
449      'java_class_for_enum': GetJavaClassForEnum,
450      'java_true_false': GetJavaTrueFalse,
451      'java_type': GetJavaType,
452      'method_ordinal_name': GetMethodOrdinalName,
453      'name': GetNameForElement,
454      'new_array': NewArray,
455      'ucc': lambda x: UpperCamelCase(x.name),
456    }
457    return java_filters
458
459  def _GetJinjaExportsForInterface(self, interface):
460    exports = self._GetJinjaExports()
461    exports.update({'interface': interface})
462    return exports
463
464  @UseJinja('enum.java.tmpl')
465  def _GenerateEnumSource(self, enum):
466    exports = self._GetJinjaExports()
467    exports.update({'enum': enum})
468    return exports
469
470  @UseJinja('struct.java.tmpl')
471  def _GenerateStructSource(self, struct):
472    exports = self._GetJinjaExports()
473    exports.update({'struct': struct})
474    return exports
475
476  @UseJinja('union.java.tmpl')
477  def _GenerateUnionSource(self, union):
478    exports = self._GetJinjaExports()
479    exports.update({'union': union})
480    return exports
481
482  @UseJinja('interface.java.tmpl')
483  def _GenerateInterfaceSource(self, interface):
484    return self._GetJinjaExportsForInterface(interface)
485
486  @UseJinja('interface_internal.java.tmpl')
487  def _GenerateInterfaceInternalSource(self, interface):
488    return self._GetJinjaExportsForInterface(interface)
489
490  @UseJinja('constants.java.tmpl')
491  def _GenerateConstantsSource(self, module):
492    exports = self._GetJinjaExports()
493    exports.update({'main_entity': GetConstantsMainEntityName(module),
494                    'constants': module.constants})
495    return exports
496
497  def _DoGenerateFiles(self):
498    fileutil.EnsureDirectoryExists(self.output_dir)
499
500    for struct in self.module.structs:
501      self.Write(self._GenerateStructSource(struct),
502                 '%s.java' % GetNameForElement(struct))
503
504    for union in self.module.unions:
505      self.Write(self._GenerateUnionSource(union),
506                 '%s.java' % GetNameForElement(union))
507
508    for enum in self.module.enums:
509      self.Write(self._GenerateEnumSource(enum),
510                 '%s.java' % GetNameForElement(enum))
511
512    for interface in self.module.interfaces:
513      self.Write(self._GenerateInterfaceSource(interface),
514                 '%s.java' % GetNameForElement(interface))
515      self.Write(self._GenerateInterfaceInternalSource(interface),
516                 '%s_Internal.java' % GetNameForElement(interface))
517
518    if self.module.constants:
519      self.Write(self._GenerateConstantsSource(self.module),
520                 '%s.java' % GetConstantsMainEntityName(self.module))
521
522  def GenerateFiles(self, unparsed_args):
523    # TODO(rockot): Support variant output for Java.
524    if self.variant:
525      raise Exception("Variants not supported in Java bindings.")
526
527    self.module.Stylize(generator.Stylizer())
528
529    parser = argparse.ArgumentParser()
530    parser.add_argument('--java_output_directory', dest='java_output_directory')
531    args = parser.parse_args(unparsed_args)
532    package_path = GetPackage(self.module).replace('.', '/')
533
534    # Generate the java files in a temporary directory and place a single
535    # srcjar in the output directory.
536    basename = "%s.srcjar" % self.module.path
537    zip_filename = os.path.join(self.output_dir, basename)
538    with TempDir() as temp_java_root:
539      self.output_dir = os.path.join(temp_java_root, package_path)
540      self._DoGenerateFiles();
541      build_utils.ZipDir(zip_filename, temp_java_root)
542
543    if args.java_output_directory:
544      # If requested, generate the java files directly into indicated directory.
545      self.output_dir = os.path.join(args.java_output_directory, package_path)
546      self._DoGenerateFiles();
547
548  def GetJinjaParameters(self):
549    return {
550      'lstrip_blocks': True,
551      'trim_blocks': True,
552    }
553
554  def GetGlobals(self):
555    return {
556      'namespace': self.module.namespace,
557      'module': self.module,
558    }
559