blob: 53e5c7211583d9e46bcc6f9be97ecc404a2eaa8c [file] [log] [blame]
/*
* Copyright 2021 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* 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.
*/
package com.google.android.enterprise.connectedapps.processor;
import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME;
import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME;
import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_RUNTIME_EXCEPTION_CLASSNAME;
import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME;
import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS;
import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo;
import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo;
import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper;
import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext;
import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import com.squareup.javapoet.ParameterizedTypeName;
import com.squareup.javapoet.TypeSpec;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
/**
* Generate the {@code Profile_*_FakeOther} class for a single cross-profile type.
*
* <p>This must only be used once. It should be used after {@link EarlyValidator} has been used to
* validate that the annotated code is correct.
*/
final class FakeOtherGenerator {
private boolean generated = false;
private final GeneratorContext generatorContext;
private final GeneratorUtilities generatorUtilities;
private final CrossProfileTypeInfo crossProfileType;
FakeOtherGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
this.generatorContext = checkNotNull(generatorContext);
this.generatorUtilities = new GeneratorUtilities(generatorContext);
this.crossProfileType = checkNotNull(crossProfileType);
}
void generate() {
if (generated) {
throw new IllegalStateException("FakeSingleSenderGenerator#generate can only be called once");
}
generated = true;
generateFakeOther();
}
private void generateFakeOther() {
ClassName className = getFakeOtherClassName(generatorContext, crossProfileType);
ClassName singleSenderCanThrowInterface =
InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName(
generatorContext, crossProfileType);
ClassName fakeProfileConnectorClassName =
crossProfileType.profileConnector().isPresent()
? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName(
crossProfileType.profileConnector().get())
: ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
TypeSpec.Builder classBuilder =
TypeSpec.classBuilder(className)
.addJavadoc(
"Fake implementation of {@link $T} for use during tests.\n\n"
+ "<p>This acts based on the state of the passed in {@link $T} and acts as if"
+ " making a call on the other profile.\n",
singleSenderCanThrowInterface,
fakeProfileConnectorClassName)
.addModifiers(Modifier.PUBLIC, Modifier.FINAL)
.addSuperinterface(singleSenderCanThrowInterface);
classBuilder.addField(
fakeProfileConnectorClassName, "connector", Modifier.PRIVATE, Modifier.FINAL);
addConstructor(classBuilder);
classBuilder.addMethod(
MethodSpec.methodBuilder("timeout")
.addAnnotation(Override.class)
.addAnnotation(
AnnotationSpec.builder(SuppressWarnings.class)
.addMember("value", "$S", "GoodTime")
.build())
.addModifiers(Modifier.PUBLIC)
.returns(className)
.addParameter(long.class, "timeout")
.addStatement("return this")
.build());
ClassName ifAvailableClass =
IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType);
classBuilder.addMethod(
MethodSpec.methodBuilder("ifAvailable")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ifAvailableClass)
.addStatement("return new $T(this)", ifAvailableClass)
.build());
for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) {
if (method.isBlocking(generatorContext, crossProfileType)) {
generateBlockingMethodOnFakeOther(classBuilder, method, crossProfileType);
} else if (method.isCrossProfileCallback(generatorContext)) {
generateCrossProfileCallbackMethodOnFakeOther(classBuilder, method, crossProfileType);
} else if (method.isFuture(crossProfileType)) {
generateFutureMethodOnFakeOther(classBuilder, method, crossProfileType);
} else {
throw new IllegalStateException("Unknown method type: " + method);
}
}
generatorUtilities.writeClassToFile(className.packageName(), classBuilder);
}
private void addConstructor(TypeSpec.Builder classBuilder) {
ClassName fakeProfileConnectorClassName =
crossProfileType.profileConnector().isPresent()
? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName(
crossProfileType.profileConnector().get())
: ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME;
classBuilder.addField(CONTEXT_CLASSNAME, "context", Modifier.PRIVATE, Modifier.FINAL);
if (crossProfileType.isStatic()) {
classBuilder.addMethod(
MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(fakeProfileConnectorClassName, "connector")
.addStatement("this.context = connector.applicationContext()")
.addStatement("this.connector = connector")
.build());
} else {
classBuilder.addField(
crossProfileType.className(), "crossProfileType", Modifier.PRIVATE, Modifier.FINAL);
classBuilder.addMethod(
MethodSpec.constructorBuilder()
.addModifiers(Modifier.PUBLIC)
.addParameter(fakeProfileConnectorClassName, "connector")
.addParameter(crossProfileType.className(), "crossProfileType")
.addStatement("this.context = connector.applicationContext()")
.addStatement("this.connector = connector")
.addStatement("this.crossProfileType = crossProfileType")
.build());
}
}
private void generateBlockingMethodOnFakeOther(
TypeSpec.Builder classBuilder,
CrossProfileMethodInfo method,
CrossProfileTypeInfo crossProfileType) {
MethodSpec.Builder methodBuilder =
MethodSpec.methodBuilder(method.simpleName())
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addExceptions(method.thrownExceptions())
.addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME)
.returns(method.returnTypeTypeName())
.addParameters(
GeneratorUtilities.extractParametersFromMethod(
crossProfileType.supportedTypes(),
method.methodElement(),
REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
CodeBlock methodCall =
CodeBlock.of(
"$L.$L($L)",
getCrossProfileTypeReference(method, crossProfileType),
method.simpleName(),
method.commaSeparatedParameters(
crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
if (method.returnType().getKind() != TypeKind.VOID) {
methodCall = CodeBlock.of("return $L", methodCall);
}
methodBuilder.beginControlFlow("if (!connector.isConnected())");
methodBuilder.addStatement(
"throw new $T($S)",
UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME,
"Could not access other profile");
methodBuilder.endControlFlow();
methodBuilder.beginControlFlow("if (!connector.isManuallyManagingConnection())");
methodBuilder.addStatement(
"throw new $T($S)",
UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME,
"Synchronous calls can only be used when manually connected");
methodBuilder.endControlFlow();
methodBuilder.beginControlFlow("try");
methodBuilder.addStatement(methodCall);
methodBuilder.nextControlFlow("catch ($T e)", RuntimeException.class);
methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
methodBuilder.nextControlFlow("catch ($T e)", Error.class);
methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
methodBuilder.endControlFlow();
classBuilder.addMethod(methodBuilder.build());
}
private static CodeBlock getCrossProfileTypeReference(
CrossProfileMethodInfo method, CrossProfileTypeInfo crossProfileType) {
return method.isStatic()
? CodeBlock.of("$1T", crossProfileType.className())
: CodeBlock.of("crossProfileType");
}
private void generateCrossProfileCallbackMethodOnFakeOther(
TypeSpec.Builder classBuilder,
CrossProfileMethodInfo method,
CrossProfileTypeInfo crossProfileType) {
MethodSpec.Builder methodBuilder =
MethodSpec.methodBuilder(method.simpleName())
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(method.returnTypeTypeName())
.addParameters(
GeneratorUtilities.extractParametersFromMethod(
crossProfileType.supportedTypes(),
method.methodElement(),
REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS))
.addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback");
CodeBlock methodCall =
CodeBlock.of(
"$L.$L($L)",
getCrossProfileTypeReference(method, crossProfileType),
method.simpleName(),
method.commaSeparatedParameters(
crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
if (method.returnType().getKind() != TypeKind.VOID) {
methodCall = CodeBlock.of("return $L", methodCall);
}
methodBuilder.beginControlFlow("if (!connector.isAvailable())");
methodBuilder.addStatement(
"exceptionCallback.onException(new $T($S))",
UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME,
"Could not access other profile");
methodBuilder.addStatement("return");
methodBuilder.endControlFlow();
methodBuilder.addStatement("connector.automaticallyConnect()");
methodBuilder.beginControlFlow("try");
methodBuilder.addStatement(methodCall);
methodBuilder.nextControlFlow("catch ($T e)", RuntimeException.class);
methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
methodBuilder.nextControlFlow("catch ($T e)", Error.class);
methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
methodBuilder.endControlFlow();
classBuilder.addMethod(methodBuilder.build());
}
private void generateFutureMethodOnFakeOther(
TypeSpec.Builder classBuilder,
CrossProfileMethodInfo method,
CrossProfileTypeInfo crossProfileType) {
MethodSpec.Builder methodBuilder =
MethodSpec.methodBuilder(method.simpleName())
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(method.returnTypeTypeName())
.addParameters(
GeneratorUtilities.extractParametersFromMethod(
crossProfileType.supportedTypes(),
method.methodElement(),
REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS));
CodeBlock methodCall =
CodeBlock.of(
"$L.$L($L)",
getCrossProfileTypeReference(method, crossProfileType),
method.simpleName(),
method.commaSeparatedParameters(
crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS));
if (method.returnType().getKind() != TypeKind.VOID) {
methodCall = CodeBlock.of("$1T returnValue = $2L", method.returnType(), methodCall);
}
TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType());
FutureWrapper futureWrapper =
crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get();
// This assumes futures are only generic on one argument, which is enforced
TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).get(0);
ParameterizedTypeName futureWrapperType =
ParameterizedTypeName.get(futureWrapper.wrapperClassName(), ClassName.get(wrappedType));
methodBuilder.beginControlFlow("if (!connector.isAvailable())");
methodBuilder.addStatement(
"$1T failedFuture = $2T.create(new $3T(), $4L)",
futureWrapperType,
futureWrapper.wrapperClassName(),
BundlerGenerator.getBundlerClassName(generatorContext, crossProfileType),
TypeUtils.generateBundlerType(wrappedType));
methodBuilder.addStatement(
"failedFuture.onException(new $1T($2S))",
UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME,
"Could not access other profile");
methodBuilder.addStatement("return failedFuture.getFuture()");
methodBuilder.endControlFlow();
methodBuilder.beginControlFlow("try");
methodBuilder.addStatement("connector.automaticallyConnect()");
methodBuilder.addStatement(methodCall);
if (method.returnType().getKind() != TypeKind.VOID) {
methodBuilder.addStatement("return returnValue");
}
methodBuilder.nextControlFlow("catch ($T e)", RuntimeException.class);
methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
methodBuilder.nextControlFlow("catch ($T e)", Error.class);
methodBuilder.addStatement("throw new $T(e)", PROFILE_RUNTIME_EXCEPTION_CLASSNAME);
methodBuilder.endControlFlow();
classBuilder.addMethod(methodBuilder.build());
}
static ClassName getFakeOtherClassName(
GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) {
return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_FakeOther");
}
}