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