// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. library generate_vm_service_java; import 'package:markdown/markdown.dart'; import 'package:pub_semver/pub_semver.dart'; import '../common/generate_common.dart'; import '../common/parser.dart'; import '../common/src_gen_common.dart'; import 'src_gen_java.dart'; export 'src_gen_java.dart' show JavaGenerator; const String servicePackage = 'org.dartlang.vm.service'; const List simpleTypes = [ 'BigDecimal', 'boolean', 'int', 'String', 'double' ]; const vmServiceJavadoc = ''' {@link VmService} allows control of and access to information in a running Dart VM instance.
Launch the Dart VM with the arguments:
--pause_isolates_on_start
--observe
--enable-vm-service=some-port
where some-port is a port number of your choice which this client will use to communicate with the Dart VM. See https://dart.dev/tools/dart-vm for more details. Once the VM is running, instantiate a new {@link VmService} to connect to that VM via {@link VmService#connect(String)} or {@link VmService#localConnect(int)}.
{@link VmService} is not thread safe and should only be accessed from a single thread. In addition, a given VM should only be accessed from a single instance of {@link VmService}.
Calls to {@link VmService} should not be nested. More specifically, you should not make any calls to {@link VmService} from within any {@link Consumer} method. '''; late Api api; /// Convert documentation references /// from spec style of `className` to javadoc style {@link className} String? convertDocLinks(String? doc) { if (doc == null) return null; var sb = StringBuffer(); int start = 0; int end = doc.indexOf('['); while (end != -1) { sb.write(doc.substring(start, end)); start = end; end = doc.indexOf(']', start); if (end == -1) break; if (end == start + 1) { sb.write('[]'); } else { sb.write('{@link '); sb.write(doc.substring(start + 1, end)); sb.write('}'); } start = end + 1; end = doc.indexOf('[', start); } sb.write(doc.substring(start)); return sb.toString(); } String? _coerceRefType(String? typeName) { if (typeName == 'Class') typeName = 'ClassObj'; if (typeName == 'Error') typeName = 'ErrorObj'; if (typeName == 'Object') typeName = 'Obj'; if (typeName == '@Object') typeName = 'ObjRef'; if (typeName == 'Function') typeName = 'Func'; if (typeName == '@Function') typeName = 'FuncRef'; if (typeName!.startsWith('@')) typeName = '${typeName.substring(1)}Ref'; if (typeName == 'string') typeName = 'String'; if (typeName == 'bool') typeName = 'boolean'; if (typeName == 'num') typeName = 'BigDecimal'; if (typeName == 'map') typeName = 'Map'; return typeName; } class Api extends Member with ApiParseUtil { int? serviceMajor; int? serviceMinor; String? serviceVersion; List methods = []; List enums = []; List types = []; Map> streamIdMap = {}; final String scriptLocation; @override String? get docs => null; @override String get name => 'api'; Api(this.scriptLocation); void addProperty(String typeName, String propertyName, {String? javadoc}) { var t = types.firstWhere((t) => t!.name == typeName)!; for (var f in t.fields) { if (f.name == propertyName) { print('$typeName already has $propertyName field'); return; } } var f = TypeField(t, javadoc); f.name = propertyName; f.type = MemberType(); f.type.types = [TypeRef('String')]; t.fields.add(f); print('added $propertyName field to $typeName'); } void generate(JavaGenerator gen) { _setFileHeader(); // Set default value for unspecified property setDefaultValue('Instance', 'valueAsStringIsTruncated'); setDefaultValue('InstanceRef', 'valueAsStringIsTruncated'); // Hack to populate method argument docs for (var m in methods) { for (var a in m.args) { if (a.hasDocs) continue; var t = types.firstWhere((Type? t) => t!.name == a.type.name, orElse: () => null); if (t != null) { a.docs = t.docs; continue; } var e = enums.firstWhere((Enum? e) => e!.name == a.type.name, orElse: () => null); if (e != null) { a.docs = e.docs; continue; } } } gen.writeType('$servicePackage.VmService', scriptLocation, (TypeWriter writer) { writer.addImport('com.google.gson.JsonArray'); writer.addImport('com.google.gson.JsonObject'); writer.addImport('com.google.gson.JsonPrimitive'); writer.addImport('java.util.List'); writer.addImport('$servicePackage.consumer.*'); writer.addImport('$servicePackage.element.*'); writer.javadoc = vmServiceJavadoc; writer.superclassName = '$servicePackage.VmServiceBase'; for (String streamId in streamIdMap.keys.toList()..sort()) { String alias = streamId.toUpperCase(); while (alias.startsWith('_')) { alias = alias.substring(1); } writer.addField('${alias}_STREAM_ID', 'String', modifiers: 'public static final', value: '"$streamId"'); } writer.addField('versionMajor', 'int', modifiers: 'public static final', value: '$serviceMajor', javadoc: 'The major version number of the protocol supported by this client.'); writer.addField('versionMinor', 'int', modifiers: 'public static final', value: '$serviceMinor', javadoc: 'The minor version number of the protocol supported by this client.'); for (var m in methods) { m.generateVmServiceMethod(writer); if (m.hasOptionalArgs) { m.generateVmServiceMethod(writer, includeOptional: true); } } writer.addMethod('forwardResponse', [ JavaMethodArg('consumer', 'Consumer'), JavaMethodArg('responseType', 'String'), JavaMethodArg('json', 'JsonObject') ], (StatementWriter writer) { var generatedForwards = {}; var sorted = methods.toList() ..sort((m1, m2) { return m1.consumerTypeName.compareTo(m2.consumerTypeName); }); for (var m in sorted) { if (generatedForwards.add(m.consumerTypeName)) { m.generateVmServiceForward(writer); } } writer.addLine('if (consumer instanceof ServiceExtensionConsumer) {'); writer .addLine(' ((ServiceExtensionConsumer) consumer).received(json);'); writer.addLine(' return;'); writer.addLine('}'); writer.addLine('logUnknownResponse(consumer, json);'); }, modifiers: null, isOverride: true); writer.addMethod('convertMapToJsonObject', [ JavaMethodArg('map', 'Map') ], (StatementWriter writer) { writer.addLine('JsonObject obj = new JsonObject();'); writer.addLine('for (String key : map.keySet()) {'); writer.addLine(' obj.addProperty(key, map.get(key));'); writer.addLine('}'); writer.addLine('return obj;'); }, modifiers: 'private', returnType: 'JsonObject'); writer.addMethod( 'convertIterableToJsonArray', [JavaMethodArg('list', 'Iterable')], (StatementWriter writer) { writer.addLine('JsonArray arr = new JsonArray();'); writer.addLine('for (Object element : list) {'); writer.addLine(' arr.add(new JsonPrimitive(element.toString()));'); writer.addLine('}'); writer.addLine('return arr;'); }, modifiers: 'private', returnType: 'JsonArray'); }); for (var m in methods) { m.generateConsumerInterface(gen); } for (var t in types) { t!.generateElement(gen); } for (var e in enums) { e!.generateEnum(gen); } } Type? getType(String? name) => types.firstWhere((t) => t!.name == name, orElse: () => null); bool isEnumName(String? typeName) => enums.any((Enum? e) => e!.name == typeName); void parse(List nodes) { Version version = ApiParseUtil.parseVersionSemVer(nodes); serviceMajor = version.major; serviceMinor = version.minor; serviceVersion = '$serviceMajor.$serviceMinor'; // Look for h3 nodes // the pre following it is the definition // the optional p following that is the documentation String? h3Name; // TODO(helin24): This code does not capture documentation with more than // one paragraph or lists. It may help to check generate_dart to fix this. for (int i = 0; i < nodes.length; i++) { Node node = nodes[i]; if (isPre(node) && h3Name != null) { String definition = textForCode(node) .replaceAll('(string|Null)', 'string'); // this is terrible. String? docs; if (i + 1 < nodes.length && isPara(nodes[i + 1])) { Element p = nodes[++i] as Element; docs = collapseWhitespace(TextOutputVisitor.printText(p)); } _parse(h3Name, definition, docs); } else if (isH3(node)) { h3Name = textForElement(node); } else if (isHeader(node)) { h3Name = null; } else if (isPara(node)) { var children = (node as Element).children!; if (children.isNotEmpty && children.first is Text) { var text = children.expand((child) { if (child is Text) return [child.text]; return []; }).join(); if (text.startsWith('streamId |')) { _parseStreamIds(text); } } } } for (Type? type in types) { type!.calculateFieldOverrides(); } } void setDefaultValue(String typeName, String propertyName) { var type = types.firstWhere((t) => t!.name == typeName)!; var field = type.fields.firstWhere((f) => f.name == propertyName); field.defaultValue = 'false'; } void _parse(String name, String definition, [String? docs]) { name = replaceHTMLEntities(name.trim()); definition = replaceHTMLEntities(definition.trim()); if (docs != null) docs = replaceHTMLEntities(docs.trim()); if (definition.startsWith('class ')) { types.add(Type(this, scriptLocation, name, definition, docs)); } else if (name.substring(0, 1).toLowerCase() == name.substring(0, 1)) { methods.add(Method(name, scriptLocation, definition, docs)); } else if (definition.startsWith('enum ')) { enums.add(Enum(name, scriptLocation, definition, docs)); } else { throw 'unexpected entity: $name, $definition'; } } void _parseStreamIds(String text) { for (String line in text.split('\n')) { if (line.startsWith('streamId |')) continue; if (line.startsWith('---')) continue; var index = line.indexOf('|'); var streamId = line.substring(0, index).trim(); List eventTypes = List.from(line.substring(index + 1).split(',').map((t) => t.trim())); eventTypes.sort(); streamIdMap[streamId] = eventTypes; } } void _setFileHeader() { fileHeader = r'''/* * Copyright (c) 2015, the Dart project authors. * * Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.eclipse.org/legal/epl-v10.html * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ '''; } static String printNode(Node n) { if (n is Text) { return n.text; } else if (n is Element) { if (n.tag != 'h3') return n.tag; return '${n.tag}:[${n.children!.map((c) => printNode(c)).join(', ')}]'; } else { return '$n'; } } } class Enum extends Member { @override final String name; final String scriptLocation; @override final String? docs; List enums = []; Enum(this.name, this.scriptLocation, String definition, [this.docs]) { _parse(Tokenizer(definition).tokenize()); } String get elementTypeName => '$servicePackage.element.$name'; void generateEnum(JavaGenerator gen) { gen.writeType(elementTypeName, scriptLocation, (TypeWriter writer) { writer.javadoc = convertDocLinks(docs); writer.isEnum = true; enums.sort((v1, v2) => v1.name!.compareTo(v2.name!)); for (var value in enums) { writer.addEnumValue(value.name, javadoc: value.docs); } writer.addEnumValue('Unknown', javadoc: 'Represents a value returned by the VM' ' but unknown to this client.', isLast: true); }); } void _parse(Token? token) { EnumParser(token).parseInto(this); } } class EnumParser extends Parser { EnumParser(super.startToken); void parseInto(Enum e) { // enum ErrorKind { UnhandledException, Foo, Bar } // enum name { (comment* name ,)+ } expect('enum'); Token t = expectName(); validate(t.text == e.name, 'enum name ${e.name} equals ${t.text}'); expect('{'); while (!t.eof) { if (consume('}')) break; String? docs = collectComments(); t = expectName(); consume(','); e.enums.add(EnumValue(e, t.text, docs)); } } } class EnumValue extends Member { final Enum parent; @override final String? name; @override final String? docs; EnumValue(this.parent, this.name, [this.docs]); bool get isLast => parent.enums.last == this; } abstract class Member { String? get docs => null; bool get hasDocs => docs != null; String? get name; @override String toString() => name!; } class MemberType extends Member { List types = []; MemberType(); bool get hasSentinel => types.any((t) => t.name == 'Sentinel'); bool get isEnum => types.length == 1 && api.isEnumName(types.first.name); bool get isMultipleReturns => types.length > 1; List get subsetOfTypesThatAreSimple => types.where((TypeRef typeRef) => typeRef.isSimple).toList(); List get subsetOfTypesThatAreNotSimple => types.where((TypeRef typeRef) => !typeRef.isSimple).toList(); bool get isSimple => types.length == 1 && types.first.isSimple; bool get isValueAndSentinel => types.length == 2 && hasSentinel; @override String? get name { if (types.isEmpty) return ''; if (types.length == 1) return types.first.ref; return 'dynamic'; } TypeRef? get valueType { if (types.length == 1) return types.first; if (isValueAndSentinel) { return types.firstWhere((t) => t.name != 'Sentinel'); } return null; } void parse(Parser parser) { // foo|bar[]|baz // (@Instance|Sentinel)[] bool loop = true; bool isMulti = false; while (loop) { parser.consume('('); Token t = parser.expectName(); if (parser.consume(')')) isMulti = true; TypeRef ref = TypeRef(_coerceRefType(t.text)); types.add(ref); while (parser.consume('[')) { parser.expect(']'); if (isMulti) { for (var t in types) { t.arrayDepth++; } } else { ref.arrayDepth++; } } loop = parser.consume('|'); } } } class Method extends Member { @override final String name; final String scriptLocation; @override final String? docs; MemberType returnType = MemberType(); bool deprecated = false; List args = []; Method(this.name, this.scriptLocation, String definition, [this.docs]) { _parse(Tokenizer(definition).tokenize()); } String get consumerTypeName { String? prefix; if (returnType.isMultipleReturns) { prefix = titleCase(name); } else { prefix = returnType.types.first.javaBoxedName; } return '$servicePackage.consumer.${prefix}Consumer'; } bool get hasArgs => args.isNotEmpty; bool get hasOptionalArgs => args.any((MethodArg arg) => arg.optional); void generateConsumerInterface(JavaGenerator gen) { gen.writeType(consumerTypeName, scriptLocation, (TypeWriter writer) { writer.javadoc = convertDocLinks(returnType.docs); writer.interfaceNames.add('$servicePackage.consumer.Consumer'); writer.isInterface = true; for (var t in returnType.types) { writer.addImport(t.elementTypeName); writer.addMethod( 'received', [JavaMethodArg('response', t.elementTypeName)], null); } }); } void generateVmServiceForward(StatementWriter writer) { var consumerName = classNameFor(consumerTypeName); writer.addLine('if (consumer instanceof $consumerName) {'); List types = List.from(returnType.types.map((ref) => ref.type)); for (int index = 0; index < types.length; ++index) { types.addAll(types[index]!.subtypes); } types.sort((t1, t2) => t1!.name!.compareTo(t2!.name!)); for (var t in types) { var responseName = classNameFor(t!.elementTypeName!); writer.addLine(' if (responseType.equals("${t.rawName}")) {'); writer.addLine( ' (($consumerName) consumer).received(new $responseName(json));'); writer.addLine(' return;'); writer.addLine(' }'); } writer.addLine('}'); } void generateVmServiceMethod(TypeWriter writer, {includeOptional = false}) { // Update method docs var javadoc = StringBuffer(docs == null ? '' : docs!); bool firstParamDoc = true; for (var a in args) { if (!includeOptional && a.optional) continue; var paramDoc = StringBuffer(a.docs ?? ''); if (paramDoc.isEmpty) {} if (a.optional) { if (paramDoc.isNotEmpty) paramDoc.write(' '); paramDoc.write('This parameter is optional and may be null.'); } if (paramDoc.isNotEmpty) { if (firstParamDoc) { javadoc.writeln(); firstParamDoc = false; } javadoc.writeln('@param ${a.name} $paramDoc'); } } if (args.any((MethodArg arg) => (arg.type.name == 'Map'))) { writer.addImport('java.util.Map'); } List mthArgs = args; if (!includeOptional) { mthArgs = mthArgs.toList()..removeWhere((a) => a.optional); } List javaMethodArgs = List.from(mthArgs.map((a) => a.asJavaMethodArg)); javaMethodArgs .add(JavaMethodArg('consumer', classNameFor(consumerTypeName))); writer.addMethod(name, javaMethodArgs, (StatementWriter writer) { writer.addLine('final JsonObject params = new JsonObject();'); for (MethodArg arg in args) { if (!includeOptional && arg.optional) continue; var argName = arg.name; String op = arg.optional ? 'if ($argName != null) ' : ''; if (arg.isEnumType) { writer .addLine('${op}params.addProperty("$argName", $argName.name());'); } else if (arg.type.name == 'Map') { writer.addLine( '${op}params.add("$argName", convertMapToJsonObject($argName));'); } else if (arg.type.arrayDepth > 0) { writer.addLine( '${op}params.add("$argName", convertIterableToJsonArray($argName));'); } else if (name.startsWith('evaluate') && argName == 'expression') { // Special case the eval expression parameters. writer.addLine( '${op}params.addProperty("$argName", removeNewLines($argName));'); } else { writer.addLine('${op}params.addProperty("$argName", $argName);'); } } writer.addLine('request("$name", params, consumer);'); }, javadoc: javadoc.toString(), isDeprecated: deprecated); } void _parse(Token? token) { MethodParser(token).parseInto(this); } } class MethodArg extends Member { final Method parent; final TypeRef type; @override String? name; @override String? docs; bool optional = false; MethodArg(this.parent, this.type, this.name); JavaMethodArg get asJavaMethodArg { if (optional && type.ref == 'int') { return JavaMethodArg(name, 'Integer'); } if (optional && type.ref == 'double') { return JavaMethodArg(name, 'Double'); } if (optional && type.ref == 'boolean') { return JavaMethodArg(name, 'Boolean'); } return JavaMethodArg(name, type.ref); } /// TODO: Hacked enum arg type determination bool get isEnumType => name == 'step' || name == 'mode' || name == 'exceptionPauseMode'; } class MethodParser extends Parser { MethodParser(super.startToken); void parseInto(Method method) { // method is return type, name, (, args ) // args is type name, [optional], comma if (peek()?.text?.startsWith('@deprecated') ?? false) { advance(); expect('('); consumeString(); expect(')'); method.deprecated = true; } method.returnType.parse(this); Token t = expectName(); validate( t.text == method.name, 'method name ${method.name} equals ${t.text}'); expect('('); while (peek()!.text != ')') { Token type = expectName(); TypeRef ref = TypeRef(_coerceRefType(type.text)); if (peek()!.text == '[') { while (consume('[')) { expect(']'); ref.arrayDepth++; } } else if (peek()!.text == '<') { // handle generics expect('<'); ref.genericTypes = []; while (peek()!.text != '>') { Token genericTypeName = expectName(); ref.genericTypes!.add(TypeRef(_coerceRefType(genericTypeName.text))); consume(','); } expect('>'); } Token name = expectName(); MethodArg arg = MethodArg(method, ref, name.text); if (consume('[')) { expect('optional'); expect(']'); arg.optional = true; } method.args.add(arg); consume(','); } expect(')'); } } class TextOutputVisitor implements NodeVisitor { StringBuffer buf = StringBuffer(); bool _inRef = false; TextOutputVisitor(); @override String toString() => buf.toString().trim(); @override bool visitElementBefore(Element element) { if (element.tag == 'em') { buf.write('['); _inRef = true; } else if (element.tag == 'p') { // Nothing to do. } else if (element.tag == 'a') { // Nothing to do - we're not writing out refs (they won't resolve). } else if (element.tag == 'code') { buf.write(renderToHtml([element])); } else { print('unknown tag: ${element.tag}'); buf.write(renderToHtml([element])); } return true; } @override void visitText(Text text) { String? t = text.text; if (_inRef) t = _coerceRefType(t); buf.write(t); } @override void visitElementAfter(Element element) { if (element.tag == 'em') { buf.write(']'); _inRef = false; } else if (element.tag == 'p') { buf.write('\n\n'); } } static String printText(Node node) { TextOutputVisitor visitor = TextOutputVisitor(); node.accept(visitor); return visitor.toString(); } } class Type extends Member { final Api parent; final String scriptLocation; String? rawName; @override String? name; String? superName; @override final String? docs; List fields = []; Type(this.parent, this.scriptLocation, String categoryName, String definition, [this.docs]) { _parse(Tokenizer(definition).tokenize()); } String? get elementTypeName { if (isSimple) return null; return '$servicePackage.element.$name'; } bool get isRef => name!.endsWith('Ref'); bool get isResponse { if (superName == null) return false; if (name == 'Response' || superName == 'Response') return true; return parent.getType(superName)!.isResponse; } bool get isSimple => simpleTypes.contains(name); String? get jsonTypeName { if (name == 'ClassObj') return 'Class'; if (name == 'ErrorObj') return 'Error'; return name; } Iterable get subtypes => api.types.toList()..retainWhere((t) => t!.superName == name); void generateElement(JavaGenerator gen) { gen.writeType('$servicePackage.element.$name', scriptLocation, (TypeWriter writer) { if (fields.any((f) => f.type.types.any((t) => t.isArray))) { writer.addImport('com.google.gson.JsonObject'); } writer.addImport('com.google.gson.JsonObject'); writer.javadoc = convertDocLinks(docs); writer.superclassName = superName ?? 'Element'; writer.addConstructor( [JavaMethodArg('json', 'com.google.gson.JsonObject')], (StatementWriter writer) { writer.addLine('super(json);'); }); if (name == 'InstanceRef' || name == 'Instance') { writer.addMethod( 'isNull', [], (StatementWriter writer) { writer.addLine('return getKind() == InstanceKind.Null;'); }, returnType: 'boolean', javadoc: 'Returns whether this instance represents null.', ); } for (var field in fields) { field.generateAccessor(writer); } }); } List getAllFields() { if (superName == null) return fields; List all = []; all.insertAll(0, fields); Type? s = getSuper(); while (s != null) { all.insertAll(0, s.fields); s = s.getSuper(); } return all; } Type? getSuper() => superName == null ? null : api.getType(superName); bool hasField(String? name) { if (fields.any((field) => field.name == name)) return true; return getSuper()?.hasField(name) ?? false; } void _parse(Token? token) { TypeParser(token).parseInto(this); } void calculateFieldOverrides() { for (TypeField field in fields.toList()) { if (superName == null) continue; if (getSuper()!.hasField(field.name)) { field.setOverrides(); } } } } // @Instance|@Error|Sentinel evaluate( // string isolateId, // string targetId [optional], // string expression) class TypeField extends Member { static final Map _nameRemap = { 'const': 'isConst', 'final': 'isFinal', 'static': 'isStatic', 'abstract': 'isAbstract', 'super': 'superClass', 'class': 'classRef' }; final Type parent; final String? _docs; MemberType type = MemberType(); @override String? name; bool optional = false; String? defaultValue; bool overrides = false; TypeField(this.parent, this._docs); void setOverrides() { overrides = true; } String get accessorName { var remappedName = _nameRemap[name]; if (remappedName != null) { if (remappedName.startsWith('is')) return remappedName; } else { remappedName = name; } return 'get${titleCase(remappedName!)}'; } @override String? get docs { String str = _docs ?? ''; if (type.isMultipleReturns) { str += '\n\n@return one of ' '${joinLast(type.types.map((t) => '$t'), ', ', ' or ')}'; str = str.trim(); } if (optional) { str += '\n\nCan return null.'; str = str.trim(); } return str; } void generateAccessor(TypeWriter writer) { if (type.isMultipleReturns && !type.isValueAndSentinel) { writer.addMethod(accessorName, [], (StatementWriter w) { w.addImport('com.google.gson.JsonElement'); w.addLine('final JsonElement elem = json.get("$name");'); w.addLine('if (elem == null) return null;\n'); final subsetOfTypesThatAreSimple = type.subsetOfTypesThatAreSimple; final subsetOfTypesThatAreNotSimple = type.subsetOfTypesThatAreNotSimple; if (subsetOfTypesThatAreSimple.isNotEmpty) { w.addImport('com.google.gson.JsonPrimitive'); w.addLine('if (elem.isJsonPrimitive()) {'); w.addLine('final JsonPrimitive p = (JsonPrimitive) elem;'); for (TypeRef t in subsetOfTypesThatAreSimple) { if (t.name == 'boolean') { w.addLine('if (p.isBoolean()) return p.getAsBoolean();'); } else if (t.name == 'int') { w.addLine('if (p.isNumber()) return p.getAsInt();'); } else if (t.name == 'String') { w.addLine('if (p.isString()) return p.getAsString();'); } } w.addLine('}'); } if (subsetOfTypesThatAreNotSimple.isNotEmpty) { w.addImport('com.google.gson.JsonObject'); w.addLine('if (elem.isJsonObject()) {'); w.addLine('final JsonObject o = (JsonObject) elem;'); for (TypeRef t in subsetOfTypesThatAreNotSimple) { String refName = t.name!; if (refName.endsWith('Ref')) { refName = '@${refName.substring(0, refName.length - 3)}'; } w.addLine('if (o.get("type").getAsString().equals("$refName")) ' 'return new ${t.name}(o);'); } w.addLine('}'); } w.addLine('return null;'); }, javadoc: docs, returnType: 'Object'); } else { String? returnType = type.valueType!.ref; if (name == 'timestamp') { returnType = 'long'; } writer.addMethod( accessorName, [], (StatementWriter writer) { type.valueType!.generateAccessStatements( writer, name, canBeSentinel: type.isValueAndSentinel, defaultValue: defaultValue, optional: optional, ); }, javadoc: docs, returnType: returnType, isOverride: overrides, ); } } } class TypeParser extends Parser { TypeParser(super.startToken); void parseInto(Type type) { // class ClassList extends Response { // // Docs here. // @Class[] classes [optional]; // } expect('class'); Token t = expectName(); type.rawName = t.text; type.name = _coerceRefType(type.rawName); if (consume('extends')) { t = expectName(); type.superName = _coerceRefType(t.text); } expect('{'); while (peek()!.text != '}') { TypeField field = TypeField(type, collectComments()); field.type.parse(this); field.name = expectName().text; if (consume('[')) { expect('optional'); expect(']'); field.optional = true; } type.fields.add(field); expect(';'); } expect('}'); } } class TypeRef { String? name; int arrayDepth = 0; List? genericTypes; TypeRef(this.name); String? get elementTypeName { if (isSimple) return null; return '$servicePackage.element.$name'; } bool get isArray => arrayDepth > 0; /// Hacked enum determination bool get isEnum => name!.endsWith('Kind') || name!.endsWith('Mode'); bool get isSimple => simpleTypes.contains(name); String? get javaBoxedName { if (name == 'boolean') return 'Boolean'; if (name == 'int') return 'Integer'; if (name == 'double') return 'Double'; return name; } String? get ref { if (genericTypes != null) { return '$name<${genericTypes!.join(', ')}>'; } else if (isSimple) { if (arrayDepth == 2) return 'List>'; if (arrayDepth == 1) return 'List<$javaBoxedName>'; } else { if (arrayDepth == 2) return 'ElementList>'; if (arrayDepth == 1) return 'ElementList<$javaBoxedName>'; } return name; } Type? get type => api.types.firstWhere((t) => t!.name == name); void generateAccessStatements( StatementWriter writer, String? propertyName, { bool canBeSentinel = false, String? defaultValue, bool optional = false, }) { if (name == 'boolean') { if (isArray) { print('skipped accessor body for $propertyName'); } else { if (defaultValue != null) { writer.addImport('com.google.gson.JsonElement'); writer.addLine('final JsonElement elem = json.get("$propertyName");'); writer.addLine( 'return elem != null ? elem.getAsBoolean() : $defaultValue;'); } else { writer.addLine('return getAsBoolean("$propertyName");'); } } } else if (name == 'int') { if (arrayDepth > 1) { writer.addImport('java.util.List'); writer.addLine('return getListListInt("$propertyName");'); } else if (arrayDepth == 1) { writer.addImport('java.util.List'); writer.addLine('return getListInt("$propertyName");'); } else { if (propertyName == 'timestamp') { writer.addLine('return json.get("$propertyName") == null ? ' '-1 : json.get("$propertyName").getAsLong();'); } else { writer.addLine('return getAsInt("$propertyName");'); } } } else if (name == 'double') { writer.addLine('return json.get("$propertyName") == null ? ' '0.0 : json.get("$propertyName").getAsDouble();'); } else if (name == 'BigDecimal') { if (isArray) { print('skipped accessor body for $propertyName'); } else { writer.addImport('java.math.BigDecimal'); writer.addLine('return json.get("$propertyName").getAsBigDecimal();'); } } else if (name == 'String') { if (isArray) { writer.addImport('java.util.List'); if (optional) { writer.addLine('return json.get("$propertyName") == null ? ' 'null : getListString("$propertyName");'); } else { writer.addLine('return getListString("$propertyName");'); } } else { writer.addLine('return getAsString("$propertyName");'); } } else if (isEnum) { if (isArray) { print('skipped accessor body for $propertyName'); } else { if (optional) { writer.addLine('if (json.get("$propertyName") == null) return null;'); writer.addLine(''); } writer.addImport('com.google.gson.JsonElement'); writer.addLine('final JsonElement value = json.get("$propertyName");'); writer.addLine('try {'); writer.addLine(' return value == null ? $name.Unknown' ' : $name.valueOf(value.getAsString());'); writer.addLine('} catch (IllegalArgumentException e) {'); writer.addLine(' return $name.Unknown;'); writer.addLine('}'); } } else { if (arrayDepth > 1) { print('skipped accessor body for $propertyName'); } else if (arrayDepth == 1) { writer.addImport('com.google.gson.JsonArray'); if (optional) { writer.addLine('if (json.get("$propertyName") == null) return null;'); writer.addLine(''); } writer.addLine( 'return new ElementList<$javaBoxedName>(json.get("$propertyName").getAsJsonArray()) {'); writer.addLine(' @Override'); writer.addLine( ' protected $javaBoxedName basicGet(JsonArray array, int index) {'); writer.addLine( ' return new $javaBoxedName(array.get(index).getAsJsonObject());'); writer.addLine(' }'); writer.addLine('};'); } else { if (canBeSentinel) { writer.addImport('com.google.gson.JsonElement'); writer.addLine('final JsonElement elem = json.get("$propertyName");'); writer.addLine('if (!elem.isJsonObject()) return null;'); writer.addLine('final JsonObject child = elem.getAsJsonObject();'); writer .addLine('final String type = child.get("type").getAsString();'); writer.addLine('if ("Sentinel".equals(type)) return null;'); writer.addLine('return new $name(child);'); } else { if (optional) { writer.addLine( 'JsonObject obj = (JsonObject) json.get("$propertyName");'); writer.addLine('if (obj == null) return null;'); if ((name != 'InstanceRef') && (name != 'Instance')) { writer.addLine( 'final String type = json.get("type").getAsString();'); writer.addLine( 'if ("Instance".equals(type) || "@Instance".equals(type)) {'); writer.addLine( ' final String kind = json.get("kind").getAsString();'); writer.addLine(' if ("Null".equals(kind)) return null;'); writer.addLine('}'); } writer.addLine('return new $name(obj);'); } else { writer.addLine( 'return new $name((JsonObject) json.get("$propertyName"));'); } } } } } @override String toString() => ref!; }