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