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