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