blob: e414d366e14159ac4c320cf3c1afadfc9ffd72e4 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'ast.dart';
import 'functional.dart';
import 'generator_tools.dart';
import 'pigeon_lib.dart' show TaskQueueType;
/// Options that control how Java code will be generated.
class JavaOptions {
/// Creates a [JavaOptions] object
const JavaOptions({
this.className,
this.package,
this.copyrightHeader,
this.useGeneratedAnnotation,
});
/// The name of the class that will house all the generated classes.
final String? className;
/// The package where the generated class will live.
final String? package;
/// A copyright header that will get prepended to generated code.
final Iterable<String>? copyrightHeader;
/// Determines if the `javax.annotation.Generated` is used in the output. This
/// is false by default since that dependency isn't available in plugins by
/// default .
final bool? useGeneratedAnnotation;
/// Creates a [JavaOptions] from a Map representation where:
/// `x = JavaOptions.fromMap(x.toMap())`.
static JavaOptions fromMap(Map<String, Object> map) {
final Iterable<dynamic>? copyrightHeader =
map['copyrightHeader'] as Iterable<dynamic>?;
return JavaOptions(
className: map['className'] as String?,
package: map['package'] as String?,
copyrightHeader: copyrightHeader?.cast<String>(),
useGeneratedAnnotation: map['useGeneratedAnnotation'] as bool?,
);
}
/// Converts a [JavaOptions] to a Map representation where:
/// `x = JavaOptions.fromMap(x.toMap())`.
Map<String, Object> toMap() {
final Map<String, Object> result = <String, Object>{
if (className != null) 'className': className!,
if (package != null) 'package': package!,
if (copyrightHeader != null) 'copyrightHeader': copyrightHeader!,
if (useGeneratedAnnotation != null)
'useGeneratedAnnotation': useGeneratedAnnotation!,
};
return result;
}
/// Overrides any non-null parameters from [options] into this to make a new
/// [JavaOptions].
JavaOptions merge(JavaOptions options) {
return JavaOptions.fromMap(mergeMaps(toMap(), options.toMap()));
}
}
/// Calculates the name of the codec that will be generated for [api].
String _getCodecName(Api api) => '${api.name}Codec';
/// Converts an expression that evaluates to an nullable int to an expression
/// that evaluates to a nullable enum.
String _intToEnum(String expression, String enumName) =>
'$expression == null ? null : $enumName.values()[(int)$expression]';
/// Writes the codec class that will be used by [api].
/// Example:
/// private static class FooCodec extends StandardMessageCodec {...}
void _writeCodec(Indent indent, Api api, Root root) {
final String codecName = _getCodecName(api);
indent.write('private static class $codecName extends StandardMessageCodec ');
indent.scoped('{', '}', () {
indent
.writeln('public static final $codecName INSTANCE = new $codecName();');
indent.writeln('private $codecName() {}');
if (getCodecClasses(api, root).isNotEmpty) {
indent.writeln('@Override');
indent.write(
'protected Object readValueOfType(byte type, ByteBuffer buffer) ');
indent.scoped('{', '}', () {
indent.write('switch (type) ');
indent.scoped('{', '}', () {
for (final EnumeratedClass customClass
in getCodecClasses(api, root)) {
indent.write('case (byte)${customClass.enumeration}: ');
indent.writeScoped('', '', () {
indent.writeln(
'return ${customClass.name}.fromMap((Map<String, Object>) readValue(buffer));');
});
}
indent.write('default:');
indent.writeScoped('', '', () {
indent.writeln('return super.readValueOfType(type, buffer);');
});
});
});
indent.writeln('@Override');
indent.write(
'protected void writeValue(ByteArrayOutputStream stream, Object value) ');
indent.writeScoped('{', '}', () {
for (final EnumeratedClass customClass in getCodecClasses(api, root)) {
indent.write('if (value instanceof ${customClass.name}) ');
indent.scoped('{', '} else ', () {
indent.writeln('stream.write(${customClass.enumeration});');
indent.writeln(
'writeValue(stream, ((${customClass.name}) value).toMap());');
});
}
indent.scoped('{', '}', () {
indent.writeln('super.writeValue(stream, value);');
});
});
}
});
}
/// Write the java code that represents a host [Api], [api].
/// Example:
/// public interface Foo {
/// int add(int x, int y);
/// static void setup(BinaryMessenger binaryMessenger, Foo api) {...}
/// }
void _writeHostApi(Indent indent, Api api, Root root) {
assert(api.location == ApiLocation.host);
bool isEnum(TypeDeclaration type) =>
root.enums.map((Enum e) => e.name).contains(type.baseName);
/// Write a method in the interface.
/// Example:
/// int add(int x, int y);
void writeInterfaceMethod(final Method method) {
final String returnType = method.isAsynchronous
? 'void'
: _nullsafeJavaTypeForDartType(method.returnType);
final List<String> argSignature = <String>[];
if (method.arguments.isNotEmpty) {
final Iterable<String> argTypes = method.arguments
.map((NamedType e) => _nullsafeJavaTypeForDartType(e.type));
final Iterable<String> argNames =
method.arguments.map((NamedType e) => e.name);
argSignature
.addAll(map2(argTypes, argNames, (String argType, String argName) {
return '$argType $argName';
}));
}
if (method.isAsynchronous) {
final String resultType = method.returnType.isVoid
? 'Void'
: _javaTypeForDartType(method.returnType);
argSignature.add('Result<$resultType> result');
}
indent.writeln('$returnType ${method.name}(${argSignature.join(', ')});');
}
/// Write a static setup function in the interface.
/// Example:
/// static void setup(BinaryMessenger binaryMessenger, Foo api) {...}
void writeMethodSetup(final Method method) {
final String channelName = makeChannelName(api, method);
indent.write('');
indent.scoped('{', '}', () {
String? taskQueue;
if (method.taskQueueType != TaskQueueType.serial) {
taskQueue = 'taskQueue';
indent.writeln(
'BinaryMessenger.TaskQueue taskQueue = binaryMessenger.makeBackgroundTaskQueue();');
}
indent.writeln('BasicMessageChannel<Object> channel =');
indent.inc();
indent.inc();
indent.write(
'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec()');
if (taskQueue != null) {
indent.addln(', $taskQueue);');
} else {
indent.addln(');');
}
indent.dec();
indent.dec();
indent.write('if (api != null) ');
indent.scoped('{', '} else {', () {
indent.write('channel.setMessageHandler((message, reply) -> ');
indent.scoped('{', '});', () {
final String returnType = method.returnType.isVoid
? 'Void'
: _javaTypeForDartType(method.returnType);
indent.writeln('Map<String, Object> wrapped = new HashMap<>();');
indent.write('try ');
indent.scoped('{', '}', () {
final List<String> methodArgument = <String>[];
if (method.arguments.isNotEmpty) {
indent.writeln(
'ArrayList<Object> args = (ArrayList<Object>)message;');
enumerate(method.arguments, (int index, NamedType arg) {
// The StandardMessageCodec can give us [Integer, Long] for
// a Dart 'int'. To keep things simple we just use 64bit
// longs in Pigeon with Java.
final bool isInt = arg.type.baseName == 'int';
final String argType =
isInt ? 'Number' : _javaTypeForDartType(arg.type);
final String argName = _getSafeArgumentName(index, arg);
final String argExpression = isInt
? '($argName == null) ? null : $argName.longValue()'
: argName;
String accessor = 'args.get($index)';
if (isEnum(arg.type)) {
accessor = _intToEnum(accessor, arg.type.baseName);
} else {
accessor = '($argType)$accessor';
}
indent.writeln('$argType $argName = $accessor;');
if (!arg.type.isNullable) {
indent.write('if ($argName == null) ');
indent.scoped('{', '}', () {
indent.writeln(
'throw new NullPointerException("$argName unexpectedly null.");');
});
}
methodArgument.add(argExpression);
});
}
if (method.isAsynchronous) {
final String resultValue =
method.returnType.isVoid ? 'null' : 'result';
const String resultName = 'resultCallback';
indent.format('''
Result<$returnType> $resultName = new Result<$returnType>() {
\tpublic void success($returnType result) {
\t\twrapped.put("${Keys.result}", $resultValue);
\t\treply.reply(wrapped);
\t}
\tpublic void error(Throwable error) {
\t\twrapped.put("${Keys.error}", wrapError(error));
\t\treply.reply(wrapped);
\t}
};
''');
methodArgument.add(resultName);
}
final String call =
'api.${method.name}(${methodArgument.join(', ')})';
if (method.isAsynchronous) {
indent.writeln('$call;');
} else if (method.returnType.isVoid) {
indent.writeln('$call;');
indent.writeln('wrapped.put("${Keys.result}", null);');
} else {
indent.writeln('$returnType output = $call;');
indent.writeln('wrapped.put("${Keys.result}", output);');
}
});
indent.write('catch (Error | RuntimeException exception) ');
indent.scoped('{', '}', () {
indent
.writeln('wrapped.put("${Keys.error}", wrapError(exception));');
if (method.isAsynchronous) {
indent.writeln('reply.reply(wrapped);');
}
});
if (!method.isAsynchronous) {
indent.writeln('reply.reply(wrapped);');
}
});
});
indent.scoped(null, '}', () {
indent.writeln('channel.setMessageHandler(null);');
});
});
}
indent.writeln(
'/** Generated interface from Pigeon that represents a handler of messages from Flutter.*/');
indent.write('public interface ${api.name} ');
indent.scoped('{', '}', () {
api.methods.forEach(writeInterfaceMethod);
indent.addln('');
final String codecName = _getCodecName(api);
indent.format('''
/** The codec used by ${api.name}. */
static MessageCodec<Object> getCodec() {
\treturn $codecName.INSTANCE;
}
''');
indent.writeln(
'/** Sets up an instance of `${api.name}` to handle messages through the `binaryMessenger`. */');
indent.write(
'static void setup(BinaryMessenger binaryMessenger, ${api.name} api) ');
indent.scoped('{', '}', () {
api.methods.forEach(writeMethodSetup);
});
});
}
String _getArgumentName(int count, NamedType argument) =>
argument.name.isEmpty ? 'arg$count' : argument.name;
/// Returns an argument name that can be used in a context where it is possible to collide.
String _getSafeArgumentName(int count, NamedType argument) =>
'${_getArgumentName(count, argument)}Arg';
/// Writes the code for a flutter [Api], [api].
/// Example:
/// public static class Foo {
/// public Foo(BinaryMessenger argBinaryMessenger) {...}
/// public interface Reply<T> {
/// void reply(T reply);
/// }
/// public int add(int x, int y, Reply<int> callback) {...}
/// }
void _writeFlutterApi(Indent indent, Api api) {
assert(api.location == ApiLocation.flutter);
indent.writeln(
'/** Generated class from Pigeon that represents Flutter messages that can be called from Java.*/');
indent.write('public static class ${api.name} ');
indent.scoped('{', '}', () {
indent.writeln('private final BinaryMessenger binaryMessenger;');
indent.write('public ${api.name}(BinaryMessenger argBinaryMessenger)');
indent.scoped('{', '}', () {
indent.writeln('this.binaryMessenger = argBinaryMessenger;');
});
indent.write('public interface Reply<T> ');
indent.scoped('{', '}', () {
indent.writeln('void reply(T reply);');
});
final String codecName = _getCodecName(api);
indent.format('''
static MessageCodec<Object> getCodec() {
\treturn $codecName.INSTANCE;
}
''');
for (final Method func in api.methods) {
final String channelName = makeChannelName(api, func);
final String returnType = func.returnType.isVoid
? 'Void'
: _javaTypeForDartType(func.returnType);
String sendArgument;
if (func.arguments.isEmpty) {
indent.write('public void ${func.name}(Reply<$returnType> callback) ');
sendArgument = 'null';
} else {
final Iterable<String> argTypes = func.arguments
.map((NamedType e) => _nullsafeJavaTypeForDartType(e.type));
final Iterable<String> argNames =
indexMap(func.arguments, _getSafeArgumentName);
sendArgument =
'new ArrayList<Object>(Arrays.asList(${argNames.join(', ')}))';
final String argsSignature =
map2(argTypes, argNames, (String x, String y) => '$x $y')
.join(', ');
indent.write(
'public void ${func.name}($argsSignature, Reply<$returnType> callback) ');
}
indent.scoped('{', '}', () {
const String channel = 'channel';
indent.writeln('BasicMessageChannel<Object> $channel =');
indent.inc();
indent.inc();
indent.writeln(
'new BasicMessageChannel<>(binaryMessenger, "$channelName", getCodec());');
indent.dec();
indent.dec();
indent.write('$channel.send($sendArgument, channelReply -> ');
indent.scoped('{', '});', () {
if (func.returnType.isVoid) {
indent.writeln('callback.reply(null);');
} else {
const String output = 'output';
indent.writeln('@SuppressWarnings("ConstantConditions")');
if (func.returnType.baseName == 'int') {
indent.writeln(
'$returnType $output = channelReply == null ? null : ((Number)channelReply).longValue();');
} else {
indent
.writeln('$returnType $output = ($returnType)channelReply;');
}
indent.writeln('callback.reply($output);');
}
});
});
}
});
}
String _makeGetter(NamedType field) {
final String uppercased =
field.name.substring(0, 1).toUpperCase() + field.name.substring(1);
return 'get$uppercased';
}
String _makeSetter(NamedType field) {
final String uppercased =
field.name.substring(0, 1).toUpperCase() + field.name.substring(1);
return 'set$uppercased';
}
/// Converts a [List] of [TypeDeclaration]s to a comma separated [String] to be
/// used in Java code.
String _flattenTypeArguments(List<TypeDeclaration> args) {
return args.map<String>(_javaTypeForDartType).join(', ');
}
String _javaTypeForBuiltinGenericDartType(
TypeDeclaration type,
int numberTypeArguments,
) {
if (type.typeArguments.isEmpty) {
return '${type.baseName}<${repeat('Object', numberTypeArguments).join(', ')}>';
} else {
return '${type.baseName}<${_flattenTypeArguments(type.typeArguments)}>';
}
}
String? _javaTypeForBuiltinDartType(TypeDeclaration type) {
const Map<String, String> javaTypeForDartTypeMap = <String, String>{
'bool': 'Boolean',
'int': 'Long',
'String': 'String',
'double': 'Double',
'Uint8List': 'byte[]',
'Int32List': 'int[]',
'Int64List': 'long[]',
'Float64List': 'double[]',
};
if (javaTypeForDartTypeMap.containsKey(type.baseName)) {
return javaTypeForDartTypeMap[type.baseName];
} else if (type.baseName == 'List') {
return _javaTypeForBuiltinGenericDartType(type, 1);
} else if (type.baseName == 'Map') {
return _javaTypeForBuiltinGenericDartType(type, 2);
} else {
return null;
}
}
String _javaTypeForDartType(TypeDeclaration type) {
return _javaTypeForBuiltinDartType(type) ?? type.baseName;
}
String _nullsafeJavaTypeForDartType(TypeDeclaration type) {
final String nullSafe =
type.isVoid ? '' : (type.isNullable ? '@Nullable ' : '@NonNull ');
return '$nullSafe${_javaTypeForDartType(type)}';
}
/// Casts variable named [varName] to the correct host datatype for [field].
/// This is for use in codecs where we may have a map representation of an
/// object.
String _castObject(
NamedType field, List<Class> classes, List<Enum> enums, String varName) {
final HostDatatype hostDatatype = getFieldHostDatatype(field, classes, enums,
(TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
if (field.type.baseName == 'int') {
return '($varName == null) ? null : (($varName instanceof Integer) ? (Integer)$varName : (${hostDatatype.datatype})$varName)';
} else if (!hostDatatype.isBuiltin &&
classes.map((Class x) => x.name).contains(field.type.baseName)) {
return '($varName == null) ? null : ${hostDatatype.datatype}.fromMap((Map)$varName)';
} else {
return '(${hostDatatype.datatype})$varName';
}
}
/// Generates the ".java" file for the AST represented by [root] to [sink] with the
/// provided [options].
void generateJava(JavaOptions options, Root root, StringSink sink) {
final Set<String> rootClassNameSet =
root.classes.map((Class x) => x.name).toSet();
final Set<String> rootEnumNameSet =
root.enums.map((Enum x) => x.name).toSet();
final Indent indent = Indent(sink);
void writeHeader() {
if (options.copyrightHeader != null) {
addLines(indent, options.copyrightHeader!, linePrefix: '// ');
}
indent.writeln('// $generatedCodeWarning');
indent.writeln('// $seeAlsoWarning');
}
void writeImports() {
indent.writeln('import android.util.Log;');
indent.writeln('import androidx.annotation.NonNull;');
indent.writeln('import androidx.annotation.Nullable;');
indent.writeln('import io.flutter.plugin.common.BasicMessageChannel;');
indent.writeln('import io.flutter.plugin.common.BinaryMessenger;');
indent.writeln('import io.flutter.plugin.common.MessageCodec;');
indent.writeln('import io.flutter.plugin.common.StandardMessageCodec;');
indent.writeln('import java.io.ByteArrayOutputStream;');
indent.writeln('import java.nio.ByteBuffer;');
indent.writeln('import java.util.Arrays;');
indent.writeln('import java.util.ArrayList;');
indent.writeln('import java.util.List;');
indent.writeln('import java.util.Map;');
indent.writeln('import java.util.HashMap;');
}
String camelToSnake(String camelCase) {
final RegExp regex = RegExp('([a-z])([A-Z]+)');
return camelCase
.replaceAllMapped(regex, (Match m) => '${m[1]}_${m[2]}')
.toUpperCase();
}
void writeEnum(Enum anEnum) {
indent.write('public enum ${anEnum.name} ');
indent.scoped('{', '}', () {
int index = 0;
for (final String member in anEnum.members) {
indent.writeln(
'${camelToSnake(member)}($index)${index == anEnum.members.length - 1 ? ';' : ','}');
index++;
}
indent.writeln('');
// We use explicit indexing here as use of the ordinal() method is
// discouraged. The toMap and fromMap API matches class API to allow
// the same code to work with enums and classes, but this
// can also be done directly in the host and flutter APIs.
indent.writeln('private int index;');
indent.write('private ${anEnum.name}(final int index) ');
indent.scoped('{', '}', () {
indent.writeln('this.index = index;');
});
});
}
void writeDataClass(Class klass) {
void writeField(NamedType field) {
final HostDatatype hostDatatype = getFieldHostDatatype(
field,
root.classes,
root.enums,
(TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
final String nullability =
field.type.isNullable ? '@Nullable' : '@NonNull';
indent.writeln(
'private $nullability ${hostDatatype.datatype} ${field.name};');
indent.writeln(
'public $nullability ${hostDatatype.datatype} ${_makeGetter(field)}() { return ${field.name}; }');
indent.writeScoped(
'public void ${_makeSetter(field)}($nullability ${hostDatatype.datatype} setterArg) {',
'}', () {
if (!field.type.isNullable) {
indent.writeScoped('if (setterArg == null) {', '}', () {
indent.writeln(
'throw new IllegalStateException("Nonnull field \\"${field.name}\\" is null.");');
});
}
indent.writeln('this.${field.name} = setterArg;');
});
}
void writeToMap() {
indent.write('@NonNull Map<String, Object> toMap() ');
indent.scoped('{', '}', () {
indent.writeln('Map<String, Object> toMapResult = new HashMap<>();');
for (final NamedType field in klass.fields) {
final HostDatatype hostDatatype = getFieldHostDatatype(
field,
root.classes,
root.enums,
(TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
String toWriteValue = '';
final String fieldName = field.name;
if (!hostDatatype.isBuiltin &&
rootClassNameSet.contains(field.type.baseName)) {
toWriteValue = '($fieldName == null) ? null : $fieldName.toMap()';
} else if (!hostDatatype.isBuiltin &&
rootEnumNameSet.contains(field.type.baseName)) {
toWriteValue = '$fieldName == null ? null : $fieldName.index';
} else {
toWriteValue = field.name;
}
indent.writeln('toMapResult.put("${field.name}", $toWriteValue);');
}
indent.writeln('return toMapResult;');
});
}
void writeFromMap() {
indent.write(
'static @NonNull ${klass.name} fromMap(@NonNull Map<String, Object> map) ');
indent.scoped('{', '}', () {
const String result = 'pigeonResult';
indent.writeln('${klass.name} $result = new ${klass.name}();');
for (final NamedType field in klass.fields) {
final String fieldVariable = field.name;
final String setter = _makeSetter(field);
indent.writeln('Object $fieldVariable = map.get("${field.name}");');
if (rootEnumNameSet.contains(field.type.baseName)) {
indent.writeln(
'$result.$setter(${_intToEnum(fieldVariable, field.type.baseName)});');
} else {
indent.writeln(
'$result.$setter(${_castObject(field, root.classes, root.enums, fieldVariable)});');
}
}
indent.writeln('return $result;');
});
}
void writeBuilder() {
indent.write('public static final class Builder ');
indent.scoped('{', '}', () {
for (final NamedType field in klass.fields) {
final HostDatatype hostDatatype = getFieldHostDatatype(
field,
root.classes,
root.enums,
(TypeDeclaration x) => _javaTypeForBuiltinDartType(x));
final String nullability =
field.type.isNullable ? '@Nullable' : '@NonNull';
indent.writeln(
'private @Nullable ${hostDatatype.datatype} ${field.name};');
indent.writeScoped(
'public @NonNull Builder ${_makeSetter(field)}($nullability ${hostDatatype.datatype} setterArg) {',
'}', () {
indent.writeln('this.${field.name} = setterArg;');
indent.writeln('return this;');
});
}
indent.write('public @NonNull ${klass.name} build() ');
indent.scoped('{', '}', () {
const String returnVal = 'pigeonReturn';
indent.writeln('${klass.name} $returnVal = new ${klass.name}();');
for (final NamedType field in klass.fields) {
indent.writeln('$returnVal.${_makeSetter(field)}(${field.name});');
}
indent.writeln('return $returnVal;');
});
});
}
indent.writeln(
'/** Generated class from Pigeon that represents data sent in messages. */');
indent.write('public static class ${klass.name} ');
indent.scoped('{', '}', () {
for (final NamedType field in klass.fields) {
writeField(field);
indent.addln('');
}
if (klass.fields
.map((NamedType e) => !e.type.isNullable)
.any((bool e) => e)) {
indent.writeln(
'/** Constructor is private to enforce null safety; use Builder. */');
indent.writeln('private ${klass.name}() {}');
}
writeBuilder();
writeToMap();
writeFromMap();
});
}
void writeResultInterface() {
indent.write('public interface Result<T> ');
indent.scoped('{', '}', () {
indent.writeln('void success(T result);');
indent.writeln('void error(Throwable error);');
});
}
void writeApi(Api api) {
if (api.location == ApiLocation.host) {
_writeHostApi(indent, api, root);
} else if (api.location == ApiLocation.flutter) {
_writeFlutterApi(indent, api);
}
}
void writeWrapError() {
indent.format('''
private static Map<String, Object> wrapError(Throwable exception) {
\tMap<String, Object> errorMap = new HashMap<>();
\terrorMap.put("${Keys.errorMessage}", exception.toString());
\terrorMap.put("${Keys.errorCode}", exception.getClass().getSimpleName());
\terrorMap.put("${Keys.errorDetails}", "Cause: " + exception.getCause() + ", Stacktrace: " + Log.getStackTraceString(exception));
\treturn errorMap;
}''');
}
writeHeader();
indent.addln('');
if (options.package != null) {
indent.writeln('package ${options.package};');
}
indent.addln('');
writeImports();
indent.addln('');
indent.writeln('/** Generated class from Pigeon. */');
indent.writeln(
'@SuppressWarnings({"unused", "unchecked", "CodeBlock2Expr", "RedundantSuppression"})');
if (options.useGeneratedAnnotation ?? false) {
indent.writeln('@javax.annotation.Generated("dev.flutter.pigeon")');
}
indent.write('public class ${options.className!} ');
indent.scoped('{', '}', () {
for (final Enum anEnum in root.enums) {
indent.writeln('');
writeEnum(anEnum);
}
for (final Class klass in root.classes) {
indent.addln('');
writeDataClass(klass);
}
if (root.apis.any((Api api) =>
api.location == ApiLocation.host &&
api.methods.any((Method it) => it.isAsynchronous))) {
indent.addln('');
writeResultInterface();
}
for (final Api api in root.apis) {
_writeCodec(indent, api, root);
indent.addln('');
writeApi(api);
}
writeWrapError();
});
}