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 tempfile
14import zipfile
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
23
24GENERATOR_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
92def 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
101def UpperCamelCase(name):
102  return ''.join([x.capitalize() for x in NameToComponent(name)])
103
104def CamelCase(name):
105  uccc = UpperCamelCase(name)
106  return uccc[0].lower() + uccc[1:]
107
108def 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
117def 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
136def GetInterfaceResponseName(method):
137  return UpperCamelCase(method.name + 'Response')
138
139def ParseStringAttribute(attribute):
140  assert isinstance(attribute, basestring)
141  return attribute
142
143def GetJavaTrueFalse(value):
144  return 'true' if value else 'false'
145
146def 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
172def 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
192def 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
212def EncodeMethod(context, kind, variable, offset, bit):
213  params = AppendEncodeDecodeParams(
214      [ variable, str(offset) ], context, kind, bit)
215  return 'encode(%s)' % ', '.join(params)
216
217def 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
225def 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
241def GetJavaClassForEnum(context, kind):
242  return GetNameForKind(context, kind)
243
244def 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
251def 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
279def 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
289def 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
295def 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
302def 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
343def 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
351def 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
357def 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
363def IsUnionArrayKind(kind):
364  if not mojom.IsArrayKind(kind):
365    return False
366  sub_kind = kind.kind
367  return mojom.IsUnionKind(sub_kind)
368
369def 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
377def GetMethodOrdinalName(method):
378  return ConstantStyle(method.name) + '_ORDINAL'
379
380def HasMethodWithResponse(interface):
381  for method in interface.methods:
382    if method.response_parameters is not None:
383      return True
384  return False
385
386def 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
393def TempDir():
394  dirname = tempfile.mkdtemp()
395  try:
396    yield dirname
397  finally:
398    shutil.rmtree(dirname)
399
400def 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
408class 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