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.generate.generator as generator
19import mojom.generate.module as mojom
20from mojom.generate.template_expander import UseJinja
21
22
23GENERATOR_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
93def 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
102def UpperCamelCase(name):
103  return ''.join([x.capitalize() for x in NameToComponent(name)])
104
105def CamelCase(name):
106  uccc = UpperCamelCase(name)
107  return uccc[0].lower() + uccc[1:]
108
109def 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
115def 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
133def GetInterfaceResponseName(method):
134  return UpperCamelCase(method.name + 'Response')
135
136def ParseStringAttribute(attribute):
137  assert isinstance(attribute, basestring)
138  return attribute
139
140def GetJavaTrueFalse(value):
141  return 'true' if value else 'false'
142
143def 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
169def 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
193def 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
209def EncodeMethod(context, kind, variable, offset, bit):
210  params = AppendEncodeDecodeParams(
211      [ variable, str(offset) ], context, kind, bit)
212  return 'encode(%s)' % ', '.join(params)
213
214def 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
220def 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
235def 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
242def 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
257def 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
267def 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
273def 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
279def 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
320def IsPointerArrayKind(kind):
321  if not mojom.IsAnyArrayKind(kind):
322    return False
323  sub_kind = kind.kind
324  return mojom.IsObjectKind(sub_kind)
325
326def GetResponseStructFromMethod(method):
327  return generator.GetDataHeader(
328      False, generator.GetResponseStructFromMethod(method))
329
330def GetStructFromMethod(method):
331  return generator.GetDataHeader(
332      False, generator.GetStructFromMethod(method))
333
334def 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
342def GetMethodOrdinalName(method):
343  return ConstantStyle(method.name) + '_ORDINAL'
344
345def HasMethodWithResponse(interface):
346  for method in interface.methods:
347    if method.response_parameters:
348      return True
349  return False
350
351def HasMethodWithoutResponse(interface):
352  for method in interface.methods:
353    if not method.response_parameters:
354      return True
355  return False
356
357@contextlib.contextmanager
358def TempDir():
359  dirname = tempfile.mkdtemp()
360  try:
361    yield dirname
362  finally:
363    shutil.rmtree(dirname)
364
365def 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
373class 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