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