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