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