blob: 9ef243d14b0eadba148b1d7387fed442f3371075 [file] [log] [blame]
// 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<String> simpleTypes = [
'BigDecimal',
'boolean',
'int',
'String',
'double'
];
const vmServiceJavadoc = '''
{@link VmService} allows control of and access to information in a running
Dart VM instance.
<br/>
Launch the Dart VM with the arguments:
<pre>
--pause_isolates_on_start
--observe
--enable-vm-service=some-port
</pre>
where <strong>some-port</strong> 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)}.
<br/>
{@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}.
<br/>
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<Method> methods = [];
List<Enum?> enums = [];
List<Type?> types = [];
Map<String, List<String>> streamIdMap = {};
String? get docs => null;
String get name => 'api';
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', (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 = Set<String>();
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<String, String>')
], (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<Node> 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;
for (int i = 0; i < nodes.length; i++) {
Node node = nodes[i];
if (isPre(node) && h3Name != null) {
String definition = textForCode(node);
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<String>((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 = name.trim();
definition = definition.trim();
// clean markdown introduced changes
definition = definition.replaceAll('&lt;', '<').replaceAll('&gt;', '>');
if (docs != null) docs = docs.trim();
if (definition.startsWith('class ')) {
types.add(Type(this, name, definition, docs));
} else if (name.substring(0, 1).toLowerCase() == name.substring(0, 1)) {
methods.add(Method(name, definition, docs));
} else if (definition.startsWith('enum ')) {
enums.add(Enum(name, 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<String> 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 {
final String name;
final String? docs;
List<EnumValue> enums = [];
Enum(this.name, String definition, [this.docs]) {
_parse(Tokenizer(definition).tokenize());
}
String get elementTypeName => '$servicePackage.element.$name';
void generateEnum(JavaGenerator gen) {
gen.writeType(elementTypeName, (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(Token? startToken) : 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;
final String? name;
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;
String toString() => name!;
}
class MemberType extends Member {
List<TypeRef> 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;
bool get isSimple => types.length == 1 && types.first.isSimple;
bool get isValueAndSentinel => types.length == 2 && hasSentinel;
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) {
types.forEach((t) => t.arrayDepth++);
} else {
ref.arrayDepth++;
}
}
loop = parser.consume('|');
}
}
}
class Method extends Member {
final String name;
final String? docs;
MemberType returnType = MemberType();
List<MethodArg> args = [];
Method(this.name, 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, (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<Type?> 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<MethodArg> mthArgs = args;
if (!includeOptional) {
mthArgs = mthArgs.toList()..removeWhere((a) => a.optional);
}
List<JavaMethodArg> 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());
}
void _parse(Token? token) {
MethodParser(token).parseInto(this);
}
}
class MethodArg extends Member {
final Method parent;
final TypeRef type;
String? name;
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';
}
class MethodParser extends Parser {
MethodParser(Token? startToken) : super(startToken);
void parseInto(Method method) {
// method is return type, name, (, args )
// args is type name, [optional], comma
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();
String toString() => buf.toString().trim();
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 <a> 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;
}
void visitText(Text text) {
String? t = text.text;
if (_inRef) t = _coerceRefType(t);
buf.write(t);
}
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;
String? rawName;
String? name;
String? superName;
final String? docs;
List<TypeField> fields = [];
Type(this.parent, 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<Type?> get subtypes =>
api.types.toList()..retainWhere((t) => t!.superName == name);
void generateElement(JavaGenerator gen) {
gen.writeType('$servicePackage.element.$name', (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>[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<TypeField> getAllFields() {
if (superName == null) return fields;
List<TypeField> 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<String, String> _nameRemap = {
'const': 'isConst',
'final': 'isFinal',
'static': 'isStatic',
'abstract': 'isAbstract',
'super': 'superClass',
'class': 'classRef'
};
final Type parent;
final String? _docs;
MemberType type = MemberType();
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!)}';
}
String? get docs {
String str = _docs == null ? '' : _docs!;
if (type.isMultipleReturns) {
str += '\n\n@return one of '
'${joinLast(type.types.map((t) => '<code>${t}</code>'), ', ', ' or ')}';
str = str.trim();
}
if (optional) {
str += '\n\nCan return <code>null</code>.';
str = str.trim();
}
return str;
}
void generateAccessor(TypeWriter writer) {
if (type.isMultipleReturns && !type.isValueAndSentinel) {
writer.addMethod(accessorName, [], (StatementWriter w) {
w.addImport('com.google.gson.JsonObject');
w.addLine('final JsonObject elem = (JsonObject)json.get("$name");');
w.addLine('if (elem == null) return null;\n');
for (TypeRef t in type.types) {
String refName = t.name!;
if (refName.endsWith('Ref')) {
refName = "@" + refName.substring(0, refName.length - 3);
}
w.addLine('if (elem.get("type").getAsString().equals("${refName}")) '
'return new ${t.name}(elem);');
}
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(Token? startToken) : 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<TypeRef>? 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<List<${javaBoxedName}>>';
if (arrayDepth == 1) return 'List<${javaBoxedName}>';
} else {
if (arrayDepth == 2) return 'ElementList<ElementList<${javaBoxedName}>>';
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"));');
}
}
}
}
}
String toString() => ref!;
}