diff options
author | Jonathan Scott <scottjonathan@google.com> | 2021-04-15 23:52:57 +0100 |
---|---|---|
committer | Jonathan Scott <scottjonathan@google.com> | 2021-04-15 23:53:22 +0100 |
commit | 7451f6c15e755236d0e1aef2e1ae40f01c2ea105 (patch) | |
tree | dd553a86ad2ab309954ebdc46e5592979c734942 /processor/src/main | |
parent | 98aadee05251dfff3611cd31000da37d00d943f2 (diff) | |
download | connectedappssdk-7451f6c15e755236d0e1aef2e1ae40f01c2ea105.tar.gz |
Import platform/external/connectedappssdk
Test: N/A importing code - BUILD to follow
Bug: 179354604
Change-Id: I6b9833d1b148f526643e6ba34bd09ac4a17f37cd
Diffstat (limited to 'processor/src/main')
91 files changed, 14994 insertions, 0 deletions
diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java new file mode 100644 index 0000000..05a506e --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/AlwaysThrowsGenerator.java @@ -0,0 +1,221 @@ +/* + * 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.EXCEPTION_CALLBACK_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.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.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_AlwaysThrows} class for a single cross-profile type. + * + * <p>This class is used when running on Pre-O devices to shortcut any cross-profile code and just + * throw an {@code UnavailableProfileException}. + * + * <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 AlwaysThrowsGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + AlwaysThrowsGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("AlwaysThrowsGenerator#generate can only be called once"); + } + generated = true; + + generateAlwaysThrowsClass(); + } + + private void generateAlwaysThrowsClass() { + ClassName className = getAlwaysThrowsClassName(generatorContext, crossProfileType); + + ClassName singleSenderCanThrowInterface = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} which throws an {@link $T} for every call.\n", + singleSenderCanThrowInterface, + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(singleSenderCanThrowInterface); + + classBuilder.addField(String.class, "errorMessage", Modifier.PRIVATE, Modifier.FINAL); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(String.class, "errorMessage") + .beginControlFlow("if (errorMessage == null)") + .addStatement("throw new $T()", NullPointerException.class) + .endControlFlow() + .addStatement("this.errorMessage = errorMessage") + .build()); + + 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)) { + generateBlockingMethodOnAlwaysThrowsClass(classBuilder, method, crossProfileType); + } else if (method.isCrossProfileCallback(generatorContext)) { + generateCrossProfileCallbackMethodOnAlwaysThrowsClass( + classBuilder, method, crossProfileType); + } else if (method.isFuture(crossProfileType)) { + generateFutureMethodOnAlwaysThrowsClass(classBuilder, method, crossProfileType); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateBlockingMethodOnAlwaysThrowsClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addStatement("throw new $T(errorMessage)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + + classBuilder.addMethod(methodBuilder.build()); + } + + private static void generateCrossProfileCallbackMethodOnAlwaysThrowsClass( + 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") + .addStatement( + "exceptionCallback.onException(new $T(errorMessage))", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateFutureMethodOnAlwaysThrowsClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + 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)); + + 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)) + .addStatement( + "$1T failedFuture = $2T.create(new $3T(), $4L)", + futureWrapperType, + futureWrapper.wrapperClassName(), + BundlerGenerator.getBundlerClassName(generatorContext, crossProfileType), + TypeUtils.generateBundlerType(wrappedType)) + .addStatement( + "failedFuture.onException(new $T(errorMessage))", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addStatement("return failedFuture.getFuture()"); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getAlwaysThrowsClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_AlwaysThrows"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java new file mode 100644 index 0000000..8d421a3 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/BundlerGenerator.java @@ -0,0 +1,270 @@ +/* + * 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.BUNDLER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_TYPE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.Type; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import java.util.List; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code *_Bundler} class for a single {@link CrossProfileConfiguration} annotated + * method. + * + * <p>This class is responsible for reading and writing {@code Bundle} and {@code Parcel} instances. + * + * <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 BundlerGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + BundlerGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("BundlerGenerator#generate can only be called once"); + } + generated = true; + + generateBundlerClass(); + } + + private void generateBundlerClass() { + ClassName className = getBundlerClassName(generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} for use with {@link $T}.\n", + BUNDLER_CLASSNAME, + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(BUNDLER_CLASSNAME); + + classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC).build()); + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(PARCEL_CLASSNAME, "in") + .build()); + + makeParcelable(classBuilder, className); + addWriteToParcelMethod(classBuilder); + addReadFromParcelMethod(classBuilder); + addCreateArrayMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void makeParcelable(TypeSpec.Builder classBuilder, ClassName bundlerClassName) { + classBuilder.addMethod( + MethodSpec.methodBuilder("writeToParcel") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(PARCEL_CLASSNAME, "dest") + .addParameter(int.class, "flags") + .build()); + + generatorUtilities.addDefaultParcelableMethods(classBuilder, bundlerClassName); + } + + + private void addWriteToParcelMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + List<Type> types = + crossProfileType.supportedTypes().usableTypes().stream() + .filter(Type::canBeBundled) + .filter(t -> !t.isPrimitive()) + .collect(toList()); + addWriteToParcelTypes(methodCode, types); + + classBuilder.addMethod( + MethodSpec.methodBuilder("writeToParcel") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + // This is for passing rawtypes into the Parcelable*.of() methods + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "\"unchecked\"") + .build()) + .addParameter(PARCEL_CLASSNAME, "parcel") + .addParameter(Object.class, "value") + .addParameter(BUNDLER_TYPE_CLASSNAME, "valueType") + .addParameter(int.class, "flags") + .addCode(methodCode.build()) + .build()); + } + + private void addWriteToParcelTypes(CodeBlock.Builder codeBuilder, List<Type> types) { + codeBuilder.beginControlFlow( + "if ($S.equals(valueType.rawTypeQualifiedName()))", "java.lang.Void"); + codeBuilder.addStatement("return"); + for (Type type : types) { + codeBuilder.nextControlFlow( + "else if ($S.equals(valueType.rawTypeQualifiedName()))", + TypeUtils.getRawTypeQualifiedName(type.getTypeMirror())); + addWriteToParcelType(codeBuilder, type); + } + codeBuilder.endControlFlow(); + + codeBuilder.addStatement( + "throw new $T(\"Type \" + valueType.rawTypeQualifiedName() + \" cannot be written to" + + " Parcel\")", + IllegalArgumentException.class); + } + + private void addWriteToParcelType(CodeBlock.Builder codeBuilder, Type type) { + CodeBlock convertedValue = + CodeBlock.of("($L) value", TypeUtils.getRawTypeQualifiedName(type.getTypeMirror())); + codeBuilder.addStatement( + crossProfileType + .supportedTypes() + .generateWriteToParcelCode("parcel", type, convertedValue.toString())); + codeBuilder.addStatement("return"); + } + + private void addReadFromParcelMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + List<Type> types = + crossProfileType.supportedTypes().usableTypes().stream() + .filter(Type::canBeBundled) + .collect(toList()); + addReadFromParcelTypes(methodCode, types); + + methodCode.addStatement( + "throw new $T(\"Type \" + valueType.rawTypeQualifiedName() + \" cannot be read from" + + " Parcel\")", + IllegalArgumentException.class); + + classBuilder.addMethod( + MethodSpec.methodBuilder("readFromParcel") + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "\"unchecked\"") + .build()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(Object.class) + .addParameter(PARCEL_CLASSNAME, "parcel") + .addParameter(BUNDLER_TYPE_CLASSNAME, "valueType") + .addCode(methodCode.build()) + .build()); + } + + private void addReadFromParcelTypes(CodeBlock.Builder codeBuilder, List<Type> types) { + codeBuilder.beginControlFlow( + "if ($S.equals(valueType.rawTypeQualifiedName()))", "java.lang.Void"); + codeBuilder.addStatement("return null"); + for (Type type : types) { + codeBuilder.nextControlFlow( + "else if ($S.equals(valueType.rawTypeQualifiedName()))", + TypeUtils.getRawTypeQualifiedName(type.getTypeMirror())); + addReadFromParcelType(codeBuilder, type); + } + codeBuilder.endControlFlow(); + } + + private void addReadFromParcelType(CodeBlock.Builder codeBuilder, Type type) { + TypeMirror objectType = type.getTypeMirror(); + if (objectType.getKind().isPrimitive()) { + PrimitiveType primitiveType = (PrimitiveType) objectType; + objectType = generatorContext.types().boxedClass(primitiveType).asType(); + } + + codeBuilder.addStatement( + "return ($L) $L", + TypeUtils.getRawTypeQualifiedName(objectType), + crossProfileType.supportedTypes().generateReadFromParcelCode("parcel", type)); + } + + private void addCreateArrayMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + List<Type> types = + crossProfileType.supportedTypes().usableTypes().stream() + .filter(Type::canBeBundled) + .filter(t -> !t.isGeneric()) + .filter( + t -> + !t.isPrimitive()) // We can't return a primitive array with return type Object[] + .filter(t -> !t.isArray()) // We don't support multidimensional arrays + .collect(toList()); + addCreateArrayTypes(methodCode, types); + + methodCode.addStatement( + "throw new $T(\"Cannot create array of type \" + valueType.rawTypeQualifiedName())", + IllegalArgumentException.class); + + classBuilder.addMethod( + MethodSpec.methodBuilder("createArray") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(ArrayTypeName.of(Object.class)) + .addParameter(BUNDLER_TYPE_CLASSNAME, "valueType") + .addParameter(int.class, "size") + .addCode(methodCode.build()) + .build()); + } + + private void addCreateArrayTypes(CodeBlock.Builder codeBuilder, List<Type> types) { + codeBuilder.beginControlFlow( + "if ($S.equals(valueType.rawTypeQualifiedName()))", "java.lang.Void"); + codeBuilder.addStatement("return new Void[size]"); + for (Type type : types) { + codeBuilder.nextControlFlow( + "else if ($S.equals(valueType.rawTypeQualifiedName()))", + TypeUtils.getRawTypeQualifiedName(type.getTypeMirror())); + addCreateArrayType(codeBuilder, type); + } + codeBuilder.endControlFlow(); + } + + private void addCreateArrayType(CodeBlock.Builder codeBuilder, Type type) { + codeBuilder.addStatement("return new $T[size]", type.getTypeMirror()); + } + + static ClassName getBundlerClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_Bundler"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java new file mode 100644 index 0000000..7fcf28a --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CodeGenerator.java @@ -0,0 +1,80 @@ +/* + * 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.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo; + +/** + * Generator of code for connected apps. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <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 CodeGenerator { + private boolean generated = false; + private final GeneratorContext generatorContext; + private final ParcelableWrappersGenerator parcelableWrappersGenerator; + private final FutureWrappersGenerator futureWrappersGenerator; + private final TestCodeGenerator testCodeGenerator; + + CodeGenerator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + this.parcelableWrappersGenerator = new ParcelableWrappersGenerator(generatorContext); + this.futureWrappersGenerator = new FutureWrappersGenerator(generatorContext); + this.testCodeGenerator = new TestCodeGenerator(generatorContext); + } + + void generate() { + if (generated) { + throw new IllegalStateException("CodeGenerator#generate can only be called once"); + } + generated = true; + + parcelableWrappersGenerator.generate(); + futureWrappersGenerator.generate(); + testCodeGenerator.generate(); + + for (ProfileConnectorInfo connector : generatorContext.generatedProfileConnectors()) { + new ProfileConnectorCodeGenerator(generatorContext, connector).generate(); + } + + for (UserConnectorInfo connector : generatorContext.generatedUserConnectors()) { + new UserConnectorCodeGenerator(generatorContext, connector).generate(); + } + + for (CrossProfileConfigurationInfo configuration : generatorContext.configurations()) { + new ConfigurationCodeGenerator(generatorContext, configuration).generate(); + } + + for (ProviderClassInfo providerClass : generatorContext.providers()) { + new ProviderClassCodeGenerator(generatorContext, providerClass).generate(); + } + + for (CrossProfileCallbackInterfaceInfo callbackInterface : + generatorContext.crossProfileCallbackInterfaces()) { + new CrossProfileCallbackCodeGenerator(generatorContext, callbackInterface).generate(); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java new file mode 100644 index 0000000..cebeebc --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CommonClassNames.java @@ -0,0 +1,124 @@ +/* + * 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 com.squareup.javapoet.ClassName; + +/** + * {@link ClassName} instances shared across the processor. + * + * <p>This is required as most classes are not available to the processor so need to be referenced + * through {@link ClassName} + */ +public class CommonClassNames { + static final ClassName CONTEXT_CLASSNAME = ClassName.get("android.content", "Context"); + static final ClassName PARCEL_CLASSNAME = ClassName.get("android.os", "Parcel"); + static final ClassName PARCELABLE_CLASSNAME = ClassName.get("android.os", "Parcelable"); + static final ClassName CROSS_PROFILE_FUTURE_RESULT_WRITER = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", "CrossProfileFutureResultWriter"); + static final ClassName IF_AVAILABLE_FUTURE_RESULT_WRITER = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", "IfAvailableFutureResultWriter"); + static final ClassName PARCELABLE_CREATOR_CLASSNAME = + ClassName.get("android.os.Parcelable", "Creator"); + static final ClassName UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.exceptions", "UnavailableProfileException"); + static final ClassName AVAILABILITY_RESTRICTIONS_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.annotations", "AvailabilityRestrictions"); + static final ClassName PROFILE_RUNTIME_EXCEPTION_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.exceptions", "ProfileRuntimeException"); + static final ClassName PROFILE_AWARE_UTILS_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ConnectedAppsUtils"); + static final ClassName BACKGROUND_EXCEPTION_THROWER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", "BackgroundExceptionThrower"); + static final ClassName PARCEL_UTILITIES_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "ParcelUtilities"); + static final ClassName METHOD_RUNNER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "MethodRunner"); + static final ClassName BUNDLER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "Bundler"); + static final ClassName BUNDLER_TYPE_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "BundlerType"); + static final ClassName PARCEL_CALL_RECEIVER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.internal", "ParcelCallReceiver"); + public static final ClassName BINDER_CLASSNAME = ClassName.get("android.os", "Binder"); + public static final ClassName INTENT_CLASSNAME = ClassName.get("android.content", "Intent"); + static final ClassName CROSS_PROFILE_SENDER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "CrossProfileSender"); + public static final ClassName CROSSPROFILESERVICE_STUB_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.ICrossProfileService", "Stub"); + static final ClassName INVALID_PROTOCOL_BUFFER_EXCEPTION_CLASSNAME = + ClassName.get("com.google.protobuf", "InvalidProtocolBufferException"); + static final ClassName PROFILE_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "Profile"); + static final ClassName LOCAL_CALLBACK_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "LocalCallback"); + public static final ClassName CROSS_PROFILE_CALLBACK_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ICrossProfileCallback"); + static final ClassName ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", + "CrossProfileCallbackMultiMerger"); + static final ClassName CROSS_PROFILE_CALLBACK_PARCEL_CALL_SENDER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", + "CrossProfileCallbackParcelCallSender"); + static final ClassName CROSS_PROFILE_CALLBACK_EXCEPTION_PARCEL_CALL_SENDER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", + "CrossProfileCallbackExceptionParcelCallSender"); + static final ClassName ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger", + "CrossProfileCallbackMultiMergerCompleteListener"); + + public static final ClassName SERVICE_CLASSNAME = ClassName.get("android.app", "Service"); + public static final ClassName EXCEPTION_CALLBACK_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ExceptionCallback"); + public static final ClassName CALLBACK_MERGER_EXCEPTION_CALLBACK_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.internal", + "CallbackMergerExceptionCallback"); + public static final ClassName PROFILE_CONNECTOR_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ProfileConnector"); + public static final ClassName ABSTRACT_PROFILE_CONNECTOR_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "AbstractProfileConnector"); + public static final ClassName ABSTRACT_USER_CONNECTOR_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "AbstractUserConnector"); + public static final ClassName ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.AbstractProfileConnector", "Builder"); + public static final ClassName ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps.AbstractUserConnector", "Builder"); + public static final ClassName CONNECTION_BINDER_CLASSNAME = + ClassName.get("com.google.android.enterprise.connectedapps", "ConnectionBinder"); + public static final ClassName SCHEDULED_EXECUTOR_SERVICE_CLASSNAME = + ClassName.get("java.util.concurrent", "ScheduledExecutorService"); + public static final ClassName ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME = + ClassName.get( + "com.google.android.enterprise.connectedapps.testing", "AbstractFakeProfileConnector"); + + public static final ClassName VERSION_CLASSNAME = ClassName.get("android.os.Build", "VERSION"); + public static final ClassName VERSION_CODES_CLASSNAME = + ClassName.get("android.os.Build", "VERSION_CODES"); + + private CommonClassNames() {} +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java new file mode 100644 index 0000000..a4647dd --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ConfigurationCodeGenerator.java @@ -0,0 +1,59 @@ +/* + * 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.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; + +/** + * Generator of code for a single {@link CrossProfileConfiguration}. + * + * <p>The {@code Service} will only be generated if the configuration contains at least one provider + * class which has at least one {@link CrossProfile} type. + */ +class ConfigurationCodeGenerator { + private boolean generated = false; + private final CrossProfileConfigurationInfo configuration; + private final ServiceGenerator serviceGenerator; + private final DispatcherGenerator dispatcherGenerator; + + ConfigurationCodeGenerator( + GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + this.configuration = checkNotNull(configuration); + this.serviceGenerator = new ServiceGenerator(checkNotNull(generatorContext), configuration); + this.dispatcherGenerator = new DispatcherGenerator(generatorContext, configuration); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ConfigurationCodeGenerator#generate can only be called once"); + } + generated = true; + + if (configuration.profileConnector() == null) { + // Without a connector we can't line things up so don't generate + return; + } + + serviceGenerator.generate(); + dispatcherGenerator.generate(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java new file mode 100644 index 0000000..2822bf2 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileCallbackCodeGenerator.java @@ -0,0 +1,501 @@ +/* + * 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.ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_EXCEPTION_PARCEL_CALL_SENDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_PARCEL_CALL_SENDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.LOCAL_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_UTILITIES_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.Map; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; + +/** Generator of code for a single {@link CrossProfileCallback}. */ +public class CrossProfileCallbackCodeGenerator { + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileCallbackInterfaceInfo callbackInterface; + + private final TypeMirror voidTypeMirror; + + CrossProfileCallbackCodeGenerator( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + this.generatorContext = checkNotNull(generatorContext); + this.callbackInterface = checkNotNull(callbackInterface); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + + voidTypeMirror = generatorContext.elements().getTypeElement("java.lang.Void").asType(); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "CrossProfileCallbackCodeGenerator#generate can only be called once"); + } + generated = true; + + generateReceiverClass(); + generateSenderClass(); + + if (callbackInterface.isSimple()) { + // There can be only one method + ExecutableElement method = callbackInterface.methods().get(0); + generateMultiInterface(method); + generateMultiMergerResultClass(method); + generateMultiMergerInputClass(method); + } + } + + private void generateMultiInterface(ExecutableElement method) { + ClassName interfaceName = + getCrossProfileCallbackMultiInterfaceClassName(generatorContext, callbackInterface); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addJavadoc( + "Callback interface used when using a {@link $T} with multiple profiles.\n", + callbackInterface.interfaceElement()) + .addModifiers(Modifier.PUBLIC); + + addMultiMethod(interfaceBuilder, method); + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generateMultiMergerResultClass(ExecutableElement method) { + ClassName className = + getCrossProfileCallbackMultiMergerResultClassName(generatorContext, callbackInterface); + + TypeMirror paramType = + method.getParameters().isEmpty() ? voidTypeMirror : method.getParameters().get(0).asType(); + + TypeName mergerInterface = + ParameterizedTypeName.get( + ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME, + ClassName.get(generatorUtilities.boxIfNecessary(paramType))); + + ParameterizedTypeName multiParameterType = getMultiParameterType(paramType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} which forwards completed results to an instance of" + + " {@link $T}.\n", + ASYNC_CALLBACK_PARAM_MULTIMERGER_COMPLETE_LISTENER_CLASSNAME, + callbackInterface.interfaceElement()) + .addSuperinterface(mergerInterface) + .addModifiers(Modifier.PUBLIC); + + classBuilder.addField( + FieldSpec.builder( + getCrossProfileCallbackMultiInterfaceClassName(generatorContext, callbackInterface), + "callback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter( + getCrossProfileCallbackMultiInterfaceClassName(generatorContext, callbackInterface), + "callback") + .addStatement("this.callback = callback") + .build()); + + String resultToPass = method.getParameters().isEmpty() ? "" : "results"; + + classBuilder.addMethod( + MethodSpec.methodBuilder("onResult") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(multiParameterType, "results") + .addStatement("callback.$L($L)", method.getSimpleName(), resultToPass) + .build()); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateMultiMergerInputClass(ExecutableElement method) { + ClassName className = + getCrossProfileCallbackMultiMergerInputClassName(generatorContext, callbackInterface); + + TypeMirror paramType = + method.getParameters().isEmpty() ? voidTypeMirror : method.getParameters().get(0).asType(); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} which passes results into an instance of {@link" + + " $T}.\n", + callbackInterface.interfaceElement(), + ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME) + .addSuperinterface(ClassName.get(callbackInterface.interfaceElement())) + .addModifiers(Modifier.PUBLIC); + + classBuilder.addField( + FieldSpec.builder(PROFILE_CLASSNAME, "profileId") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder( + ParameterizedTypeName.get( + ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME, + ClassName.get(generatorUtilities.boxIfNecessary(paramType))), + "callback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(PROFILE_CLASSNAME, "profileId") + .addParameter( + ParameterizedTypeName.get( + ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME, + ClassName.get(generatorUtilities.boxIfNecessary(paramType))), + "callback") + .addStatement("this.profileId = profileId") + .addStatement("this.callback = callback") + .build()); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.getSimpleName().toString()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC); + + if (!method.getParameters().isEmpty()) { + String paramName = method.getParameters().get(0).getSimpleName().toString(); + methodBuilder.addParameter(ClassName.get(paramType), paramName); + methodBuilder.addStatement("callback.onResult(profileId, $L)", paramName); + } else { + methodBuilder.addStatement("callback.onResult(profileId, null)"); + } + + classBuilder.addMethod(methodBuilder.build()); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addMultiMethod(TypeSpec.Builder interfaceBuilder, ExecutableElement method) { + if (method.getParameters().isEmpty()) { + interfaceBuilder.addMethod( + MethodSpec.methodBuilder(method.getSimpleName().toString()) + .addModifiers(method.getModifiers()) + .build()); + + return; + } + + // There can be only one parameter + VariableElement param = method.getParameters().get(0); + ParameterizedTypeName paramType = getMultiParameterType(param.asType()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder(method.getSimpleName().toString()) + .addModifiers(method.getModifiers()) + .addParameter(paramType, param.getSimpleName().toString()) + .build()); + } + + private ParameterizedTypeName getMultiParameterType(TypeMirror paramType) { + TypeName boxedParamType = TypeName.get(generatorUtilities.boxIfNecessary(paramType)); + return ParameterizedTypeName.get(ClassName.get(Map.class), PROFILE_CLASSNAME, boxedParamType); + } + + private void generateReceiverClass() { + ClassName className = + getCrossProfileCallbackReceiverClassName(generatorContext, callbackInterface); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(ClassName.get(callbackInterface.interfaceElement())) + .addJavadoc( + "Implementation of {@link $1T} which wraps an {@link $2T},\n" + + "writing the callback value to a {@link $3T} and passing it to the {@link" + + " $2T}.\n", + callbackInterface.interfaceElement(), + CROSS_PROFILE_CALLBACK_CLASSNAME, + PARCEL_CLASSNAME); + + classBuilder.addField( + FieldSpec.builder(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder(BUNDLER_CLASSNAME, "bundler") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addParameter(BUNDLER_CLASSNAME, "bundler") + .beginControlFlow("if (callback == null || bundler == null)") + .addStatement("throw new $T()", NullPointerException.class) + .endControlFlow() + .addStatement("this.callback = callback") + .addStatement("this.bundler = bundler") + .build()); + + for (ExecutableElement method : callbackInterface.methods()) { + addReceiverMethod(classBuilder, callbackInterface, method); + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private static void addReceiverMethod( + TypeSpec.Builder classBuilder, + CrossProfileCallbackInterfaceInfo callbackInterface, + ExecutableElement method) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.getSimpleName().toString()) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + // Allow catching of Exception + .addMember("value", "\"CatchSpecificExceptionsChecker\"") + .build()) + .addParameters(GeneratorUtilities.extractParametersFromMethod(method)); + + methodBuilder.beginControlFlow("try"); + + methodBuilder.addStatement( + "$1T callSender = new $1T(callback, /* methodIdentifier= */ $2L)", + CROSS_PROFILE_CALLBACK_PARCEL_CALL_SENDER_CLASSNAME, + callbackInterface.getIdentifier(method)); + + // parcel is recycled in this method + methodBuilder.addStatement("$1T parcel = $1T.obtain()", PARCEL_CLASSNAME); + + for (VariableElement param : method.getParameters()) { + methodBuilder.addStatement( + "bundler.writeToParcel(parcel, $1L, $2L, /* flags= */ 0)", + param.getSimpleName(), + TypeUtils.generateBundlerType(param.asType())); + } + + methodBuilder.addStatement("callSender.makeParcelCall(parcel)", PARCEL_CLASSNAME); + + methodBuilder.addStatement("parcel.recycle()"); + + methodBuilder + .nextControlFlow("catch ($T e)", Exception.class) + .beginControlFlow("try") + .addStatement( + "$1T unavailableProfileException = new $1T(\"Error when writing callback result\", e)", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + // parcel is recycled in this method + .addStatement("$1T parcel = $1T.obtain()", PARCEL_CLASSNAME) + .addStatement( + "$T.writeThrowableToParcel(parcel, unavailableProfileException)", + PARCEL_UTILITIES_CLASSNAME) + .addStatement( + "$1T callSender = new $1T(callback)", + CROSS_PROFILE_CALLBACK_EXCEPTION_PARCEL_CALL_SENDER_CLASSNAME) + .addStatement("callSender.makeParcelCall(parcel)", PARCEL_CLASSNAME) + .addStatement("parcel.recycle()") + .nextControlFlow("catch ($T r)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addComment( + "TODO: Decide what should happen if the connection is dropped between the call and" + + " response") + .endControlFlow() + .endControlFlow(); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSenderClass() { + ClassName className = + getCrossProfileCallbackSenderClassName(generatorContext, callbackInterface); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $1T} which wraps an instance of {@link $2T},\n" + + "extracting results and exceptions in callbacks and passing them on to the" + + " {@link $2T}.\n", + LOCAL_CALLBACK_CLASSNAME, + callbackInterface.interfaceElement()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(LOCAL_CALLBACK_CLASSNAME); + + classBuilder.addField( + FieldSpec.builder(ClassName.get(callbackInterface.interfaceElement()), "callback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder(BUNDLER_CLASSNAME, "bundler") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(ClassName.get(callbackInterface.interfaceElement()), "callback") + .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback") + .addParameter(BUNDLER_CLASSNAME, "bundler") + .beginControlFlow("if (callback == null || bundler == null)") + .addStatement("throw new $T()", NullPointerException.class) + .endControlFlow() + .addStatement("this.callback = callback") + .addStatement("this.exceptionCallback = exceptionCallback") + .addStatement("this.bundler = bundler") + .build()); + + addSenderCallbackMethod(classBuilder); + addSenderExceptionMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addSenderCallbackMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("onResult") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(int.class, "methodIdentifier") + .addParameter(PARCEL_CLASSNAME, "params"); + methodBuilder.beginControlFlow("switch (methodIdentifier)$>"); + + for (ExecutableElement method : callbackInterface.methods()) { + // $> means increase indentation, $< means decrease + methodBuilder.addCode("$<case $L:\n$>", callbackInterface.getIdentifier(method)); + addDispatchCode(methodBuilder, method); + methodBuilder.addStatement("return"); + } + + methodBuilder.endControlFlow(); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void addSenderExceptionMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("onException") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(PARCEL_CLASSNAME, "exception"); + methodBuilder.addStatement( + "$1T throwable = $2T.readThrowableFromParcel(exception)", + Throwable.class, + PARCEL_UTILITIES_CLASSNAME); + + methodBuilder.addStatement("exceptionCallback.onException(throwable)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void addDispatchCode(MethodSpec.Builder methodBuilder, ExecutableElement method) { + for (VariableElement parameter : method.getParameters()) { + methodBuilder.addStatement( + "@SuppressWarnings(\"unchecked\") $1T $2L = ($1T) bundler.readFromParcel(params, $3L)", + parameter.asType(), + parameter.getSimpleName().toString(), + TypeUtils.generateBundlerType(parameter.asType())); + } + + String commaSeparatedParams = + method.getParameters().stream() + .map(p -> p.getSimpleName().toString()) + .collect(joining(",")); + + methodBuilder.addStatement("callback.$L($L)", method.getSimpleName(), commaSeparatedParams); + } + + static ClassName getCrossProfileCallbackMultiInterfaceClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = String.format("%s_Multi", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } + + static ClassName getCrossProfileCallbackMultiMergerResultClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = + String.format("Profile_%s_MultiMergerResult", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } + + static ClassName getCrossProfileCallbackMultiMergerInputClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = + String.format("Profile_%s_MultiMergerInput", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } + + static ClassName getCrossProfileCallbackReceiverClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = String.format("Profile_%s_Receiver", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } + + static ClassName getCrossProfileCallbackSenderClassName( + GeneratorContext generatorContext, CrossProfileCallbackInterfaceInfo callbackInterface) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(callbackInterface.interfaceElement()); + String interfaceName = String.format("Profile_%s_Sender", callbackInterface.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), interfaceName); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java new file mode 100644 index 0000000..075ac6c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CrossProfileTypeCodeGenerator.java @@ -0,0 +1,73 @@ +/* + * 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.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; + +class CrossProfileTypeCodeGenerator { + private boolean generated = false; + private final InterfaceGenerator interfaceGenerator; + private final CurrentProfileGenerator currentProfileGenerator; + private final OtherProfileGenerator otherProfileGenerator; + private final IfAvailableGenerator ifAvailableGenerator; + private final AlwaysThrowsGenerator alwaysThrowsGenerator; + private final MultipleProfilesGenerator multipleProfilesGenerator; + private final DefaultProfileClassGenerator defaultProfileClassGenerator; + private final InternalCrossProfileClassGenerator internalCrossProfileClassGenerator; + private final BundlerGenerator bundlerGenerator; + + public CrossProfileTypeCodeGenerator( + GeneratorContext generatorContext, + ProviderClassInfo providerClass, + CrossProfileTypeInfo crossProfileType) { + checkNotNull(generatorContext); + checkNotNull(crossProfileType); + this.interfaceGenerator = new InterfaceGenerator(generatorContext, crossProfileType); + this.currentProfileGenerator = new CurrentProfileGenerator(generatorContext, crossProfileType); + this.otherProfileGenerator = new OtherProfileGenerator(generatorContext, crossProfileType); + this.ifAvailableGenerator = new IfAvailableGenerator(generatorContext, crossProfileType); + this.alwaysThrowsGenerator = new AlwaysThrowsGenerator(generatorContext, crossProfileType); + this.multipleProfilesGenerator = + new MultipleProfilesGenerator(generatorContext, crossProfileType); + this.defaultProfileClassGenerator = + new DefaultProfileClassGenerator(generatorContext, crossProfileType); + this.internalCrossProfileClassGenerator = + new InternalCrossProfileClassGenerator(generatorContext, providerClass, crossProfileType); + this.bundlerGenerator = new BundlerGenerator(generatorContext, crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "CrossProfileTypeCodeGenerator#generate can only be called once"); + } + generated = true; + + interfaceGenerator.generate(); + currentProfileGenerator.generate(); + otherProfileGenerator.generate(); + ifAvailableGenerator.generate(); + alwaysThrowsGenerator.generate(); + multipleProfilesGenerator.generate(); + defaultProfileClassGenerator.generate(); + internalCrossProfileClassGenerator.generate(); + bundlerGenerator.generate(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java new file mode 100644 index 0000000..103b965 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/CurrentProfileGenerator.java @@ -0,0 +1,219 @@ +/* + * 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.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.EXCEPTION_CALLBACK_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.GeneratorContext; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeKind; + +/** + * Generate the {@code Profile_*_CurrentProfile} class for a single crossProfileType class. + * + * <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 CurrentProfileGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + CurrentProfileGenerator( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("CurrentProfileGenerator#generate can only be called once"); + } + generated = true; + + generateCurrentProfileClass(); + } + + private void generateCurrentProfileClass() { + ClassName className = getCurrentProfileClassName(generatorContext, crossProfileType); + + ClassName singleSenderInterface = + InterfaceGenerator.getSingleSenderInterfaceClassName(generatorContext, crossProfileType); + ClassName singleSenderCanThrowInterface = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} and {@link $T} which makes calls to the current" + + " profile.\n\n" + + "<p>{@link $T} will not be thrown by calls to methods in this class.\n", + singleSenderInterface, + singleSenderCanThrowInterface, + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(singleSenderInterface) + .addSuperinterface(singleSenderCanThrowInterface); + + addCrossProfileConstructor(classBuilder); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + generateMethodOnCurrentProfileClass(classBuilder, method, crossProfileType); + + if (method.isCrossProfileCallback(generatorContext)) { + // To meet the interface for canThrow we need a version with exceptionCallback. + // However we never use it. + generateCrossProfileCallbackWithExceptionMethodOnCurrentProfileClass( + classBuilder, method, crossProfileType); + } + } + + 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()); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addCrossProfileConstructor(TypeSpec.Builder classBuilder) { + classBuilder.addField( + FieldSpec.builder(CONTEXT_CLASSNAME, "context") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + MethodSpec.Builder constructorBuilder = + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("this.context = context"); + + if (!crossProfileType.isStatic()) { + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "crossProfileType") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + constructorBuilder + .addParameter(crossProfileType.className(), "crossProfileType") + .addStatement("this.crossProfileType = crossProfileType"); + } + + classBuilder.addMethod(constructorBuilder.build()); + } + + private void generateMethodOnCurrentProfileClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + CodeBlock crossProfileTypeReference = + method.isStatic() + ? CodeBlock.of("$1T", crossProfileType.className()) + : CodeBlock.of("crossProfileType"); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + crossProfileTypeReference, + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (method.returnType().getKind() != TypeKind.VOID) { + methodCall = CodeBlock.of("return $L", methodCall); + } + + classBuilder.addMethod( + MethodSpec.methodBuilder(method.simpleName()) + .addAnnotation(Override.class) + .addExceptions(method.thrownExceptions()) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addStatement(methodCall) + .build()); + } + + private static void generateCrossProfileCallbackWithExceptionMethodOnCurrentProfileClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + classBuilder.addMethod( + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC) + .returns(method.returnTypeTypeName()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback") + .addStatement( + "$L($L)", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .build()); + } + + static ClassName getCurrentProfileClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_CurrentProfile"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java new file mode 100644 index 0000000..114a19c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DefaultProfileClassGenerator.java @@ -0,0 +1,330 @@ +/* + * 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.AlwaysThrowsGenerator.getAlwaysThrowsClassName; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_AWARE_UTILS_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.VERSION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.VERSION_CODES_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CurrentProfileGenerator.getCurrentProfileClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getMultipleSenderInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.MultipleProfilesGenerator.getMultipleProfilesClassName; +import static com.google.android.enterprise.connectedapps.processor.OtherProfileGenerator.getOtherProfileClassName; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.HashMap; +import java.util.Map; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code DefaultProfile*} class for each cross-profile type. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <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 DefaultProfileClassGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + DefaultProfileClassGenerator( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "DefaultProfileClassGenerator#generate can only be called once"); + } + generated = true; + + generateDefaultProfileClass(); + } + + private void generateDefaultProfileClass() { + ClassName className = getDefaultProfileClassName(generatorContext, crossProfileType); + + ClassName connectorClassName = + crossProfileType.profileConnector().isPresent() + ? crossProfileType.profileConnector().get().connectorClassName() + : PROFILE_CONNECTOR_CLASSNAME; + + ClassName crossProfileTypeInterfaceClassName = + InterfaceGenerator.getCrossProfileTypeInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Default implementation of {@link $T} to be used in production.\n", + crossProfileTypeInterfaceClassName) + .addModifiers(Modifier.FINAL); + + classBuilder.addSuperinterface(crossProfileTypeInterfaceClassName); + + classBuilder.addField( + FieldSpec.builder(connectorClassName, "connector") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addParameter(connectorClassName, "connector") + .addModifiers(Modifier.PUBLIC) + .addStatement("this.connector = connector") + .build()); + + addCurrentMethod(classBuilder); + + classBuilder.addMethod( + MethodSpec.methodBuilder("other") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement( + "return new $T(connector)", + getOtherProfileClassName(generatorContext, crossProfileType)) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("personal") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement("return profile(connector.utils().getPersonalProfile())") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("work") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement("return profile(connector.utils().getWorkProfile())") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("profile") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(PROFILE_CLASSNAME, "profile") + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .beginControlFlow("if (profile.isCurrent())") + .addStatement( + "return ($T) current()", + getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .nextControlFlow("else") + .addComment("must be other profile") + .addStatement("return other()") + .endControlFlow() + .build()); + + ParameterizedTypeName senderMapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), + PROFILE_CLASSNAME, + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + + classBuilder.addMethod( + MethodSpec.methodBuilder("profiles") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(ArrayTypeName.of(PROFILE_CLASSNAME), "profiles") + .varargs(true) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$T senders = new $T<>()", senderMapType, HashMap.class) + .beginControlFlow("for ($1T profileIdentifier : profiles)", PROFILE_CLASSNAME) + .addStatement("senders.put(profileIdentifier, profile(profileIdentifier))") + .endControlFlow() + .addStatement( + "return new $1T(senders)", + getMultipleProfilesClassName(generatorContext, crossProfileType)) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("both") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME) + .addStatement( + "$1T currentProfileIdentifier = utils.getCurrentProfile()", PROFILE_CLASSNAME) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement("$T senders = new $T<>()", senderMapType, HashMap.class) + .addStatement( + "senders.put(currentProfileIdentifier, ($T) current())", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)) + .addStatement( + "return new $1T(senders)", + getMultipleProfilesClassName(generatorContext, crossProfileType)) + .endControlFlow() + .addStatement("$1T otherProfileIdentifier = utils.getOtherProfile()", PROFILE_CLASSNAME) + .addStatement("return profiles(currentProfileIdentifier, otherProfileIdentifier)") + .build()); + + if (!crossProfileType.profileConnector().isPresent() + || crossProfileType.profileConnector().get().primaryProfile() != ProfileType.NONE) { + generatePrimarySecondaryMethods(classBuilder); + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addCurrentMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder currentMethodBuilder = + MethodSpec.methodBuilder("current") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderInterfaceClassName(generatorContext, crossProfileType)); + + if (crossProfileType.isStatic()) { + currentMethodBuilder.addStatement( + "return new $1T(connector.applicationContext())", + getCurrentProfileClassName(generatorContext, crossProfileType)); + } else { + currentMethodBuilder.addStatement( + "return new $1T(connector.applicationContext()," + + " $2T.instance().crossProfileType(connector.applicationContext()))", + getCurrentProfileClassName(generatorContext, crossProfileType), + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + } + + classBuilder.addMethod(currentMethodBuilder.build()); + } + + private void generatePrimarySecondaryMethods(TypeSpec.Builder classBuilder) { + generatePrimaryMethod(classBuilder); + generateSecondaryMethod(classBuilder); + generateSuppliersMethod(classBuilder); + } + + private void generatePrimaryMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("primary") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement( + "$T primaryProfile = connector.utils().getPrimaryProfile()", PROFILE_CLASSNAME) + .beginControlFlow("if (primaryProfile == null)") + .addStatement("throw new $T(\"No primary profile set\")", IllegalStateException.class) + .endControlFlow() + .addStatement("return profile(primaryProfile)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSecondaryMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("secondary") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement( + "return new $T($S)", + getAlwaysThrowsClassName(generatorContext, crossProfileType), + "Cross-profile calls are not supported on this version of Android") + .endControlFlow() + .addStatement( + "$T secondaryProfile = connector.utils().getSecondaryProfile()", PROFILE_CLASSNAME) + .beginControlFlow("if (secondaryProfile == null)") + .addStatement("throw new $T(\"No primary profile set\")", IllegalStateException.class) + .endControlFlow() + .addStatement("return profile(secondaryProfile)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSuppliersMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("suppliers") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if ($T.SDK_INT < $T.O)", VERSION_CLASSNAME, VERSION_CODES_CLASSNAME) + .addStatement("return both()") + .endControlFlow() + .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME) + .addStatement("$1T currentProfile = utils.getCurrentProfile()", PROFILE_CLASSNAME) + .addStatement("$1T secondaryProfile = utils.getSecondaryProfile()", PROFILE_CLASSNAME) + .beginControlFlow("if (secondaryProfile == null)") + .addStatement("throw new $T(\"No primary profile set\")", IllegalStateException.class) + .endControlFlow() + .addStatement("return profiles(currentProfile, secondaryProfile)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getDefaultProfileClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return ClassName.get( + crossProfileType.profileClassName().packageName(), + "Default" + crossProfileType.profileClassName().simpleName()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java new file mode 100644 index 0000000..ff45251 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/DispatcherGenerator.java @@ -0,0 +1,326 @@ +/* + * 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.BACKGROUND_EXCEPTION_THROWER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BINDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_SENDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CALL_RECEIVER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_UTILITIES_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.ServiceGenerator.getConnectedAppsServiceClassName; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import java.util.List; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code *_Dispatcher} class for a single {@link CrossProfileConfiguration} annotated + * class. + * + * <p>This class includes the dispatch of calls to providers. + * + * <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 DispatcherGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileConfigurationInfo configuration; + + DispatcherGenerator( + GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.configuration = checkNotNull(configuration); + } + + void generate() { + if (generated) { + throw new IllegalStateException("DispatcherGenerator#generate can only be called once"); + } + generated = true; + + generateDispatcherClass(); + } + + private void generateDispatcherClass() { + ClassName className = getDispatcherClassName(generatorContext, configuration); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addJavadoc( + "Class for dispatching calls to appropriate providers.\n\n" + + "<p>This uses a {@link $T} to construct calls before passing the completed" + + " call\n" + + "to a provider.\n", + PARCEL_CALL_RECEIVER_CLASSNAME); + + classBuilder.addField( + FieldSpec.builder(PARCEL_CALL_RECEIVER_CLASSNAME, "parcelCallReceiver") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .initializer("new $T()", PARCEL_CALL_RECEIVER_CLASSNAME) + .build()); + + addEnsureValidCallerMethod(classBuilder); + addCallMethod(classBuilder); + addPrepareCallMethod(classBuilder); + addFetchResponseMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private static void addPrepareCallMethod(TypeSpec.Builder classBuilder) { + MethodSpec prepareCallMethod = + MethodSpec.methodBuilder("prepareCall") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .addParameter(int.class, "numBytes") + .addParameter(ArrayTypeName.of(byte.class), "paramBytes") + .addStatement("ensureValidCaller(context)") + .addStatement("parcelCallReceiver.prepareCall(callId, blockId, numBytes, paramBytes)") + .addJavadoc( + "Store a block of bytes to be part of a future call to\n" + + "{@link #call(Context, long, int, long, int, byte[], ICrossProfileCallback)}." + + "\n\n" + + "@param callId Arbitrary identifier used to link together\n" + + " {@link #prepareCall(Context, long, int, int, byte[])} and\n " + + "{@link #call(Context, long, int, long, int, byte[], ICrossProfileCallback)}" + + " calls.\n" + + "@param blockId The (zero indexed) number of this block. Each block should" + + " be\n {@link $1T#MAX_BYTES_PER_BLOCK} bytes so the total number of blocks" + + " is\n {@code numBytes / $1T#MAX_BYTES_PER_BLOCK}.\n" + + "@param numBytes The total number of bytes being transferred (across all" + + " blocks for this call,\n including the final {@link" + + " #call(Context, long, int, long, int, byte[], ICrossProfileCallback)}.\n" + + "@param paramBytes The bytes for this block. Should contain\n {@link" + + " $1T#MAX_BYTES_PER_BLOCK} bytes.\n\n" + + "@see $2T#prepareCall(long, int, int, byte[])", + CROSS_PROFILE_SENDER_CLASSNAME, + PARCEL_CALL_RECEIVER_CLASSNAME) + .build(); + classBuilder.addMethod(prepareCallMethod); + } + + private static void addFetchResponseMethod(TypeSpec.Builder classBuilder) { + MethodSpec prepareCallMethod = + MethodSpec.methodBuilder("fetchResponse") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .returns(ArrayTypeName.of(byte.class)) + .addStatement("ensureValidCaller(context)") + .addStatement("return parcelCallReceiver.getPreparedResponse(callId, blockId)") + .addJavadoc( + "Fetch a response block if a previous call to\n {@link #call(Context, long, int," + + " long, int, byte[], ICrossProfileCallback)} returned a\n byte array with" + + " 1 as the first byte.\n\n" + + "@param callId should be the same callId used with\n {@link #call(Context," + + " long, int, long, int, byte[], ICrossProfileCallback)}\n" + + "@param blockId The (zero indexed) number of the block to fetch.\n\n" + + "@see $1T#getPreparedResponse(long, int)\n", + PARCEL_CALL_RECEIVER_CLASSNAME) + .build(); + classBuilder.addMethod(prepareCallMethod); + } + + private static void addEnsureValidCallerMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + methodCode.addStatement( + "$T[] callingPackageNames =" + + " context.getPackageManager().getPackagesForUid($T.getCallingUid())", + String.class, + BINDER_CLASSNAME); + methodCode.beginControlFlow("for (String callingPackageName : callingPackageNames)"); + methodCode.beginControlFlow("if (context.getPackageName().equals(callingPackageName))"); + methodCode.addStatement("return"); + methodCode.endControlFlow(); + methodCode.endControlFlow(); + + methodCode.addStatement( + "throw new $T(\"Cross-profile functionality is only available within the same package\")", + IllegalStateException.class); + + MethodSpec ensureValidCallerMethod = + MethodSpec.methodBuilder("ensureValidCaller") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addCode(methodCode.build()) + .build(); + + classBuilder.addMethod(ensureValidCallerMethod); + } + + private void addCallMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + methodCode.beginControlFlow("try"); + + methodCode.addStatement("ensureValidCaller(context)"); + + methodCode.addStatement( + "$1T parcel = parcelCallReceiver.getPreparedCall(callId, blockId, paramBytes)", + PARCEL_CLASSNAME); + + List<ProviderClassInfo> providers = configuration.providers().asList(); + + if (!providers.isEmpty()) { + addProviderDispatch(methodCode, providers); + } + + methodCode.addStatement( + "throw new $T(\"Unknown type identifier \" + crossProfileTypeIdentifier)", + IllegalArgumentException.class); + + methodCode.nextControlFlow("catch ($T e)", RuntimeException.class); + // parcel is recycled in this method + methodCode.addStatement("$1T throwableParcel = $1T.obtain()", PARCEL_CLASSNAME); + methodCode.add("throwableParcel.writeInt(1); //errors\n"); + methodCode.addStatement( + "$T.writeThrowableToParcel(throwableParcel, e)", PARCEL_UTILITIES_CLASSNAME); + methodCode.addStatement( + "$1T throwableBytes = parcelCallReceiver.prepareResponse(callId, throwableParcel)", + ArrayTypeName.of(byte.class)); + methodCode.addStatement("throwableParcel.recycle()"); + + methodCode.addStatement("$T.throwInBackground(e)", BACKGROUND_EXCEPTION_THROWER_CLASSNAME); + + methodCode.addStatement("return throwableBytes"); + methodCode.endControlFlow(); + + MethodSpec callMethod = + MethodSpec.methodBuilder("call") + .addModifiers(Modifier.PUBLIC) + .returns(ArrayTypeName.of(byte.class)) + .addAnnotation(AnnotationSpec.builder(SuppressWarnings.class) + // Allow catching of RuntimeException + .addMember("value", "\"CatchSpecificExceptionsChecker\"") + .build()) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .addParameter(long.class, "crossProfileTypeIdentifier") + .addParameter(int.class, "methodIdentifier") + .addParameter(ArrayTypeName.of(byte.class), "paramBytes") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addCode(methodCode.build()) + .addJavadoc( + "Make a call, which will execute some annotated method and return a response.\n\n" + + "<p>The parameters to the call should be contained in a {@link $1T}" + + " marshalled into\n" + + "a byte array. If the byte array is larger than {@link" + + " $2T#MAX_BYTES_PER_BLOCK},\n" + + "then it should be separated into blocks of {@link" + + " $2T#MAX_BYTES_PER_BLOCK}\n" + + "bytes, and {@link #prepareCall(Context, long, int, int, byte[])} used to" + + " set all but the final\n" + + "block, before calling this method with the final block.\n\n" + + "<p>The response will be an array of bytes. If the response is complete (it" + + " fits into a single\n" + + "block), then the first byte will be 0, otherwise the first byte will be 1" + + " and the next 4 bytes\n" + + "will be an int representing the total size of the return value. The rest of" + + " the bytes are the\n" + + "first block of the return value. {@link #fetchResponse(Context, long, int)" + + " should be used to\n" + + "fetch further blocks.\n\n" + + "@param callId Arbitrary identifier used to link together\n" + + " {@link #prepareCall(Context, long, int, int, byte[])} and\n" + + " {@link #call(Context, long, int, long, int, byte[]," + + " ICrossProfileCallback)} calls.\n" + + "@param blockId The (zero indexed) number of this block. Each block should" + + " be\n {@link CrossProfileSender#MAX_BYTES_PER_BLOCK} bytes so the total" + + " number of blocks is\n {@code numBytes /" + + " CrossProfileSender#MAX_BYTES_PER_BLOCK}.\n" + + "@param crossProfileTypeIdentifier The generated identifier for the type" + + " which contains the\n method being called.\n" + + "@param methodIdentifier The index of the method being called on the cross" + + " profile type.\n" + + "@param paramBytes The bytes for the final block, this will be merged with" + + " any blocks\n previously set by a call to" + + " {@link #prepareCall(Context, long, int, int, byte[])}.\n" + + "@param callback A callback to be used if this is an asynchronous call." + + " Otherwise this should be\n {@code null}.\n\n" + + "@see $3T#getPreparedCall(long, int, byte[])\n", + PARCEL_CLASSNAME, + CROSS_PROFILE_SENDER_CLASSNAME, + PARCEL_CALL_RECEIVER_CLASSNAME) + .build(); + + classBuilder.addMethod(callMethod); + } + + private void addProviderDispatch( + CodeBlock.Builder methodCode, List<ProviderClassInfo> providers) { + for (ProviderClassInfo provider : providers) { + addProviderDispatchInner(methodCode, provider); + } + } + + private void addProviderDispatchInner(CodeBlock.Builder methodCode, ProviderClassInfo provider) { + String condition = + provider.allCrossProfileTypes().stream() + .map( + h -> + "crossProfileTypeIdentifier == " + + h.identifier() + + "L // " + + h.crossProfileTypeElement().getQualifiedName() + + "\n") + .collect(joining(" || ")); + + methodCode.beginControlFlow("if ($L)", condition); + methodCode.addStatement( + "$1T returnParcel = $2T.instance().call(context.getApplicationContext()," + + " crossProfileTypeIdentifier, methodIdentifier, parcel, callback)", + PARCEL_CLASSNAME, + InternalProviderClassGenerator.getInternalProviderClassName(generatorContext, provider)); + methodCode.addStatement( + "$1T returnBytes = parcelCallReceiver.prepareResponse(callId, returnParcel)", + ArrayTypeName.of(byte.class)); + methodCode.addStatement("parcel.recycle()"); + methodCode.addStatement("returnParcel.recycle()"); + methodCode.addStatement("return returnBytes"); + methodCode.endControlFlow(); + } + + static ClassName getDispatcherClassName( + GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + ClassName serviceName = getConnectedAppsServiceClassName(generatorContext, configuration); + return ClassName.get(serviceName.packageName(), serviceName.simpleName() + "_Dispatcher"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java new file mode 100644 index 0000000..f1f566d --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/EarlyValidator.java @@ -0,0 +1,1297 @@ +/* + * 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.PARCELABLE_CREATOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.findCrossProfileMethodsInClass; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileConfigurationAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileConfigurationsAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileProviderAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterFor; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterForClass; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes.TypeCheckContext; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileProviderAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapperAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapperAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTestInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorProviderClassInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.tools.Diagnostic.Kind; + +/** Validator to check that annotations have been used correctly before generating code. */ +public final class EarlyValidator { + + private static final String MULTIPLE_PROVIDERS_ERROR = + "The @CROSS_PROFILE_ANNOTATION annotated type %s has been provided more than once"; + private static final String PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION must only return" + + " @CROSS_PROFILE_ANNOTATION annotated types"; + private static final String INVALID_CONSTRUCTORS_ERROR = + "Provider classes must have a single public constructor which takes either a single Context" + + " argument or no arguments"; + private static final String PROVIDER_INCORRECT_ARGS_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can only take a single Context" + + " argument, or no-args"; + private static final String STATIC_PROVIDER_ERROR = + "Methods annotated @CROSS_PROFILE_PROVIDER_ANNOTATION can not be static"; + private static final String UNSUPPORTED_RETURN_TYPE_ERROR = + "The type %s cannot be returned by methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String UNSUPPORTED_PARAMETER_TYPE_CROSS_PROFILE_METHOD = + "The type %s cannot be used by parameters of methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String UNSUPPORTED_PARAMETER_TYPE_CROSS_ASYNC_CALLBACK = + "The type %s cannot be used by parameters of methods on interfaces annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String CROSS_PROFILE_TYPE_DEFAULT_PACKAGE_ERROR = + "@CROSS_PROFILE_ANNOTATION types must not be in the default package"; + private static final String NON_PUBLIC_CROSS_PROFILE_TYPE_ERROR = + "@CROSS_PROFILE_ANNOTATION types must be public"; + private static final String NOT_A_PROVIDER_CLASS_ERROR = + "All classes specified in 'providers' must be provider classes"; + private static final String CONNECTOR_MUST_BE_INTERFACE = "Connectors must be interfaces"; + private static final String CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces specified as a connector must extend ProfileConnector"; + private static final String CUSTOM_PROFILE_CONNECTOR_MUST_BE_INTERFACE = + "@CustomProfileConnector must only be applied to interfaces"; + private static final String CUSTOM_USER_CONNECTOR_MUST_BE_INTERFACE = + "@CustomUserConnector must only be applied to interfaces"; + private static final String GENERATED_PROFILE_CONNECTOR_MUST_BE_INTERFACE = + "@GeneratedProfileConnector must only be applied to interfaces"; + private static final String GENERATED_USER_CONNECTOR_MUST_BE_INTERFACE = + "@GeneratedUserConnector must only be applied to interfaces"; + private static final String CUSTOM_PROFILE_CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces annotated with @CustomProfileConnector must extend ProfileConnector"; + private static final String CUSTOM_USER_CONNECTOR_MUST_EXTEND_CONNECTOR = + "Interfaces annotated with @CustomUserConnector must extend UserConnector"; + private static final String GENERATED_PROFILE_CONNECTOR_MUST_EXTEND_PROFILE_CONNECTOR = + "Interfaces annotated with @GeneratedProfileConnector must extend ProfileConnector"; + private static final String GENERATED_USER_CONNECTOR_MUST_EXTEND_USER_CONNECTOR = + "Interfaces annotated with @GeneratedUserConnector must extend UserConnector"; + private static final String CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must not be in the default package"; + private static final String NOT_INTERFACE_ERROR = + "Only interfaces may be annotated @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String NOT_ONE_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) must have exactly one" + + " method"; + private static final String NO_METHODS_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have at least one method"; + private static final String DEFAULT_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no default methods"; + private static final String STATIC_METHOD_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must have no static methods"; + private static final String NOT_VOID_ERROR = + "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION must return void"; + private static final String GENERIC_CALLBACK_INTERFACE_ERROR = + "Interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION can not be generic"; + private static final String MORE_THAN_ONE_PARAMETER_ERROR = + "Methods on interfaces annotated @CROSS_PROFILE_CALLBACK_ANNOTATION(simple=true) can only" + + " take a single parameter"; + private static final String MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR = + "Methods annotated @CROSS_PROFILE_ANNOTATION can have a maximum of one parameter of a type" + + " annotated @CROSS_PROFILE_CALLBACK_ANNOTATION"; + private static final String NON_VOID_CALLBACK_ERROR = + "Methods annotated @CROSS_PROFILE_ANNOTATION which take a parameter type annotated" + + " @CROSS_PROFILE_CALLBACK_ANNOTATION must return void"; + private static final String METHOD_ISSTATIC_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify isStatic"; + private static final String METHOD_CONNECTOR_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a connector"; + private static final String METHOD_PARCELABLE_WRAPPERS_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify parcelable wrappers"; + private static final String METHOD_CLASSNAME_ERROR = + "@CROSS_PROFILE_ANNOTATION annotations on methods can not specify a profile class name"; + private static final String INVALID_TIMEOUT_MILLIS = "timeoutMillis must be positive"; + private static final String ADDITIONAL_PROFILE_CONNECTOR_METHODS_ERROR = + "Interfaces annotated with @GeneratedProfileConnector can not declare non-static methods"; + private static final String ADDITIONAL_USER_CONNECTOR_METHODS_ERROR = + "Interfaces annotated with @GeneratedUserConnector can not declare non-static methods"; + private static final String NOT_A_CONFIGURATION_ERROR = + "Configurations referenced in a @CROSS_PROFILE_TEST_ANNOTATION annotation must be annotated" + + " @CROSS_PROFILE_CONFIGURATION_ANNOTATION or @CROSS_PROFILE_CONFIGURATIONS_ANNOTATION"; + private static final String ASYNC_DECLARED_EXCEPTION_ERROR = + "Asynchronous methods annotated @CROSS_PROFILE_ANNOTATION cannot declare exceptions"; + private static final String NOT_PARCELABLE_ERROR = + "Classes annotated @CustomParcelableWrapper must implement Parcelable"; + private static final String INCORRECT_OF_METHOD = + "Classes annotated @CustomParcelableWrapper must have a static 'of' method which takes a" + + " Bundler, a BundlerType, and an instance of the wrapped type as arguments and returns" + + " an instance of the parcelable wrapper"; + private static final String INCORRECT_GET_METHOD = + "Classes annotated @CustomParcelableWrapper must have a static 'get' method which takes no" + + " arguments and returns an instance of the wrapped class"; + private static final String INCORRECT_PARCELABLE_IMPLEMENTATION = + "Classes annotated @CustomParcelableWrapper must correctly implement Parcelable"; + private static final String PARCELABLE_WRAPPER_ANNOTATION_ERROR = + "Parcelable Wrappers must be annotated @CustomParcelableWrapper"; + private static final String DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR = + "Classes annotated @CustomFutureWrapper must extend FutureWrapper"; + private static final String INCORRECT_CREATE_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a create method which returns an instance" + + " of the class and takes a Bundler and BundlerType argument"; + private static final String INCORRECT_GET_FUTURE_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a getFuture method which returns an" + + " instance of the wrapped future and takes no arguments"; + private static final String INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a writeFutureResult method" + + " which returns void and takes as arguments an instance of the wrapped future and a" + + " FutureResultWriter"; + private static final String INCORRECT_GROUP_RESULTS_METHOD_ERROR = + "Classes annotated @CustomFutureWrapper must have a groupResults method which returns an" + + " instance of the wrapped future containing a map from Profile to the wrapped future" + + " type, and takes as an argument a map from Profile to an instance of the wrapped" + + " future"; + private static final String FUTURE_WRAPPER_ANNOTATION_ERROR = + "Future Wrappers must be annotated @CustomFutureWrapper"; + private static final String IMPORTS_NOT_PROFILE_CONNECTOR_ERROR = + "Classes included in includes= must be annotated @CustomProfileConnector"; + private static final String IMPORTS_NOT_USER_CONNECTOR_ERROR = + "Classes included in includes= must be annotated @CustomUserConnector"; + private static final String MUST_HAVE_ONE_TYPE_PARAMETER_ERROR = + "Classes annotated @CustomFutureWrapper must have a single type parameter"; + private static final String NOT_STATIC_ERROR = + "Types annotated @CROSS_PROFILE_ANNOTATION(isStatic=true) must not contain any non-static" + + " methods annotated @CROSS_PROFILE_ANNOTATION"; + private static final String METHOD_STATICTYPES_ERROR = + "@CROSS_PROFILE_PROVIDER_ANNOTATION annotations on methods can not specify staticTypes"; + + private final ValidatorContext validatorContext; + private final TypeMirror contextType; + private final TypeMirror profileConnectorType; + private final TypeMirror userConnectorType; + private final TypeMirror parcelableType; + private final TypeMirror bundlerType; + private final TypeMirror bundlerTypeType; + private final TypeMirror futureResultWriterType; + private final TypeMirror profileType; + + EarlyValidator(ValidatorContext validatorContext) { + this.validatorContext = validatorContext; + contextType = validatorContext.elements().getTypeElement("android.content.Context").asType(); + + parcelableType = validatorContext.elements().getTypeElement("android.os.Parcelable").asType(); + + profileConnectorType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.ProfileConnector") + .asType(); + + userConnectorType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.UserConnector") + .asType(); + + bundlerType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.internal.Bundler") + .asType(); + + bundlerTypeType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.internal.BundlerType") + .asType(); + + futureResultWriterType = + validatorContext + .elements() + .getTypeElement( + "com.google.android.enterprise.connectedapps.internal.FutureResultWriter") + .asType(); + + profileType = + validatorContext + .elements() + .getTypeElement("com.google.android.enterprise.connectedapps.Profile") + .asType(); + } + + /** + * Validate code. + * + * <p>This will show errors for all issues found. It will not terminate upon finding the first + * error. + * + * @return True if the code is valid + */ + boolean validate() { + + return Stream.of( + validateProfileConnectorInterfaces(validatorContext.newProfileConnectorInterfaces()), + validateUserConnectorInterfaces(validatorContext.newUserConnectorInterfaces()), + validateGeneratedProfileConnectors(validatorContext.newGeneratedProfileConnectors()), + validateGeneratedUserConnectors(validatorContext.newGeneratedUserConnectors()), + validateConfigurations(validatorContext.newConfigurations()), + validateCrossProfileTypes(validatorContext.newCrossProfileTypes()), + validateProviderMethods(validatorContext.newProviderMethods()), + validateProviderClasses(validatorContext.newProviderClasses()), + validateCrossProfileCallbackInterfaces( + validatorContext.newCrossProfileCallbackInterfaces()), + validateCrossProfileTests(validatorContext.newCrossProfileTests()), + validateCustomParcelableWrappers(validatorContext.newCustomParcelableWrappers()), + validateCustomFutureWrappers(validatorContext.newCustomFutureWrappers())) + .allMatch(b -> b); + } + + private boolean validateProfileConnectorInterfaces( + Collection<ProfileConnectorInfo> connectorInterfaces) { + boolean isValid = true; + + for (ProfileConnectorInfo connectorInterface : connectorInterfaces) { + isValid = validateProfileConnectorInterface(connectorInterface) && isValid; + } + + return isValid; + } + + private boolean validateProfileConnectorInterface(ProfileConnectorInfo connectorInterface) { + boolean isValid = true; + + if (!connectorInterface.connectorElement().getKind().equals(ElementKind.INTERFACE)) { + showError(CUSTOM_PROFILE_CONNECTOR_MUST_BE_INTERFACE, connectorInterface.connectorElement()); + isValid = false; + } + + if (!implementsInterface(connectorInterface.connectorElement(), profileConnectorType)) { + showError( + CUSTOM_PROFILE_CONNECTOR_MUST_EXTEND_CONNECTOR, connectorInterface.connectorElement()); + isValid = false; + } + + for (TypeElement parcelableWrapper : connectorInterface.parcelableWrapperClasses()) { + if (parcelableWrapper.getAnnotation(CustomParcelableWrapper.class) == null) { + showError(PARCELABLE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement()); + } + } + + for (TypeElement futureWrapper : connectorInterface.futureWrapperClasses()) { + if (futureWrapper.getAnnotation(CustomFutureWrapper.class) == null) { + isValid = false; + showError(FUTURE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement()); + } + } + + for (TypeElement importer : connectorInterface.importsClasses()) { + if (importer.getAnnotation(CustomProfileConnector.class) == null) { + isValid = false; + showError(IMPORTS_NOT_PROFILE_CONNECTOR_ERROR, connectorInterface.connectorElement()); + showError(IMPORTS_NOT_PROFILE_CONNECTOR_ERROR, connectorInterface.connectorElement()); + } + } + + return isValid; + } + + private boolean validateUserConnectorInterfaces( + Collection<UserConnectorInfo> connectorInterfaces) { + boolean isValid = true; + + for (UserConnectorInfo connectorInterface : connectorInterfaces) { + isValid = validateUserConnectorInterface(connectorInterface) && isValid; + } + + return isValid; + } + + private boolean validateUserConnectorInterface(UserConnectorInfo connectorInterface) { + boolean isValid = true; + + if (!connectorInterface.connectorElement().getKind().equals(ElementKind.INTERFACE)) { + showError(CUSTOM_USER_CONNECTOR_MUST_BE_INTERFACE, connectorInterface.connectorElement()); + isValid = false; + } + + if (!implementsInterface(connectorInterface.connectorElement(), userConnectorType)) { + showError(CUSTOM_USER_CONNECTOR_MUST_EXTEND_CONNECTOR, connectorInterface.connectorElement()); + isValid = false; + } + + for (TypeElement parcelableWrapper : connectorInterface.parcelableWrapperClasses()) { + if (parcelableWrapper.getAnnotation(CustomParcelableWrapper.class) == null) { + showError(PARCELABLE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement()); + } + } + + for (TypeElement futureWrapper : connectorInterface.futureWrapperClasses()) { + if (futureWrapper.getAnnotation(CustomFutureWrapper.class) == null) { + isValid = false; + showError(FUTURE_WRAPPER_ANNOTATION_ERROR, connectorInterface.connectorElement()); + } + } + + for (TypeElement importer : connectorInterface.importsClasses()) { + if (importer.getAnnotation(CustomUserConnector.class) == null) { + isValid = false; + showError(IMPORTS_NOT_USER_CONNECTOR_ERROR, connectorInterface.connectorElement()); + } + } + + return isValid; + } + + private boolean validateGeneratedProfileConnectors(Collection<TypeElement> generatedConnectors) { + boolean isValid = true; + + for (TypeElement generatedConnector : generatedConnectors) { + isValid = validateGeneratedProfileConnector(generatedConnector) && isValid; + } + + return isValid; + } + + private boolean validateGeneratedProfileConnector(TypeElement generatedConnector) { + boolean isValid = true; + + if (!generatedConnector.getKind().equals(ElementKind.INTERFACE)) { + showError(GENERATED_PROFILE_CONNECTOR_MUST_BE_INTERFACE, generatedConnector); + isValid = false; + } + + if (!implementsInterface(generatedConnector, profileConnectorType)) { + showError(GENERATED_PROFILE_CONNECTOR_MUST_EXTEND_PROFILE_CONNECTOR, generatedConnector); + isValid = false; + } + + if (generatedConnector.getEnclosedElements().stream() + .anyMatch(i -> !i.getModifiers().contains(Modifier.STATIC))) { + showError(ADDITIONAL_PROFILE_CONNECTOR_METHODS_ERROR, generatedConnector); + isValid = false; + } + + return isValid; + } + + private boolean validateGeneratedUserConnectors(Collection<TypeElement> generatedConnectors) { + boolean isValid = true; + + for (TypeElement generatedConnector : generatedConnectors) { + isValid = validateGeneratedUserConnector(generatedConnector) && isValid; + } + + return isValid; + } + + private boolean validateGeneratedUserConnector(TypeElement generatedConnector) { + boolean isValid = true; + + if (!generatedConnector.getKind().equals(ElementKind.INTERFACE)) { + showError(GENERATED_USER_CONNECTOR_MUST_BE_INTERFACE, generatedConnector); + isValid = false; + } + + if (!implementsInterface(generatedConnector, userConnectorType)) { + showError(GENERATED_USER_CONNECTOR_MUST_EXTEND_USER_CONNECTOR, generatedConnector); + isValid = false; + } + + if (generatedConnector.getEnclosedElements().stream() + .anyMatch(i -> !i.getModifiers().contains(Modifier.STATIC))) { + showError(ADDITIONAL_USER_CONNECTOR_METHODS_ERROR, generatedConnector); + isValid = false; + } + + return isValid; + } + + private boolean implementsInterface(TypeElement type, TypeMirror interfaceType) { + for (TypeMirror t : type.getInterfaces()) { + if (validatorContext.types().isSameType(t, interfaceType)) { + return true; + } + } + return false; + } + + private boolean validateConfigurations( + Collection<ValidatorCrossProfileConfigurationInfo> configurations) { + boolean isValid = true; + + for (ValidatorCrossProfileConfigurationInfo configuration : configurations) { + isValid = validateConfiguration(configuration) && isValid; + } + + return isValid; + } + + private boolean validateConfiguration(ValidatorCrossProfileConfigurationInfo configuration) { + boolean isValid = true; + + for (TypeElement providerClass : configuration.providerClassElements()) { + if (!hasCrossProfileProviderAnnotation(providerClass) + && GeneratorUtilities.findCrossProfileProviderMethodsInClass(providerClass).isEmpty()) { + showError(NOT_A_PROVIDER_CLASS_ERROR, configuration.configurationElement()); + isValid = false; + } + } + + if (configuration.connector().isPresent() + && !configuration.connector().get().getKind().equals(ElementKind.INTERFACE)) { + showError(CONNECTOR_MUST_BE_INTERFACE, configuration.configurationElement()); + isValid = false; + } + + if (configuration.connector().isPresent() + && !implementsInterface(configuration.connector().get(), profileConnectorType)) { + showError(CONNECTOR_MUST_EXTEND_CONNECTOR, configuration.configurationElement()); + isValid = false; + } + + return isValid; + } + + private boolean validateCrossProfileTypes( + Collection<ValidatorCrossProfileTypeInfo> crossProfileTypes) { + boolean isValid = + validateCrossProfileTypesAreProvided( + crossProfileTypes.stream() + .map(ValidatorCrossProfileTypeInfo::crossProfileTypeElement) + .collect(toSet()), + validatorContext.newProviderMethods(), + validatorContext.newProviderClasses()); + + for (ValidatorCrossProfileTypeInfo crossProfileType : crossProfileTypes) { + isValid = validateCrossProfileType(crossProfileType) && isValid; + } + + return isValid; + } + + private boolean validateCrossProfileType(ValidatorCrossProfileTypeInfo crossProfileType) { + boolean isValid = true; + + PackageElement packageElement = + (PackageElement) crossProfileType.crossProfileTypeElement().getEnclosingElement(); + if (packageElement.getQualifiedName().toString().isEmpty()) { + showError( + CROSS_PROFILE_TYPE_DEFAULT_PACKAGE_ERROR, + crossProfileType.crossProfileTypeElement(), + validationMessageFormatterForClass(crossProfileType.crossProfileTypeElement())); + isValid = false; + } + + if (!crossProfileType.crossProfileTypeElement().getModifiers().contains(Modifier.PUBLIC)) { + showError( + NON_PUBLIC_CROSS_PROFILE_TYPE_ERROR, + crossProfileType.crossProfileTypeElement(), + validationMessageFormatterForClass(crossProfileType.crossProfileTypeElement())); + isValid = false; + } + + if (crossProfileType.isStatic()) { + for (ExecutableElement crossProfileMethod : crossProfileType.crossProfileMethods()) { + if (!crossProfileMethod.getModifiers().contains(Modifier.STATIC)) { + showError(NOT_STATIC_ERROR, crossProfileMethod); + isValid = false; + } + } + } + + if (crossProfileType.profileConnector().isPresent() + && !crossProfileType + .profileConnector() + .get() + .connectorElement() + .getKind() + .equals(ElementKind.INTERFACE)) { + showError(CONNECTOR_MUST_BE_INTERFACE, crossProfileType.crossProfileTypeElement()); + isValid = false; + } + + if (crossProfileType.profileConnector().isPresent() + && !implementsInterface( + crossProfileType.profileConnector().get().connectorElement(), profileConnectorType)) { + showError(CONNECTOR_MUST_EXTEND_CONNECTOR, crossProfileType.crossProfileTypeElement()); + isValid = false; + } + + if (crossProfileType.timeoutMillis() <= 0) { + showError(INVALID_TIMEOUT_MILLIS, crossProfileType.crossProfileTypeElement()); + isValid = false; + } + + for (TypeElement parcelableWrapper : crossProfileType.parcelableWrapperClasses()) { + if (parcelableWrapper.getAnnotation(CustomParcelableWrapper.class) == null) { + showError(PARCELABLE_WRAPPER_ANNOTATION_ERROR, crossProfileType.crossProfileTypeElement()); + } + } + + for (TypeElement futureWrapper : crossProfileType.futureWrapperClasses()) { + if (futureWrapper.getAnnotation(CustomFutureWrapper.class) == null) { + isValid = false; + showError(FUTURE_WRAPPER_ANNOTATION_ERROR, crossProfileType.crossProfileTypeElement()); + } + } + + isValid = + crossProfileType.crossProfileMethods().stream() + .map(m -> validateCrossProfileMethod(crossProfileType, m)) + .allMatch(b -> b) + && isValid; + + return isValid; + } + + private boolean validateCrossProfileTypesAreProvided( + Collection<TypeElement> crossProfileTypeElements, + Collection<ExecutableElement> providerMethods, + Collection<ValidatorProviderClassInfo> providerClasses) { + Map<String, Collection<Element>> crossProfileTypeProviders = + crossProfileTypeElements.stream() + .collect(toMap(element -> element.asType().toString(), element -> new HashSet<>())); + + for (ExecutableElement provider : providerMethods) { + String providedTypeName = provider.getReturnType().toString(); + + if (crossProfileTypeProviders.containsKey(providedTypeName)) { + crossProfileTypeProviders.get(providedTypeName).add(provider); + } + } + + for (ValidatorProviderClassInfo provider : providerClasses) { + for (TypeElement staticType : provider.staticTypes()) { + String providedTypeName = staticType.getQualifiedName().toString(); + + if (crossProfileTypeProviders.containsKey(providedTypeName)) { + crossProfileTypeProviders.get(providedTypeName).add(provider.providerClassElement()); + } + } + } + + boolean isValid = true; + + for (String crossProfileType : crossProfileTypeProviders.keySet()) { + Collection<Element> providers = crossProfileTypeProviders.get(crossProfileType); + + if (providers.size() > 1) { + isValid = false; + for (Element providerElement : providers) { + showError(String.format(MULTIPLE_PROVIDERS_ERROR, crossProfileType), providerElement); + } + } + } + + return isValid; + } + + private boolean validateProviderMethods(Collection<ExecutableElement> providerMethods) { + boolean isValid = true; + + for (ExecutableElement providerMethod : providerMethods) { + TypeElement crossProfileType = + validatorContext.elements().getTypeElement(providerMethod.getReturnType().toString()); + if (!hasCrossProfileAnnotation(crossProfileType) + && findCrossProfileMethodsInClass(crossProfileType).isEmpty()) { + showError(PROVIDING_NON_CROSS_PROFILE_TYPE_ERROR, providerMethod); + isValid = false; + } + + if (providerMethod.getParameters().stream() + .anyMatch(v -> !validatorContext.types().isSameType(v.asType(), contextType)) + || providerMethod.getParameters().size() > 1) { + showError(PROVIDER_INCORRECT_ARGS_ERROR, providerMethod); + isValid = false; + } + + if (providerMethod.getModifiers().contains(Modifier.STATIC)) { + showError(STATIC_PROVIDER_ERROR, providerMethod); + isValid = false; + } + + CrossProfileProviderAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileProviderAnnotationInfo( + providerMethod, validatorContext.types(), validatorContext.elements()); + + if (!annotationInfo.staticTypes().isEmpty()) { + showError(METHOD_STATICTYPES_ERROR, providerMethod); + isValid = false; + } + } + + return isValid; + } + + private boolean validateProviderClasses(Collection<ValidatorProviderClassInfo> providerClasses) { + boolean isValid = true; + + for (ValidatorProviderClassInfo provider : providerClasses) { + if (!hasValidProviderClassConstructor(provider.providerClassElement())) { + showError(INVALID_CONSTRUCTORS_ERROR, provider.providerClassElement()); + isValid = false; + } + + if (provider.providerClassElement().getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind().equals(ElementKind.CONSTRUCTOR)) + .filter(e -> e.getModifiers().contains(Modifier.PUBLIC)) + .count() + > 1) { + showError(INVALID_CONSTRUCTORS_ERROR, provider.providerClassElement()); + isValid = false; + } + } + + return isValid; + } + + private boolean hasValidProviderClassConstructor(TypeElement clazz) { + for (ExecutableElement constructor : + ElementFilter.constructorsIn(clazz.getEnclosedElements())) { + if (constructor.getModifiers().contains(Modifier.PUBLIC)) { + if (isValidProviderClassConstructor(constructor)) { + return true; + } + } + } + return false; + } + + private boolean isValidProviderClassConstructor(ExecutableElement constructor) { + if (constructor.getParameters().size() == 0) { + return true; + } + + if (constructor.getParameters().size() > 1) { + return false; + } + + return validatorContext + .types() + .isSameType(constructor.getParameters().iterator().next().asType(), contextType); + } + + private boolean validateCrossProfileMethod( + ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement crossProfileMethod) { + boolean isValid = true; + + CrossProfileAnnotationInfo crossProfileAnnotation = + AnnotationFinder.extractCrossProfileAnnotationInfo( + crossProfileMethod, validatorContext.types(), validatorContext.elements()); + + if (!crossProfileAnnotation.connectorIsDefault()) { + showError(METHOD_CONNECTOR_ERROR, crossProfileMethod); + isValid = false; + } + + if (!crossProfileAnnotation.parcelableWrapperClasses().isEmpty()) { + showError(METHOD_PARCELABLE_WRAPPERS_ERROR, crossProfileMethod); + isValid = false; + } + + if (!crossProfileAnnotation.isProfileClassNameDefault()) { + showError(METHOD_CLASSNAME_ERROR, crossProfileMethod); + isValid = false; + } + + if (crossProfileAnnotation.timeoutMillis().isPresent() + && crossProfileAnnotation.timeoutMillis().get() <= 0) { + showError(INVALID_TIMEOUT_MILLIS, crossProfileMethod); + isValid = false; + } + + if (!crossProfileMethod.getThrownTypes().isEmpty()) { + if (CrossProfileMethodInfo.isFuture(crossProfileType.supportedTypes(), crossProfileMethod) + || CrossProfileMethodInfo.getCrossProfileCallbackParam( + validatorContext.elements(), crossProfileMethod) + .isPresent()) { + showError(ASYNC_DECLARED_EXCEPTION_ERROR, crossProfileMethod); + isValid = false; + } + } + + if (crossProfileAnnotation.isStatic()) { + showError(METHOD_ISSTATIC_ERROR, crossProfileMethod); + isValid = false; + } + + isValid = + isValid + && validateReturnType(crossProfileType, crossProfileMethod) + && validateParameterTypesForCrossProfileMethod(crossProfileType, crossProfileMethod); + return isValid; + } + + private boolean validateReturnType( + ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement crossProfileMethod) { + TypeMirror returnType = crossProfileMethod.getReturnType(); + + if (crossProfileType.supportedTypes().isValidReturnType(returnType)) { + return true; + } + + showError(String.format(UNSUPPORTED_RETURN_TYPE_ERROR, returnType), crossProfileMethod); + return false; + } + + private boolean validateParameterTypesForCrossProfileMethod( + ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement crossProfileMethod) { + boolean isValid = + crossProfileMethod.getParameters().stream() + .allMatch(p -> validateParameterTypeForCrossProfileMethod(crossProfileType, p)); + + List<TypeElement> crossProfileCallbackParameters = + crossProfileMethod.getParameters().stream() + .map(v -> validatorContext.elements().getTypeElement(v.asType().toString())) + .filter(Objects::nonNull) + .filter(AnnotationFinder::hasCrossProfileCallbackAnnotation) + .collect(Collectors.toList()); + + if (crossProfileCallbackParameters.size() > 1) { + isValid = false; + showError(MULTIPLE_ASYNC_CALLBACK_PARAMETERS_ERROR, crossProfileMethod); + } + + if (crossProfileCallbackParameters.size() == 1) { + if (!crossProfileMethod.getReturnType().getKind().equals(TypeKind.VOID)) { + isValid = false; + showError(NON_VOID_CALLBACK_ERROR, crossProfileMethod); + } + + isValid = + validateParameterTypesForCrossProfileCallbackInterface( + crossProfileType, crossProfileCallbackParameters.get(0)) + && isValid; + } + + if (!crossProfileCallbackParameters.isEmpty() + && !crossProfileMethod.getReturnType().getKind().equals(TypeKind.VOID)) { + isValid = false; + showError(NON_VOID_CALLBACK_ERROR, crossProfileMethod); + } + + return isValid; + } + + private boolean validateParameterTypeForCrossProfileCallbackInterface( + ValidatorCrossProfileTypeInfo crossProfileType, VariableElement parameter) { + TypeMirror parameterType = parameter.asType(); + + if (crossProfileType + .supportedTypes() + .isValidParameterType( + parameterType, TypeCheckContext.createForCrossProfileCallbackInterface())) { + return true; + } + + showError( + String.format(UNSUPPORTED_PARAMETER_TYPE_CROSS_ASYNC_CALLBACK, parameterType), + parameter, + validationMessageFormatterFor(crossProfileType.crossProfileMethods().get(0))); + return false; + } + + private boolean validateParameterTypesForCrossProfileCallbackInterface( + ValidatorCrossProfileTypeInfo crossProfileType, TypeElement crossProfileCallbackInterface) { + return crossProfileCallbackInterface.getEnclosedElements().stream() + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .map(m -> validateParameterTypesForCrossProfileCallbackInterface(crossProfileType, m)) + .allMatch(b -> b); + } + + private boolean validateParameterTypesForCrossProfileCallbackInterface( + ValidatorCrossProfileTypeInfo crossProfileType, ExecutableElement method) { + return method.getParameters().stream() + .allMatch(m -> validateParameterTypeForCrossProfileCallbackInterface(crossProfileType, m)); + } + + private boolean validateParameterTypeForCrossProfileMethod( + ValidatorCrossProfileTypeInfo crossProfileType, VariableElement parameter) { + TypeMirror parameterType = parameter.asType(); + + if (crossProfileType.supportedTypes().isValidParameterType(parameterType)) { + return true; + } + + showError( + String.format(UNSUPPORTED_PARAMETER_TYPE_CROSS_PROFILE_METHOD, parameterType), + parameter, + validationMessageFormatterFor(crossProfileType.crossProfileMethods().get(0))); + return false; + } + + private boolean validateCrossProfileCallbackInterfaces( + Collection<TypeElement> crossProfileCallbackInterfaces) { + return crossProfileCallbackInterfaces.stream() + .allMatch(this::validateCrossProfileCallbackInterface); + } + + private boolean validateCrossProfileCallbackInterface(TypeElement crossProfileCallbackInterface) { + boolean isValid = true; + + CrossProfileCallbackAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileCallbackAnnotationInfo( + crossProfileCallbackInterface, validatorContext.types(), validatorContext.elements()); + + PackageElement packageElement = + (PackageElement) crossProfileCallbackInterface.getEnclosingElement(); + if (packageElement.getQualifiedName().toString().isEmpty()) { + showError(CALLBACK_INTERFACE_DEFAULT_PACKAGE_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + if (crossProfileCallbackInterface.getKind() != ElementKind.INTERFACE) { + showError(NOT_INTERFACE_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + if (!crossProfileCallbackInterface.getTypeParameters().isEmpty()) { + showError(GENERIC_CALLBACK_INTERFACE_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + Collection<ExecutableElement> methods = getMethods(crossProfileCallbackInterface); + + if (methods.isEmpty()) { + showError(NO_METHODS_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + if (annotationInfo.simple() && methods.size() > 1) { + showError(NOT_ONE_METHOD_ERROR, crossProfileCallbackInterface); + isValid = false; + } + + isValid = + methods.stream() + .allMatch( + (method) -> + validateMethodOnCrossProfileCallbackInterface( + annotationInfo, method, crossProfileCallbackInterface)) + && isValid; + + return isValid; + } + + private boolean validateMethodOnCrossProfileCallbackInterface( + CrossProfileCallbackAnnotationInfo annotationInfo, + ExecutableElement method, + TypeElement crossProfileCallbackInterface) { + boolean isValid = true; + + if (method.isDefault()) { + showError( + DEFAULT_METHOD_ERROR, + method, + validationMessageFormatterFor(crossProfileCallbackInterface)); + isValid = false; + } + + if (method.getModifiers().contains(Modifier.STATIC)) { + showError( + STATIC_METHOD_ERROR, + method, + validationMessageFormatterFor(crossProfileCallbackInterface)); + isValid = false; + } + + if (!method.getReturnType().getKind().equals(TypeKind.VOID)) { + showError( + NOT_VOID_ERROR, method, validationMessageFormatterFor(crossProfileCallbackInterface)); + isValid = false; + } + + if (annotationInfo.simple() && method.getParameters().size() > 1) { + showError( + MORE_THAN_ONE_PARAMETER_ERROR, + method, + validationMessageFormatterFor(crossProfileCallbackInterface)); + isValid = false; + } + + return isValid; + } + + private boolean validateCrossProfileTests( + Collection<ValidatorCrossProfileTestInfo> crossProfileTests) { + return crossProfileTests.stream().allMatch(this::validateCrossProfileTest); + } + + private boolean validateCrossProfileTest(ValidatorCrossProfileTestInfo crossProfileTest) { + boolean isValid = true; + + if (!hasCrossProfileConfigurationAnnotation(crossProfileTest.configurationElement()) + && !hasCrossProfileConfigurationsAnnotation(crossProfileTest.configurationElement())) { + showError(NOT_A_CONFIGURATION_ERROR, crossProfileTest.crossProfileTestElement()); + isValid = false; + } + return isValid; + } + + private boolean validateCustomParcelableWrappers( + Collection<TypeElement> customParcelableWrappers) { + return customParcelableWrappers.stream().allMatch(this::validateCustomParcelableWrapper); + } + + private boolean validateCustomParcelableWrapper(TypeElement customParcelableWrapper) { + boolean isValid = true; + if (!validatorContext.types().isAssignable(customParcelableWrapper.asType(), parcelableType)) { + showError(NOT_PARCELABLE_ERROR, customParcelableWrapper); + isValid = false; + } + + ClassName parcelableWrapperRawType = + TypeUtils.getRawTypeClassName(customParcelableWrapper.asType()); + ClassName wrappedParamRawType = + TypeUtils.getRawTypeClassName( + ParcelableWrapperAnnotationInfo.extractFromParcelableWrapperAnnotation( + validatorContext.types(), + customParcelableWrapper.getAnnotation(CustomParcelableWrapper.class)) + .originalType() + .asType()); + + Optional<ExecutableElement> ofMethod = + customParcelableWrapper.getEnclosedElements().stream() + .filter(p -> p.getKind().equals(ElementKind.METHOD)) + .map(p -> (ExecutableElement) p) + .filter(p -> p.getSimpleName().contentEquals("of")) + // We drop generics as without being overly prescriptive it's impossible to know that + // the method is returning the correct generic type + .filter( + p -> + TypeUtils.getRawTypeClassName(p.getReturnType()) + .equals(TypeUtils.getRawTypeClassName(customParcelableWrapper.asType()))) + .filter(p -> ofMethodHasExpectedArguments(wrappedParamRawType, p)) + .findFirst(); + + if (!ofMethod.isPresent()) { + showError(INCORRECT_OF_METHOD, customParcelableWrapper); + isValid = false; + } + + Optional<ExecutableElement> getMethod = + customParcelableWrapper.getEnclosedElements().stream() + .filter(p -> p.getKind().equals(ElementKind.METHOD)) + .map(p -> (ExecutableElement) p) + .filter(p -> p.getSimpleName().contentEquals("get")) + // We drop generics as without being overly prescriptive it's impossible to know that + // the method is returning the correct generic type + .filter( + p -> TypeUtils.getRawTypeClassName(p.getReturnType()).equals(wrappedParamRawType)) + .findFirst(); + + if (!getMethod.isPresent()) { + showError(INCORRECT_GET_METHOD, customParcelableWrapper); + isValid = false; + } + + TypeName creatorType = + ParameterizedTypeName.get(PARCELABLE_CREATOR_CLASSNAME, parcelableWrapperRawType); + + Optional<VariableElement> creator = + customParcelableWrapper.getEnclosedElements().stream() + .filter(p -> p.getKind().equals(ElementKind.FIELD)) + .map(p -> (VariableElement) p) + .filter(p -> p.getSimpleName().contentEquals("CREATOR")) + .filter( + p -> + p.getModifiers() + .containsAll( + Arrays.asList(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC))) + .filter(p -> ClassName.get(p.asType()).equals(creatorType)) + .findFirst(); + + if (!creator.isPresent()) { + showError(INCORRECT_PARCELABLE_IMPLEMENTATION, customParcelableWrapper); + isValid = false; + } + + return isValid; + } + + private boolean ofMethodHasExpectedArguments( + ClassName wrappedParamRawType, ExecutableElement ofMethod) { + List<? extends VariableElement> parameters = ofMethod.getParameters(); + if (parameters.size() != 3) { + return false; + } + + if (!validatorContext.types().isSameType(parameters.get(0).asType(), bundlerType)) { + return false; + } + + if (!validatorContext.types().isSameType(parameters.get(1).asType(), bundlerTypeType)) { + return false; + } + + if (!TypeUtils.getRawTypeClassName(parameters.get(2).asType()).equals(wrappedParamRawType)) { + return false; + } + + return true; + } + + private boolean validateCustomFutureWrappers(Collection<TypeElement> futureWrappers) { + return futureWrappers.stream().map(this::validateCustomFutureWrapper).allMatch(b -> b); + } + + private boolean validateCustomFutureWrapper(TypeElement futureWrapper) { + boolean isValid = true; + + ClassName wrappedFutureRawType = + TypeUtils.getRawTypeClassName( + FutureWrapperAnnotationInfo.extractFromFutureWrapperAnnotation( + validatorContext.types(), + futureWrapper.getAnnotation(CustomFutureWrapper.class)) + .originalType() + .asType()); + + if (!TypeUtils.getRawTypeQualifiedName(futureWrapper.getSuperclass()) + .equals("com.google.android.enterprise.connectedapps.FutureWrapper")) { + showError(DOES_NOT_EXTEND_FUTURE_WRAPPER_ERROR, futureWrapper); + isValid = false; + } + + if (futureWrapper.getTypeParameters().size() != 1) { + showError(MUST_HAVE_ONE_TYPE_PARAMETER_ERROR, futureWrapper); + isValid = false; + } + + Optional<ExecutableElement> createMethod = + futureWrapper.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getSimpleName().contentEquals("create")) + .filter( + e -> e.getModifiers().containsAll(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC))) + // We drop generics as without being overly prescriptive it's impossible to know that + // the method is returning the correct generic type + .filter( + e -> + TypeUtils.getRawTypeClassName(e.getReturnType()) + .equals(TypeUtils.getRawTypeClassName(futureWrapper.asType()))) + .filter(this::createMethodHasExpectedArguments) + .findFirst(); + + if (!createMethod.isPresent()) { + showError(INCORRECT_CREATE_METHOD_ERROR, futureWrapper); + isValid = false; + } + + Optional<ExecutableElement> getFutureMethod = + futureWrapper.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getSimpleName().contentEquals("getFuture")) + .filter(e -> e.getModifiers().contains(Modifier.PUBLIC)) + .filter(e -> !e.getModifiers().contains(Modifier.STATIC)) + // We drop generics as without being overly prescriptive it's impossible to know that + // the method is returning the correct generic type + .filter( + e -> TypeUtils.getRawTypeClassName(e.getReturnType()).equals(wrappedFutureRawType)) + .filter(e -> e.getParameters().isEmpty()) + .findFirst(); + + if (!getFutureMethod.isPresent()) { + showError(INCORRECT_GET_FUTURE_METHOD_ERROR, futureWrapper); + isValid = false; + } + + Optional<ExecutableElement> writeFutureResultMethod = + futureWrapper.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getSimpleName().contentEquals("writeFutureResult")) + .filter( + e -> e.getModifiers().containsAll(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC))) + .filter(e -> e.getReturnType().toString().equals("void")) + .filter(e -> writeFutureResultMethodHasExpectedArguments(e, wrappedFutureRawType)) + .findFirst(); + + if (!writeFutureResultMethod.isPresent()) { + showError(INCORRECT_RESOLVE_CALLBACK_WHEN_FUTURE_IS_SET_METHOD_ERROR, futureWrapper); + isValid = false; + } + + Optional<ExecutableElement> groupResultsMethod = + futureWrapper.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getSimpleName().contentEquals("groupResults")) + .filter( + e -> e.getModifiers().containsAll(Arrays.asList(Modifier.PUBLIC, Modifier.STATIC))) + .filter(e -> groupResultsMethodHasExpectedReturnType(e, wrappedFutureRawType)) + .filter(e -> groupResultsMethodHasExpectedArguments(e, wrappedFutureRawType)) + .findFirst(); + + if (!groupResultsMethod.isPresent()) { + showError(INCORRECT_GROUP_RESULTS_METHOD_ERROR, futureWrapper); + isValid = false; + } + + return isValid; + } + + private boolean groupResultsMethodHasExpectedReturnType( + ExecutableElement groupResultsMethod, ClassName wrappedFutureRawType) { + + if (!TypeUtils.getRawTypeClassName(groupResultsMethod.getReturnType()) + .equals(wrappedFutureRawType)) { + return false; + } + + TypeMirror wrappedReturnType = + TypeUtils.extractTypeArguments(groupResultsMethod.getReturnType()).get(0); + + if (!TypeUtils.getRawTypeClassName(wrappedReturnType).equals(ClassName.get(Map.class))) { + return false; + } + + TypeMirror wrappedReturnTypeKey = TypeUtils.extractTypeArguments(wrappedReturnType).get(0); + + if (!validatorContext.types().isSameType(wrappedReturnTypeKey, profileType)) { + return false; + } + + return true; + } + + private boolean groupResultsMethodHasExpectedArguments( + ExecutableElement groupResultsMethod, ClassName wrappedFutureRawType) { + if (groupResultsMethod.getParameters().size() != 1) { + return false; + } + + TypeMirror param = groupResultsMethod.getParameters().get(0).asType(); + + if (!TypeUtils.getRawTypeClassName(param).equals(ClassName.get(Map.class))) { + return false; + } + + List<TypeMirror> params = TypeUtils.extractTypeArguments(param); + + TypeMirror keyParam = params.get(0); + TypeMirror valueParam = params.get(1); + + if (!validatorContext.types().isSameType(keyParam, profileType)) { + return false; + } + + if (!TypeUtils.getRawTypeClassName(valueParam).equals(wrappedFutureRawType)) { + return false; + } + + return true; + } + + private boolean createMethodHasExpectedArguments(ExecutableElement createMethod) { + if (createMethod.getParameters().size() != 2) { + return false; + } + + if (!validatorContext + .types() + .isSameType(createMethod.getParameters().get(0).asType(), bundlerType)) { + return false; + } + + if (!validatorContext + .types() + .isSameType(createMethod.getParameters().get(1).asType(), bundlerTypeType)) { + return false; + } + + return true; + } + + private boolean writeFutureResultMethodHasExpectedArguments( + ExecutableElement method, ClassName wrappedFutureRawType) { + if (method.getParameters().size() != 2) { + return false; + } + + if (!TypeUtils.getRawTypeClassName(method.getParameters().get(0).asType()) + .equals(wrappedFutureRawType)) { + return false; + } + + if (!validatorContext + .types() + .isAssignable( + TypeUtils.removeTypeArguments(method.getParameters().get(1).asType()), + futureResultWriterType)) { + return false; + } + + return true; + } + + private Collection<ExecutableElement> getMethods(TypeElement typeElement) { + return typeElement.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .collect(toSet()); + } + + private void showError( + String errorText, + Element errorElement, + ValidationMessageFormatter validationMessageFormatter) { + showErrorPreformatted(validationMessageFormatter.format(errorText), errorElement); + } + + private void showError(String errorText, Element errorElement) { + showErrorPreformatted( + validationMessageFormatterFor(errorElement).format(errorText), errorElement); + } + + private void showErrorPreformatted(String errorText, Element errorElement) { + validatorContext + .processingEnv() + .getMessager() + .printMessage(Kind.ERROR, errorText, errorElement); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java new file mode 100644 index 0000000..0abba95 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeCrossProfileTypeGenerator.java @@ -0,0 +1,477 @@ +/* + * 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.PROFILE_AWARE_UTILS_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CurrentProfileGenerator.getCurrentProfileClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getMultipleSenderInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.InterfaceGenerator.getSingleSenderInterfaceClassName; +import static com.google.android.enterprise.connectedapps.processor.MultipleProfilesGenerator.getMultipleProfilesClassName; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.HashMap; +import java.util.Map; +import javax.lang.model.element.Modifier; + +class FakeCrossProfileTypeGenerator { + private boolean generated = false; + private final CrossProfileTypeInfo crossProfileType; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + + public FakeCrossProfileTypeGenerator( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "FakeCrossProfileTypeGenerator#generate can only be called once"); + } + generated = true; + + generateFakeCrossProfileType(); + } + + private void generateFakeCrossProfileType() { + ClassName className = getFakeCrossProfileTypeClassName(generatorContext, crossProfileType); + ClassName builderClassName = + getFakeCrossProfileTypeBuilderClassName(generatorContext, crossProfileType); + ClassName crossProfileTypeInterfaceClassName = + InterfaceGenerator.getCrossProfileTypeInterfaceClassName( + 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 should be injected into your code under test and the {@link $T}\n" + + "used to control the fake state. Calls will be routed to the correct {@link" + + " $T}.\n", + crossProfileTypeInterfaceClassName, + fakeProfileConnectorClassName, + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(crossProfileTypeInterfaceClassName); + + classBuilder.addField( + FieldSpec.builder(fakeProfileConnectorClassName, "connector") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + addConstructor(classBuilder); + + classBuilder.addMethod( + MethodSpec.methodBuilder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(builderClassName) + .addStatement("return new $T()", builderClassName) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("current") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if (connector.utils().runningOnPersonal())") + .addStatement( + "return ($T) personal()", + InterfaceGenerator.getSingleSenderInterfaceClassName( + generatorContext, crossProfileType)) + .nextControlFlow("else") + .addStatement( + "return ($T) work()", + InterfaceGenerator.getSingleSenderInterfaceClassName( + generatorContext, crossProfileType)) + .endControlFlow() + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("other") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if (connector.utils().runningOnPersonal())") + .addStatement("return work()") + .nextControlFlow("else") + .addStatement("return personal()") + .endControlFlow() + .build()); + + addPersonalMethod(classBuilder); + addWorkMethod(classBuilder); + + classBuilder.addMethod( + MethodSpec.methodBuilder("profile") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(PROFILE_CLASSNAME, "profile") + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow("if (profile.isCurrent())") + .addStatement( + "return ($T) current()", + getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .nextControlFlow("else") + .addComment("must be other profile") + .addStatement("return other()") + .endControlFlow() + .build()); + + ParameterizedTypeName senderMapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), + PROFILE_CLASSNAME, + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + + classBuilder.addMethod( + MethodSpec.methodBuilder("profiles") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(ArrayTypeName.of(PROFILE_CLASSNAME), "profiles") + .varargs(true) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$T senders = new $T<>()", senderMapType, HashMap.class) + .beginControlFlow("for ($1T profileIdentifier : profiles)", PROFILE_CLASSNAME) + .addStatement("senders.put(profileIdentifier, profile(profileIdentifier))") + .endControlFlow() + .addStatement( + "return new $1T(senders)", + getMultipleProfilesClassName(generatorContext, crossProfileType)) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("both") + .addAnnotation(Override.class) + .addJavadoc("Run a method on both the personal and work profile, if accessible.") + .addModifiers(Modifier.PUBLIC) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME) + .addStatement( + "$1T currentProfileIdentifier = utils.getCurrentProfile()", PROFILE_CLASSNAME) + .addStatement("$1T otherProfileIdentifier = utils.getOtherProfile()", PROFILE_CLASSNAME) + .addStatement("return profiles(currentProfileIdentifier, otherProfileIdentifier)") + .build()); + + if (!crossProfileType.profileConnector().isPresent() + || crossProfileType.profileConnector().get().primaryProfile() != ProfileType.NONE) { + generatePrimarySecondaryMethods(classBuilder); + } + + generateFakeCrossProfileTypeBuilder(classBuilder); + + 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; + + if (crossProfileType.isStatic()) { + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(fakeProfileConnectorClassName, "connector") + .addStatement("this.connector = connector") + .build()); + } else { + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "personal") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "work") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(crossProfileType.className(), "personal") + .addParameter(crossProfileType.className(), "work") + .addParameter(fakeProfileConnectorClassName, "connector") + .addStatement("this.personal = personal") + .addStatement("this.work = work") + .addStatement("this.connector = connector") + .build()); + } + } + + private void addPersonalMethod(TypeSpec.Builder classBuilder) { + ClassName currentProfileClassName = + getCurrentProfileClassName(generatorContext, crossProfileType); + CodeBlock currentPersonalFakeConstructor = + crossProfileType.isStatic() + ? CodeBlock.of("new $T(connector.applicationContext())", currentProfileClassName) + : CodeBlock.of( + "new $T(connector.applicationContext(), personal)", currentProfileClassName); + ClassName fakeOtherClassName = + FakeOtherGenerator.getFakeOtherClassName(generatorContext, crossProfileType); + CodeBlock otherPersonalFakeConstructor = + crossProfileType.isStatic() + ? CodeBlock.of("new $T(connector)", fakeOtherClassName) + : CodeBlock.of("new $T(connector, personal)", fakeOtherClassName); + + classBuilder.addMethod( + MethodSpec.methodBuilder("personal") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow( + "if (connector.runningOnProfile() == $T.ProfileType.PERSONAL)", + CustomProfileConnector.class) + .addStatement("return $L", currentPersonalFakeConstructor) + .nextControlFlow("else") + .addStatement("return $L", otherPersonalFakeConstructor) + .endControlFlow() + .build()); + } + + private void addWorkMethod(TypeSpec.Builder classBuilder) { + ClassName currentProfileClassName = + getCurrentProfileClassName(generatorContext, crossProfileType); + CodeBlock currentWorkFakeConstructor = + crossProfileType.isStatic() + ? CodeBlock.of("new $T(connector.applicationContext())", currentProfileClassName) + : CodeBlock.of("new $T(connector.applicationContext(), work)", currentProfileClassName); + ClassName fakeOtherClassName = + FakeOtherGenerator.getFakeOtherClassName(generatorContext, crossProfileType); + CodeBlock otherWorkFakeConstructor = + crossProfileType.isStatic() + ? CodeBlock.of("new $T(connector)", fakeOtherClassName) + : CodeBlock.of("new $T(connector, work)", fakeOtherClassName); + + classBuilder.addMethod( + MethodSpec.methodBuilder("work") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .beginControlFlow( + "if (connector.runningOnProfile() == $T.ProfileType.WORK)", + CustomProfileConnector.class) + .addStatement("return $L", currentWorkFakeConstructor) + .nextControlFlow("else") + .addStatement("return $L", otherWorkFakeConstructor) + .endControlFlow() + .build()); + } + + private void generateFakeCrossProfileTypeBuilder(TypeSpec.Builder fakeCrossProfileType) { + ClassName fakeCrossProfileTypeClassName = + getFakeCrossProfileTypeClassName(generatorContext, crossProfileType); + ClassName builderClassName = + getFakeCrossProfileTypeBuilderClassName(generatorContext, crossProfileType); + ClassName fakeProfileConnectorClassName = + crossProfileType.profileConnector().isPresent() + ? FakeProfileConnectorGenerator.getFakeProfileConnectorClassName( + crossProfileType.profileConnector().get()) + : ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME; + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(builderClassName) + .addJavadoc("Builder for {@link $T}.\n", fakeCrossProfileTypeClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); + + if (crossProfileType.isStatic()) { + setupStaticBuilder(fakeCrossProfileTypeClassName, classBuilder); + } else { + setupNonStaticBuilder(builderClassName, fakeCrossProfileTypeClassName, classBuilder); + } + + classBuilder.addField( + FieldSpec.builder(fakeProfileConnectorClassName, "connector") + .addModifiers(Modifier.PRIVATE) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("connector") + .addJavadoc( + "Set the {@link $T} to be used to manage the state of this fake.\n", + fakeProfileConnectorClassName) + .addModifiers(Modifier.PUBLIC) + .returns(builderClassName) + .addParameter(fakeProfileConnectorClassName, "connector") + .addStatement("this.connector = connector") + .addStatement("return this") + .build()); + + fakeCrossProfileType.addType(classBuilder.build()); + } + + private static void setupStaticBuilder( + ClassName fakeCrossProfileTypeClassName, TypeSpec.Builder classBuilder) { + classBuilder.addMethod( + MethodSpec.methodBuilder("build") + .addJavadoc("Build the {@link $T}.\n", fakeCrossProfileTypeClassName) + .addModifiers(Modifier.PUBLIC) + .returns(fakeCrossProfileTypeClassName) + .beginControlFlow("if (connector == null)") + .addStatement( + "throw new $T($S)", + IllegalStateException.class, + "All arguments must be set to build fake") + .endControlFlow() + .addStatement("return new $1T(connector)", fakeCrossProfileTypeClassName) + .build()); + } + + private void setupNonStaticBuilder( + ClassName builderClassName, + ClassName fakeCrossProfileTypeClassName, + TypeSpec.Builder classBuilder) { + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "personal") + .addModifiers(Modifier.PRIVATE) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("personal") + .addJavadoc( + "Set the {@link $T} to be used when a call needs to be made to the personal" + + " profile.\n", + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC) + .returns(builderClassName) + .addParameter(crossProfileType.className(), "personal") + .addStatement("this.personal = personal") + .addStatement("return this") + .build()); + + classBuilder.addField( + FieldSpec.builder(crossProfileType.className(), "work") + .addModifiers(Modifier.PRIVATE) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("work") + .addJavadoc( + "Set the {@link $T} to be used when a call needs to be made to the work profile.\n", + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC) + .returns(builderClassName) + .addParameter(crossProfileType.className(), "work") + .addStatement("this.work = work") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("build") + .addJavadoc("Build the {@link $T}.\n", fakeCrossProfileTypeClassName) + .addModifiers(Modifier.PUBLIC) + .returns(fakeCrossProfileTypeClassName) + .beginControlFlow("if (personal == null || work == null || connector == null)") + .addStatement( + "throw new $T($S)", + IllegalStateException.class, + "All arguments must be set to build fake") + .endControlFlow() + .addStatement( + "return new $1T(personal, work, connector)", fakeCrossProfileTypeClassName) + .build()); + } + + private void generatePrimarySecondaryMethods(TypeSpec.Builder classBuilder) { + generatePrimaryMethod(classBuilder); + generateSecondaryMethod(classBuilder); + generateSuppliersMethod(classBuilder); + } + + private void generatePrimaryMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("primary") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("return profile(connector.utils().getPrimaryProfile())"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSecondaryMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("secondary") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("return profile(connector.utils().getSecondaryProfile())"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateSuppliersMethod(TypeSpec.Builder classBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("suppliers") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .addStatement("$1T utils = connector.utils()", PROFILE_AWARE_UTILS_CLASSNAME) + .addStatement( + "$1T currentProfileIdentifier = utils.getCurrentProfile()", PROFILE_CLASSNAME) + .addStatement( + "$1T secondaryProfileIdentifier = utils.getSecondaryProfile()", PROFILE_CLASSNAME) + .addStatement("return profiles(currentProfileIdentifier, secondaryProfileIdentifier)"); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getFakeCrossProfileTypeClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + ClassName crossProfileTypeClassName = + InterfaceGenerator.getCrossProfileTypeInterfaceClassName( + generatorContext, crossProfileType); + return ClassName.get( + crossProfileTypeClassName.packageName(), "Fake" + crossProfileTypeClassName.simpleName()); + } + + static ClassName getFakeCrossProfileTypeBuilderClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + ClassName crossProfileTypeClassName = + InterfaceGenerator.getCrossProfileTypeInterfaceClassName( + generatorContext, crossProfileType); + return ClassName.get( + crossProfileTypeClassName.packageName() + + "." + + "Fake" + + crossProfileTypeClassName.simpleName(), + "Builder"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java new file mode 100644 index 0000000..20bf0fb --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeOtherGenerator.java @@ -0,0 +1,344 @@ +/* + * 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.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.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.endControlFlow(); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getFakeOtherClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_FakeOther"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java new file mode 100644 index 0000000..5ff51ea --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FakeProfileConnectorGenerator.java @@ -0,0 +1,100 @@ +/* + * 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.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; + +class FakeProfileConnectorGenerator { + private boolean generated = false; + private final ProfileConnectorInfo connector; + private final GeneratorUtilities generatorUtilities; + + public FakeProfileConnectorGenerator( + GeneratorContext generatorContext, ProfileConnectorInfo connector) { + this.generatorUtilities = new GeneratorUtilities(checkNotNull(generatorContext)); + this.connector = checkNotNull(connector); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "FakeProfileConectorGenerator#generate can only be called once"); + } + generated = true; + + generateFakeProfileConnector(); + } + + private void generateFakeProfileConnector() { + ClassName className = getFakeProfileConnectorClassName(connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Fake Profile Connector for {@link $1T}.\n\n" + + "<p>All functionality is implemented by {@link $2T}, this class is just used" + + " for compatibility with the {@link $1T} interface.\n", + connector.connectorClassName(), + ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(connector.connectorClassName()) + .superclass(ABSTRACT_FAKE_PROFILE_CONNECTOR_CLASSNAME); + + if (connector.primaryProfile().equals(ProfileType.UNKNOWN)) { + // Special case - we need to provide the profile type to the fake. + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addParameter(CONTEXT_CLASSNAME, "context") + .addModifiers(Modifier.PUBLIC) + .addStatement("super(context, $T.NONE)", ProfileType.class) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(ProfileType.class, "primaryProfile") + .addStatement("super(context, primaryProfile)") + .build()); + } else { + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement( + "super(context, $T.$L)", ProfileType.class, connector.primaryProfile().name()) + .build()); + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + static ClassName getFakeProfileConnectorClassName(ProfileConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName(), + "Fake" + connector.connectorClassName().simpleName()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java new file mode 100644 index 0000000..78902c4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/FutureWrappersGenerator.java @@ -0,0 +1,126 @@ +/* + * 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.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; + +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.FutureWrapper.WrapperType; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.Type; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Optional; +import javax.tools.JavaFileObject; + +/** + * Generate the wrapper classes for every used future type. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <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 FutureWrappersGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + + FutureWrappersGenerator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + } + + void generate() { + if (generated) { + throw new IllegalStateException("FutureWrappersGenerator#generate can only be called once"); + } + generated = true; + + generateFutureWrappers(); + } + + private void generateFutureWrappers() { + Collection<FutureWrapper> futureWrappersToGenerate = + generatorContext.crossProfileTypes().stream() + .map(CrossProfileTypeInfo::supportedTypes) + .flatMap(s -> s.usableTypes().stream()) + .filter(s -> s.getFutureWrapper().isPresent()) + .map(Type::getFutureWrapper) + .map(Optional::get) + .filter(w -> w.wrapperType().equals(WrapperType.DEFAULT)) + .collect(toSet()); + + for (FutureWrapper futureWrapper : futureWrappersToGenerate) { + generateFutureWrapper(futureWrapper); + } + } + + private void generateFutureWrapper(FutureWrapper futureWrapper) { + String futureWrapperSimpleName = futureWrapper.defaultWrapperClassName().simpleName(); + + String contents; + InputStream in = + ParcelableWrappersGenerator.class.getResourceAsStream( + "/futurewrappers/" + futureWrapperSimpleName + ".java"); + + try (BufferedReader br = + new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) { + contents = br.lines().collect(joining(System.lineSeparator())); + } catch (IOException e) { + throw new IllegalStateException( + "Could not read futurewrapper file for " + futureWrapperSimpleName, e); + } + + contents = + contents.replace( + futureWrapper.defaultWrapperClassName().packageName(), + futureWrapper.wrapperClassName().packageName()); + contents = + contents.replace( + futureWrapper.defaultWrapperClassName().simpleName(), + futureWrapper.wrapperClassName().simpleName()); + + JavaFileObject builderFile; + try { + builderFile = + generatorContext + .processingEnv() + .getFiler() + .createSourceFile( + futureWrapper.wrapperClassName().packageName() + + "." + + futureWrapper.wrapperClassName().simpleName()); + } catch (IOException e) { + throw new IllegalStateException( + "Could not write futurewrapper for " + futureWrapperSimpleName, e); + } + + try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { + out.write(contents); + } catch (IOException e) { + throw new IllegalStateException( + "Could not write futurewrapper for " + futureWrapperSimpleName, e); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java new file mode 100644 index 0000000..7d4dbd7 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/GeneratorUtilities.java @@ -0,0 +1,299 @@ +/* + * 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.PARCELABLE_CREATOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +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 static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.containers.Context; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.common.collect.Iterables; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.Set; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.MirroredTypeException; +import javax.lang.model.type.MirroredTypesException; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Types; +import javax.tools.JavaFileObject; + +/** Utility methods used for code generation. */ +public final class GeneratorUtilities { + + private final Context context; + + public GeneratorUtilities(Context context) { + this.context = checkNotNull(context); + } + + /** + * Extract a class provided in an annotation. + * + * <p>The {@code runnable} should call the annotation method that the class is being extracted + * for. + */ + public static TypeElement extractClassFromAnnotation(Types types, Runnable runnable) { + // From https://docs.oracle.com/javase/8/docs/api/javax/lang/model/AnnotatedConstruct.html + // "The annotation returned by this method could contain an element whose value is of type + // Class. This value cannot be returned directly: information necessary to locate and load a + // class (such as the class loader to use) is not available, and the class might not be loadable + // at all. Attempting to read a Class object by invoking the relevant method on the returned + // annotation will result in a MirroredTypeException, from which the corresponding TypeMirror + // may be extracted." + try { + runnable.run(); + } catch (MirroredTypeException e) { + return e.getTypeMirrors().stream() + .map(t -> (TypeElement) types.asElement(t)) + .findFirst() + .get(); + } + throw new AssertionError("Could not extract class from annotation"); + } + + /** + * Extract classes provided in an annotation. + * + * <p>The {@code runnable} should call the annotation method that the classes are being extracted + * for. + */ + public static List<TypeElement> extractClassesFromAnnotation(Types types, Runnable runnable) { + // From https://docs.oracle.com/javase/8/docs/api/javax/lang/model/AnnotatedConstruct.html + // "The annotation returned by this method could contain an element whose value is of type + // Class. This value cannot be returned directly: information necessary to locate and load a + // class (such as the class loader to use) is not available, and the class might not be loadable + // at all. Attempting to read a Class object by invoking the relevant method on the returned + // annotation will result in a MirroredTypeException, from which the corresponding TypeMirror + // may be extracted." + try { + runnable.run(); + } catch (MirroredTypesException e) { + return e.getTypeMirrors().stream() + .map(t -> (TypeElement) types.asElement(t)) + .collect(toList()); + } + throw new AssertionError("Could not extract classes from annotation"); + } + + public static Set<ExecutableElement> findCrossProfileMethodsInClass(TypeElement clazz) { + return clazz.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind() == ElementKind.METHOD) + .filter(AnnotationFinder::hasCrossProfileAnnotation) + .collect(toSet()); + } + + public static Set<ExecutableElement> findCrossProfileProviderMethodsInClass(TypeElement clazz) { + return clazz.getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind() == ElementKind.METHOD) + .filter(AnnotationFinder::hasCrossProfileProviderAnnotation) + .collect(toSet()); + } + + /** Generate a {@code @link} reference to a given method. */ + public static CodeBlock methodJavadocReference(ExecutableElement method) { + CodeBlock.Builder methodCall = CodeBlock.builder(); + methodCall.add("{@link $T#", method.getEnclosingElement()); + methodCall.add("$L(", method.getSimpleName()); + + if (!method.getParameters().isEmpty()) { + methodCall.add("$T", method.getParameters().iterator().next().asType()); + + for (VariableElement param : + method.getParameters().subList(1, method.getParameters().size())) { + methodCall.add(",$T", param.asType()); + } + } + + methodCall.add(")}"); + return methodCall.build(); + } + + public void writeClassToFile(String packageName, TypeSpec.Builder clazzBuilder) { + writeClassToFile(packageName, clazzBuilder.build()); + } + + void writeClassToFile(String packageName, TypeSpec clazz) { + final String qualifiedClassName = + packageName.isEmpty() ? clazz.name : packageName + "." + clazz.name; + + JavaFile javaFile = JavaFile.builder(packageName, clazz).build(); + try { + JavaFileObject builderFile = + context.processingEnv().getFiler().createSourceFile(qualifiedClassName); + try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { + javaFile.writeTo(out); + } + } catch (IOException e) { + throw new IllegalStateException("Error writing " + qualifiedClassName + " to file", e); + } + } + + /** + * Take the parameters of an {@link ExecutableElement} and return {@link ParameterSpec} instances + * ready to be used with a generated method. + */ + static List<ParameterSpec> extractParametersFromMethod( + SupportedTypes supportedTypes, + ExecutableElement method, + AutomaticallyResolvedParameterFilterBehaviour filterBehaviour) { + if (filterBehaviour == LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return extractParametersFromMethod(method); + } else if (filterBehaviour == REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return method.getParameters().stream() + .filter(param -> !supportedTypes.isAutomaticallyResolved(param.asType())) + .map(GeneratorUtilities::convertVariableToParameterSpec) + .collect(toList()); + } else if (filterBehaviour == REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + throw new IllegalArgumentException("Can not replace parameters when extracting"); + } + throw new IllegalArgumentException("Unknown filterBehaviour " + filterBehaviour); + } + + /** + * Take the parameters of an {@link ExecutableElement} and return {@link ParameterSpec} instances + * ready to be used with a generated method. + * + * <p>This will not filter automatically resolved parameters. For that functionality use {@link + * #extractParametersFromMethod(SupportedTypes, ExecutableElement, + * AutomaticallyResolvedParameterFilterBehaviour)}. + */ + static List<ParameterSpec> extractParametersFromMethod(ExecutableElement method) { + return method.getParameters().stream() + .map(GeneratorUtilities::convertVariableToParameterSpec) + .collect(toList()); + } + + private static ParameterSpec convertVariableToParameterSpec(VariableElement variable) { + ParameterSpec.Builder builder = + ParameterSpec.builder( + ClassName.get(variable.asType()), variable.getSimpleName().toString()); + builder.addModifiers(variable.getModifiers()); + return builder.build(); + } + + /** If type is primitive, return the boxed version of that type, otherwise return the type. */ + TypeMirror boxIfNecessary(TypeMirror type) { + if (!type.getKind().isPrimitive()) { + return type; + } + + PrimitiveType primitiveType = (PrimitiveType) type; + return context.types().boxedClass(primitiveType).asType(); + } + + void addDefaultParcelableMethods(TypeSpec.Builder classBuilder, ClassName className) { + classBuilder.addMethod( + MethodSpec.methodBuilder("describeContents") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(int.class) + .addStatement("return 0") + .build()); + + TypeName creatorType = ParameterizedTypeName.get(PARCELABLE_CREATOR_CLASSNAME, className); + + TypeSpec creator = + TypeSpec.anonymousClassBuilder("") + .addSuperinterface(creatorType) + .addMethod( + MethodSpec.methodBuilder("createFromParcel") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(className) + .addParameter(PARCEL_CLASSNAME, "in") + .addStatement("return new $T(in)", className) + .build()) + .addMethod( + MethodSpec.methodBuilder("newArray") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(ArrayTypeName.of(className)) + .addParameter(int.class, "size") + .addStatement("return new $T[size]", className) + .build()) + .build(); + + classBuilder.addField( + FieldSpec.builder(creatorType, "CREATOR") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "rawtypes") + .build()) + .initializer("$L", creator) + .build()); + } + + /** Generate a reference to a cross-profile method which can be used in javadoc. */ + public static CodeBlock generateMethodReference( + CrossProfileTypeInfo crossProfileType, CrossProfileMethodInfo method) { + CodeBlock.Builder reference = CodeBlock.builder(); + + reference.add("$T#$L(", crossProfileType.className(), method.simpleName()); + + List<TypeMirror> parameterTypes = convertParametersToTypes(method); + + if (!parameterTypes.isEmpty()) { + for (int i = 0; i < parameterTypes.size() - 1; i++) { + reference.add("$T, ", TypeUtils.getRawTypeClassName(parameterTypes.get(i))); + } + reference.add("$T", TypeUtils.getRawTypeClassName(Iterables.getLast(parameterTypes))); + } + + reference.add(")"); + return reference.build(); + } + + private static List<TypeMirror> convertParametersToTypes(CrossProfileMethodInfo method) { + return method.methodElement().getParameters().stream().map(Element::asType).collect(toList()); + } + + static ClassName appendToClassName(ClassName originalClassName, String suffix) { + return ClassName.get(originalClassName.packageName(), originalClassName.simpleName() + suffix); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java new file mode 100644 index 0000000..68d84f4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/IfAvailableGenerator.java @@ -0,0 +1,232 @@ +/* + * 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.IF_AVAILABLE_FUTURE_RESULT_WRITER; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.generateMethodReference; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +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.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_IfAvailable} 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 IfAvailableGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + IfAvailableGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("IfAvailableGenerator#generate can only be called once"); + } + generated = true; + + generateIfAvailableClass(); + } + + private void generateIfAvailableClass() { + ClassName className = getIfAvailableClassName(generatorContext, crossProfileType); + + ClassName singleSenderCanThrowInterface = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Wrapper of {@link $T} which will replace\n{@link $T} with default values.\n", + singleSenderCanThrowInterface, + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + classBuilder.addField( + FieldSpec.builder(singleSenderCanThrowInterface, "singleSenderCanThrow") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(singleSenderCanThrowInterface, "singleSenderCanThrow") + .addStatement("this.singleSenderCanThrow = singleSenderCanThrow") + .build()); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + generateMethodOnIfAvailableClass(classBuilder, method, crossProfileType); + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateMethodOnIfAvailableClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC) + .addExceptions(method.thrownExceptions()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addJavadoc("Call {@link $L}.", generateMethodReference(crossProfileType, method)); + + if (method.isBlocking(generatorContext, crossProfileType)) { + if (method.returnType().getKind().equals(TypeKind.VOID)) { + methodBuilder + .addJavadoc( + "\n\n<p>{@link $T} will be ignored.\n", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .beginControlFlow("try") + .addStatement( + "singleSenderCanThrow.$L($L)", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addComment("Ignore exception") + .endControlFlow(); + } else { + methodBuilder.addParameter(method.returnTypeTypeName(), "defaultValue"); + methodBuilder + .addJavadoc( + "\n\n<p>In case of {@link $T}, {@code defaultValue} will be returned.\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .returns(method.returnTypeTypeName()) + .beginControlFlow("try") + .addStatement( + "return singleSenderCanThrow.$L($L)", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addStatement("return defaultValue") + .endControlFlow(); + } + } else if (method.isCrossProfileCallback(generatorContext)) { + if (!method.isSimpleCrossProfileCallback(generatorContext)) { + // Non-simple callbacks can't be used with multiple profiles + return; + } + + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create( + (TypeElement) + generatorContext + .types() + .asElement( + method.getCrossProfileCallbackParam(generatorContext).get().asType())); + if (callbackInterface.argumentTypes().isEmpty()) { + // Void + // This assumes a single callback method + methodBuilder + .addJavadoc( + "\n\n<p>If the profile is not available, the callback will be called anyway.\n") + .addStatement( + "singleSenderCanThrow.$1L(\n $2L,\n (e) -> {$3L.$4L();})", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + method.getCrossProfileCallbackParam(generatorContext).get().getSimpleName(), + callbackInterface.methods().get(0).getSimpleName()); + } else { + // This assumes a single callback method + methodBuilder.addParameter( + ClassName.get(callbackInterface.argumentTypes().iterator().next()), "defaultValue"); + methodBuilder + .addJavadoc( + "\n\n" + + "<p>If the profile is not available, the callback will be called with the" + + " {@code defaultValue}.\n") + .addStatement( + "singleSenderCanThrow.$1L(\n $2L,\n (e) -> {$3L.$4L(defaultValue);})", + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + method.getCrossProfileCallbackParam(generatorContext).get().getSimpleName(), + callbackInterface.methods().get(0).getSimpleName()); + } + } else if (method.isFuture(crossProfileType)) { + // This assumes a Future is generic on a single type + TypeMirror wrappedReturnType = TypeUtils.extractTypeArguments(method.returnType()).get(0); + TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType()); + FutureWrapper futureWrapper = + crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get(); + + methodBuilder + .addParameter(ClassName.get(wrappedReturnType), "defaultValue") + .returns(ClassName.get(method.returnType())); + methodBuilder + .addJavadoc( + "\n\n" + + "<p>If the profile is not available, the future will be resolved with the" + + " {@code defaultValue}.\n") + .addStatement( + "$1T internalCrossProfileClass = $1T.instance()", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)) + .addStatement( + "$1T<$2T> futureWrapper = $1T.create(internalCrossProfileClass.bundler(), $3L)", + futureWrapper.wrapperClassName(), + wrappedReturnType, + TypeUtils.generateBundlerType(wrappedReturnType)) + .addStatement( + "$T.writeFutureResult(singleSenderCanThrow.$L($L), new" + + " $T<$T>(futureWrapper, defaultValue))", + futureWrapper.wrapperClassName(), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + IF_AVAILABLE_FUTURE_RESULT_WRITER, + wrappedReturnType) + .addStatement("return futureWrapper.getFuture()"); + } + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getIfAvailableClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_IfAvailable"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java new file mode 100644 index 0000000..a8c76c6 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InterfaceGenerator.java @@ -0,0 +1,690 @@ +/* + * 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.EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CONNECTOR_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.GeneratorUtilities.generateMethodReference; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +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.GeneratorContext; +import com.google.common.base.Ascii; +import com.squareup.javapoet.AnnotationSpec; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.List; +import java.util.Map; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** Generator of cross-profile code for a single {@link CrossProfile} type. */ +final class InterfaceGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + InterfaceGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("InterfaceGenerator#generate can only be called once"); + } + generated = true; + + generateCrossProfileTypeInterface(); + generateSingleSenderInterface(); + generateSingleSenderCanThrowInterface(); + generateMultipleSenderInterface(); + } + + private void generateCrossProfileTypeInterface() { + ClassName interfaceName = + getCrossProfileTypeInterfaceClassName(generatorContext, crossProfileType); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addJavadoc( + "Entry point for cross-profile calls to {@link $T}.\n", + crossProfileType.className()) + .addModifiers(Modifier.PUBLIC); + + ClassName connectorClassName = + crossProfileType.profileConnector().isPresent() + ? crossProfileType.profileConnector().get().connectorClassName() + : PROFILE_CONNECTOR_CLASSNAME; + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("create") + .returns(interfaceName) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(connectorClassName, "connector") + .addStatement( + "return new $T(connector)", + DefaultProfileClassGenerator.getDefaultProfileClassName( + generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("current") + .addJavadoc("Run a method on the current profile.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("other") + .addJavadoc("Run a method on the other profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("personal") + .addJavadoc("Run a method on the personal profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("work") + .addJavadoc("Run a method on the work profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("profile") + .addJavadoc("Run a method on the given profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addParameter(PROFILE_CLASSNAME, "profile") + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("profiles") + .addJavadoc( + CodeBlock.builder() + .add("Run a method on the given profiles, if accessible.\n\n") + .add( + "<p>This will deduplicate profiles to ensure that the method is only run" + + " at most once on each profile.\n") + .build()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addParameter(ArrayTypeName.of(PROFILE_CLASSNAME), "profiles") + .varargs(true) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("both") + .addJavadoc("Run a method on both the personal and work profile, if accessible.\n") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)) + .build()); + + if (!crossProfileType.profileConnector().isPresent() + || crossProfileType.profileConnector().get().primaryProfile() != ProfileType.NONE) { + generatePrimarySecondaryMethods(interfaceBuilder); + } + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generatePrimarySecondaryMethods(TypeSpec.Builder interfaceBuilder) { + generatePrimaryMethod(interfaceBuilder); + generateSecondaryMethod(interfaceBuilder); + generateSuppliersMethod(interfaceBuilder); + } + + private void generatePrimaryMethod(TypeSpec.Builder interfaceBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("primary") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)); + + if (crossProfileType.profileConnector().isPresent()) { + methodBuilder.addJavadoc( + "Run a method on the primary (" + + Ascii.toLowerCase(crossProfileType.profileConnector().get().primaryProfile().name()) + + ") profile, if accessible.\n\n@see $T#primaryProfile()\n", + CustomProfileConnector.class); + } else { + methodBuilder.addJavadoc( + "Run a method on the primary profile, if accessible.\n\n" + + "@throws $1T if the {@link $2T} does not have a primary profile set\n" + + "@see $2T#primaryProfile()\n", + IllegalStateException.class, + CustomProfileConnector.class); + } + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateSecondaryMethod(TypeSpec.Builder interfaceBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("secondary") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType)); + + if (crossProfileType.profileConnector().isPresent()) { + String secondaryProfileName = + crossProfileType.profileConnector().get().primaryProfile().equals(ProfileType.WORK) + ? Ascii.toLowerCase(ProfileType.PERSONAL.name()) + : Ascii.toLowerCase(ProfileType.WORK.name()); + methodBuilder.addJavadoc( + "Run a method on the secondary (" + + secondaryProfileName + + ") profile, if accessible.\n\n@see $T#primaryProfile()\n", + CustomProfileConnector.class); + } else { + methodBuilder.addJavadoc( + "Run a method on the secondary profile, if accessible.\n\n" + + "@throws $1T if the {@link $2T} does not have a primary profile set\n" + + "@see $2T#primaryProfile()\n", + IllegalStateException.class, + CustomProfileConnector.class); + } + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateSuppliersMethod(TypeSpec.Builder interfaceBuilder) { + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder("suppliers") + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(getMultipleSenderInterfaceClassName(generatorContext, crossProfileType)); + + if (crossProfileType.profileConnector().isPresent()) { + String primaryProfileName = + crossProfileType.profileConnector().get().primaryProfile().equals(ProfileType.WORK) + ? Ascii.toLowerCase(ProfileType.WORK.name()) + : Ascii.toLowerCase(ProfileType.PERSONAL.name()); + String secondaryProfileName = + crossProfileType.profileConnector().get().primaryProfile().equals(ProfileType.WORK) + ? Ascii.toLowerCase(ProfileType.PERSONAL.name()) + : Ascii.toLowerCase(ProfileType.WORK.name()); + methodBuilder + .addJavadoc("Run a method on supplier profiles, if accessible.\n\n") + .addJavadoc( + "<p>When run from the primary ($1L) profile, supplier profiles are the primary ($1L)" + + " and secondary ($2L) profiles. When run from the secondary ($2L) profile," + + " supplier profiles includes only the secondary ($2L) profile.\n\n", + primaryProfileName, + secondaryProfileName) + .addJavadoc("@see $T#primaryProfile()\n", CustomProfileConnector.class); + } else { + methodBuilder + .addJavadoc("Run a method on supplier profiles, if accessible.\n\n") + .addJavadoc( + "<p>When run from the primary profile, supplier profiles are the primary and" + + " secondary profiles. When run from the secondary profile, supplier profiles" + + " includes only the secondary profile.\n\n") + .addJavadoc( + "@throws $1T if the {@link $2T} does not have a primary profile set\n", + IllegalStateException.class, + CustomProfileConnector.class) + .addJavadoc("@see $T#primaryProfile()\n", CustomProfileConnector.class); + } + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateSingleSenderInterface() { + ClassName interfaceName = getSingleSenderInterfaceClassName(generatorContext, crossProfileType); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addModifiers(Modifier.PUBLIC) + .addJavadoc( + "Interface used for interacting with an instance of {@link $T} on a given" + + " profile.\n\n", + crossProfileType.className()) + .addJavadoc( + "<p>The profile is guaranteed to be available, so no {@link $T} will be thrown for" + + " any call.\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + generateMethodOnSingleSenderInterface(interfaceBuilder, method, crossProfileType); + } + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generateMethodOnSingleSenderInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(method.returnTypeTypeName()) + .addJavadoc("Make a call to {@link $L} on the given profile.\n\n", methodReference); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + methodBuilder + .addJavadoc("@see $L\n", methodReference) + .addExceptions(method.thrownExceptions()) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateSingleSenderCanThrowInterface() { + ClassName interfaceName = + getSingleSenderCanThrowInterfaceClassName(generatorContext, crossProfileType); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addModifiers(Modifier.PUBLIC) + .addJavadoc( + "Interface used for interacting with a {@link $T} on a given profile.\n", + crossProfileType.className()); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + generateMethodOnSingleSenderCanThrowInterface(interfaceBuilder, method, crossProfileType); + } + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("ifAvailable") + .addJavadoc( + "Make a call, returning a default value in case of error rather than throwing" + + " {@link $T}.\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns( + IfAvailableGenerator.getIfAvailableClassName(generatorContext, crossProfileType)) + .build()); + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("timeout") + .addJavadoc( + "Set a timeout to be used when making asynchronous calls to other profiles.\n\n" + + "<p>This overrides any timeout set on the type or method being called.\n") + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(interfaceName) + .addParameter(long.class, "timeout") + .build()); + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generateMethodOnSingleSenderCanThrowInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addExceptions(method.thrownExceptions()) + .returns(method.returnTypeTypeName()) + .addJavadoc("Make a call to {@link $L} on the given profile.\n\n", methodReference) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + if (method.isBlocking(generatorContext, crossProfileType)) { + methodBuilder.addException(UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addJavadoc( + "<p>If an unchecked exception is thrown and this call is made to a profile other than" + + " the current one, a {@link $T} will be thrown with the original exception as the" + + " cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME); + methodBuilder.addJavadoc( + "@throws $T if the profile is not connected.\n", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + } else if (method.isCrossProfileCallback(generatorContext)) { + methodBuilder.addJavadoc( + "<p>If an unchecked exception is thrown and this call is made to a profile other than" + + " the current one, a {@link $T} will be thrown on another thread with the original" + + " exception as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME); + methodBuilder.addJavadoc( + "<p>If the profile does not exist or is not available, an {@link $T} will be passed into" + + " the {@code exceptionCallback}.\n\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addParameter(EXCEPTION_CALLBACK_CLASSNAME, "exceptionCallback"); + } else { + // Future + methodBuilder.addJavadoc( + "<p>If an unchecked exception is thrown and this call is made to a profile other than" + + " the current one, a {@link $T} will be thrown on another thread with the original" + + " exception as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME); + methodBuilder.addJavadoc( + "<p>If the profile does not exist or is not available, the future will be set with an" + + " {@link $T}.\n\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + } + + methodBuilder.addJavadoc("@see $L\n", methodReference); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateMultipleSenderInterface() { + ClassName interfaceName = + getMultipleSenderInterfaceClassName(generatorContext, crossProfileType); + + TypeSpec.Builder interfaceBuilder = + TypeSpec.interfaceBuilder(interfaceName) + .addModifiers(Modifier.PUBLIC) + .addJavadoc( + "Interface used for interacting with a {@link $T} on multiple profiles.\n\n", + crossProfileType.className()) + .addJavadoc( + "<p>If any profiles are unavailable, the profile will not be included in the" + + " results. No {@link $T} will be thrown for any call.\n", + UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + if (method.isBlocking(generatorContext, crossProfileType)) { + generateBlockingMethodOnMultipleSenderInterface(interfaceBuilder, method, crossProfileType); + } else if (method.isCrossProfileCallback(generatorContext)) { + generateCrossProfileCallbackMethodOnMultipleSenderInterface( + interfaceBuilder, method, crossProfileType); + } else if (method.isFuture(crossProfileType)) { + generateFutureMethodOnMultipleSenderInterface(interfaceBuilder, method, crossProfileType); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + + interfaceBuilder.addMethod( + MethodSpec.methodBuilder("timeout") + .addJavadoc( + "Set a timeout to be used when making asynchronous calls to other profiles.\n\n" + + "<p>This overrides any timeout set on the type or method being called.") + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(interfaceName) + .addParameter(long.class, "timeout") + .build()); + + generatorUtilities.writeClassToFile(interfaceName.packageName(), interfaceBuilder); + } + + private void generateBlockingMethodOnMultipleSenderInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + TypeName returnType; + + if (!method.thrownExceptions().isEmpty()) { + // We don't add methods with exceptions to the multiplesender interface + return; + } + + if (method.returnType().getKind().equals(TypeKind.VOID)) { + // void is a special case so we don't return a map + returnType = TypeName.VOID; + } else { + TypeName boxedMethodReturnType = + TypeName.get(generatorUtilities.boxIfNecessary(method.returnType())); + returnType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, boxedMethodReturnType); + } + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(returnType) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addJavadoc("Make a call to {@link $L} on the given profiles.\n\n", methodReference); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + methodBuilder + .addJavadoc( + "<p>If any profiles are not connected, they will not be included in the" + + " results.\n\n") + .addJavadoc( + "<p>If an unchecked exception is thrown on any profile other than the current one," + + " a {@link $T} will be thrown with the original exception as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME) + .addJavadoc("@see $L\n", methodReference); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateCrossProfileCallbackMethodOnMultipleSenderInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + if (!method.isSimpleCrossProfileCallback(generatorContext)) { + // Non-simple callbacks can't be used with multiple profiles + return; + } + + VariableElement callbackParameter = method.getCrossProfileCallbackParam(generatorContext).get(); + TypeElement callbackType = + generatorContext.elements().getTypeElement(callbackParameter.asType().toString()); + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create(callbackType); + + List<ParameterSpec> parameters = + convertCallbackParametersIntoMulti( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + callbackParameter, + callbackInterface); + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .addParameters(parameters) + .addJavadoc("Make a call to {@link $L} on the given profiles.\n\n", methodReference); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + methodBuilder + .addJavadoc( + "<p>If any profiles are not available, they will not be included in the" + + " results.\n\n") + .addJavadoc( + "<p>If an unchecked exception is thrown on any profile other than the current one," + + " a {@link $T} will be thrown on another thread with the original exception" + + " as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME) + .addJavadoc("@see $L\n", methodReference); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private void generateFutureMethodOnMultipleSenderInterface( + TypeSpec.Builder interfaceBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + ClassName rawFutureType = TypeUtils.getRawTypeClassName(method.returnType()); + // We assume all Futures are generic with a single generic type + TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).iterator().next(); + + TypeMirror boxedWrappedType = generatorUtilities.boxIfNecessary(wrappedType); + + TypeName mapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, ClassName.get(boxedWrappedType)); + + ParameterizedTypeName returnType = ParameterizedTypeName.get(rawFutureType, mapType); + + CodeBlock methodReference = generateMethodReference(crossProfileType, method); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(method.simpleName()) + .addModifiers(Modifier.PUBLIC, Modifier.ABSTRACT) + .returns(returnType) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)) + .addJavadoc("Make a call to {@link $L} on the given profiles.\n\n", methodReference); + + for (TypeMirror automaticallyResolvedType : + method.automaticallyResolvedParameterTypes(crossProfileType.supportedTypes())) { + methodBuilder.addJavadoc( + "<p>A {@link $T} will automatically be passed to {@link $L}.\n\n", + automaticallyResolvedType, + methodReference); + } + + methodBuilder + .addJavadoc( + "<p>If any profiles are not available, or if any profiles set the future with an" + + " {@link $T}, they will not be included in the results.\n\n", + Exception.class) + .addJavadoc( + "<p>If an unchecked exception is thrown on any profile other than the current one," + + " a {@link $T} will be thrown on another thread with the original exception" + + " as the cause.\n\n", + PROFILE_RUNTIME_EXCEPTION_CLASSNAME) + .addJavadoc("@see $L\n", methodReference); + + interfaceBuilder.addMethod(methodBuilder.build()); + } + + private List<ParameterSpec> convertCallbackParametersIntoMulti( + List<ParameterSpec> parameters, + VariableElement callbackParameter, + CrossProfileCallbackInterfaceInfo callbackInterface) { + return parameters.stream() + .map( + e -> + e.name.equals(callbackParameter.getSimpleName().toString()) + ? convertCallbackToMulti(e, callbackInterface) + : e) + .collect(toList()); + } + + private ParameterSpec convertCallbackToMulti( + ParameterSpec parameter, CrossProfileCallbackInterfaceInfo callbackInterface) { + return ParameterSpec.builder( + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiInterfaceClassName( + generatorContext, callbackInterface), + parameter.name) + .addModifiers(parameter.modifiers) + .addAnnotations(parameter.annotations) + .build(); + } + + static ClassName getCrossProfileTypeInterfaceClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return crossProfileType.profileClassName(); + } + + static ClassName getSingleSenderInterfaceClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_SingleSender"); + } + + static ClassName getSingleSenderCanThrowInterfaceClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_SingleSenderCanThrow"); + } + + static ClassName getMultipleSenderInterfaceClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_MultipleSender"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java new file mode 100644 index 0000000..f4aad75 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalCrossProfileClassGenerator.java @@ -0,0 +1,421 @@ +/* + * 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.BUNDLER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_FUTURE_RESULT_WRITER; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.METHOD_RUNNER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_UTILITIES_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo.AutomaticallyResolvedParameterFilterBehaviour.REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +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.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.Optional; +import java.util.stream.IntStream; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_Internal} class for a single {@link CrossProfile} 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 InternalCrossProfileClassGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final ProviderClassInfo providerClass; + private final CrossProfileTypeInfo crossProfileType; + + InternalCrossProfileClassGenerator( + GeneratorContext generatorContext, + ProviderClassInfo providerClass, + CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.providerClass = checkNotNull(providerClass); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "InternalCrossProfileClassGenerator#generate can only be called once"); + } + generated = true; + + generateInternalCrossProfileClass(); + } + + private void generateInternalCrossProfileClass() { + ClassName className = getInternalCrossProfileClassName(generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + classBuilder.addJavadoc( + "Internal class for {@link $T}.\n\n" + + "<p>This is used by the Connected Apps SDK to dispatch cross-profile calls.\n\n" + + "<p>Cross-profile type identifier: $L.\n", + crossProfileType.crossProfileTypeElement().asType(), + crossProfileType.identifier()); + + classBuilder.addField( + FieldSpec.builder(className, "instance") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer("new $T()", className) + .build()); + + classBuilder.addField( + FieldSpec.builder(BUNDLER_CLASSNAME, "bundler") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer( + "new $T()", + BundlerGenerator.getBundlerClassName(generatorContext, crossProfileType)) + .build()); + + if (!crossProfileType.isStatic()) { + ExecutableElement providerMethod = + providerClass.findProviderMethodFor(generatorContext, crossProfileType); + String paramsString = providerMethod.getParameters().isEmpty() ? "()" : "(context)"; + CodeBlock providerMethodCall = + CodeBlock.of("$L$L", providerMethod.getSimpleName(), paramsString); + + classBuilder.addMethod( + MethodSpec.methodBuilder("crossProfileType") + .addParameter(CONTEXT_CLASSNAME, "context") + .returns(crossProfileType.className()) + .addStatement( + "return $T.instance().providerClass(context).$L", + InternalProviderClassGenerator.getInternalProviderClassName( + generatorContext, providerClass), + providerMethodCall) + .build()); + } + + classBuilder.addMethod( + MethodSpec.methodBuilder("bundler") + .returns(BUNDLER_CLASSNAME) + .addStatement("return bundler") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("instance") + .addModifiers(Modifier.STATIC) + .returns(className) + .addStatement("return instance") + .build()); + + classBuilder.addMethod(MethodSpec.constructorBuilder().addModifiers(Modifier.PRIVATE).build()); + + addMethodsField(classBuilder, crossProfileType); + addCrossProfileTypeMethods(classBuilder, crossProfileType); + addCallMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addMethodsField( + TypeSpec.Builder classBuilder, CrossProfileTypeInfo crossProfileType) { + int totalMethods = crossProfileType.crossProfileMethods().size(); + + classBuilder.addField( + FieldSpec.builder(ArrayTypeName.of(METHOD_RUNNER_CLASSNAME), "methods") + .addModifiers(Modifier.PRIVATE) + .initializer( + "new $T[]{$L}", + METHOD_RUNNER_CLASSNAME, + IntStream.range(0, totalMethods) + .mapToObj(n -> "this::method" + n) + .collect(joining(","))) + .build()); + } + + private void addCrossProfileTypeMethods( + TypeSpec.Builder classBuilder, CrossProfileTypeInfo crossProfileType) { + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + if (method.isBlocking(generatorContext, crossProfileType)) { + addBlockingCrossProfileTypeMethod(classBuilder, method); + } else if (method.isCrossProfileCallback(generatorContext)) { + addCrossProfileCallbackCrossProfileTypeMethod(classBuilder, method); + } else if (method.isFuture(crossProfileType)) { + addFutureCrossProfileTypeMethod(classBuilder, method); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + } + + private void addBlockingCrossProfileTypeMethod( + TypeSpec.Builder classBuilder, CrossProfileMethodInfo method) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + // parcle is recycled by caller + methodCode.addStatement("$1T returnParcel = $1T.obtain()", PARCEL_CLASSNAME); + + addExtractParametersCode(methodCode, method); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + getCrossProfileTypeReference(method), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (!method.thrownExceptions().isEmpty()) { + methodCode.beginControlFlow("try"); + } + + if (isPrimitiveOrObjectVoid(method.returnType())) { + methodCode.addStatement(methodCall); + } else { + methodCall = CodeBlock.of("$T returnValue = $L", method.returnType(), methodCall); + methodCode.addStatement(methodCall); + methodCode.add("returnParcel.writeInt(0); // No errors\n"); + methodCode.addStatement( + "bundler.writeToParcel(returnParcel, returnValue, $L, /* flags= */ 0)", + TypeUtils.generateBundlerType(method.returnType())); + } + + if (!method.thrownExceptions().isEmpty()) { + for (TypeName exceptionType : method.thrownExceptions()) { + methodCode.nextControlFlow("catch ($L e)", exceptionType); + methodCode.add("returnParcel.writeInt(1); // Errors\n"); + methodCode.addStatement( + "$T.writeThrowableToParcel(returnParcel, e)", PARCEL_UTILITIES_CLASSNAME); + } + methodCode.endControlFlow(); + } + + methodCode.addStatement("return returnParcel"); + + classBuilder.addMethod( + MethodSpec.methodBuilder("method" + method.identifier()) + .addModifiers(Modifier.PRIVATE) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(PARCEL_CLASSNAME, "params") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addCode(methodCode.build()) + .addJavadoc( + "Call $1L and return a {@link $2T} containing the return value.\n\n" + + "<p>The {@link $2T} must be recycled after use.\n", + GeneratorUtilities.methodJavadocReference(method.methodElement()), + PARCEL_CLASSNAME) + .build()); + } + + private void addCrossProfileCallbackCrossProfileTypeMethod( + TypeSpec.Builder classBuilder, CrossProfileMethodInfo method) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + // parcel is recycled by caller + methodCode.addStatement("$1T returnParcel = $1T.obtain()", PARCEL_CLASSNAME); + + addExtractParametersCode(methodCode, method); + + createCrossProfileCallbackParameter(methodCode, method); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + getCrossProfileTypeReference(method), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (isPrimitiveOrObjectVoid(method.returnType())) { + methodCode.addStatement(methodCall); + } else { + methodCall = CodeBlock.of("$T returnValue = $L", method.returnType(), methodCall); + methodCode.addStatement(methodCall); + methodCode.add("returnParcel.writeInt(0); // No errors\n"); + methodCode.addStatement( + "bundler.writeToParcel(returnParcel, returnValue, $L, /* flags= */ 0)", + TypeUtils.generateBundlerType(method.returnType())); + } + + methodCode.addStatement("return returnParcel"); + + classBuilder.addMethod( + MethodSpec.methodBuilder("method" + method.identifier()) + .addModifiers(Modifier.PRIVATE) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(PARCEL_CLASSNAME, "params") + // TODO: This should be renamed to "callback" once we prefix unpacked parameter names + // (without doing this, a param named "callback" will cause a compile error) + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "crossProfileCallback") + .addCode(methodCode.build()) + .addJavadoc( + "Call $1L, and link the callback to {@code crossProfileCallback}.\n\n" + + "@return An empty parcel. This must be recycled after use.\n", + GeneratorUtilities.methodJavadocReference(method.methodElement())) + .build()); + } + + private void addFutureCrossProfileTypeMethod( + TypeSpec.Builder classBuilder, CrossProfileMethodInfo method) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + // parcel is recycled by caller + methodCode.addStatement("$1T returnParcel = $1T.obtain()", PARCEL_CLASSNAME); + + addExtractParametersCode(methodCode, method); + + CodeBlock methodCall = + CodeBlock.of( + "$L.$L($L)", + getCrossProfileTypeReference(method), + method.simpleName(), + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + methodCode.addStatement("$T future = $L", method.returnType(), methodCall); + + TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType()); + + FutureWrapper futureWrapper = + crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get(); + // This assumes every Future is generic with one type argument + TypeMirror wrappedReturnType = + TypeUtils.extractTypeArguments(method.returnType()).iterator().next(); + methodCode.addStatement( + "$T.writeFutureResult(future, new $T<>(callback, bundler, $L))", + futureWrapper.wrapperClassName(), + CROSS_PROFILE_FUTURE_RESULT_WRITER, + TypeUtils.generateBundlerType(wrappedReturnType)); + + // TODO: Can this just return null? where does it go? that'd avoid having to obtain/recycle + methodCode.addStatement("return returnParcel"); + + classBuilder.addMethod( + MethodSpec.methodBuilder("method" + method.identifier()) + .addModifiers(Modifier.PRIVATE) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(PARCEL_CLASSNAME, "params") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addCode(methodCode.build()) + .addJavadoc( + "Call $1L, and link the returned future to {@code crossProfileCallback}.\n\n" + + "@return An empty parcel. This must be recycled after use.\n", + GeneratorUtilities.methodJavadocReference(method.methodElement())) + .build()); + } + + private void createCrossProfileCallbackParameter( + CodeBlock.Builder methodCode, CrossProfileMethodInfo method) { + VariableElement asyncCallbackParam = + method.getCrossProfileCallbackParam(generatorContext).get(); + + TypeElement callbackType = + generatorContext.elements().getTypeElement(asyncCallbackParam.asType().toString()); + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create(callbackType); + + methodCode.addStatement( + "$T $L = new $L(crossProfileCallback, bundler)", + asyncCallbackParam.asType(), + asyncCallbackParam.getSimpleName(), + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackReceiverClassName( + generatorContext, callbackInterface)); + } + + private static boolean isPrimitiveOrObjectVoid(TypeMirror typeMirror) { + return typeMirror.getKind().equals(TypeKind.VOID) + || typeMirror.toString().equals("java.lang.Void"); + } + + private void addExtractParametersCode(CodeBlock.Builder code, CrossProfileMethodInfo method) { + Optional<VariableElement> callbackParameter = + method.getCrossProfileCallbackParam(generatorContext); + for (VariableElement parameter : method.methodElement().getParameters()) { + if (callbackParameter.isPresent() + && callbackParameter.get().getSimpleName().equals(parameter.getSimpleName())) { + continue; // Don't extract a callback parameter + } + if (crossProfileType.supportedTypes().isAutomaticallyResolved(parameter.asType())) { + continue; + } + code.addStatement( + "@SuppressWarnings(\"unchecked\") $1T $2L = ($1T) bundler.readFromParcel(params, $3L)", + parameter.asType(), + parameter.getSimpleName().toString(), + TypeUtils.generateBundlerType(parameter.asType())); + } + } + + private static void addCallMethod(TypeSpec.Builder classBuilder) { + classBuilder.addMethod( + MethodSpec.methodBuilder("call") + .addModifiers(Modifier.PUBLIC) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(int.class, "methodIdentifier") + .addParameter(PARCEL_CLASSNAME, "params") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .beginControlFlow("if (methodIdentifier >= methods.length)") + .addStatement( + "throw new $T(\"Invalid method identifier\" + methodIdentifier)", + IllegalArgumentException.class) + .endControlFlow() + .addStatement("return methods[methodIdentifier].call(context, params, callback)") + .addJavadoc( + "Call the method referenced by {@code methodIdentifier}.\n\n" + + "<p>If the method is synchronous, this will return a {@link $1T} containing" + + " the return value, otherwise it will return an empty {@link $1T}. The" + + " {@link $1T} must be recycled after use.\n", + PARCEL_CLASSNAME) + .build()); + } + + private CodeBlock getCrossProfileTypeReference(CrossProfileMethodInfo method) { + if (method.isStatic()) { + return CodeBlock.of("$1T", crossProfileType.className()); + } + return CodeBlock.of("crossProfileType(context)"); + } + + static ClassName getInternalCrossProfileClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName(crossProfileType.profileClassName(), "_Internal"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java new file mode 100644 index 0000000..c02bbfb --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/InternalProviderClassGenerator.java @@ -0,0 +1,186 @@ +/* + * 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.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.PackageElement; + +/** + * Generate the {@code Profile_*_Internal} class for a single provider class. + * + * <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 InternalProviderClassGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final ProviderClassInfo providerClass; + + InternalProviderClassGenerator( + GeneratorContext generatorContext, ProviderClassInfo providerClass) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.providerClass = checkNotNull(providerClass); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "InternalProviderClassGenerator#generate can only be called once"); + } + generated = true; + + generateInternalProviderClassClass(); + } + + private void generateInternalProviderClassClass() { + ClassName className = getInternalProviderClassName(generatorContext, providerClass); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className).addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + classBuilder.addJavadoc( + "Internal provider class for $L\n", + providerClass.providerClassElement().asType().toString()); + + classBuilder.addField( + FieldSpec.builder(className, "instance") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer("new $T()", className) + .build()); + + classBuilder.addField( + FieldSpec.builder(providerClass.className(), "providerClass") + .addModifiers(Modifier.PRIVATE) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("instance") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(className) + .addStatement("return instance") + .addJavadoc("Get the singleton instance of this provider.\n") + .build()); + + String providerClassConstructorParameters = + providerClass.publicConstructorArgumentTypes().isEmpty() + ? "" + : "context"; // We only allow a context or no-args + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("providerClass") + .addJavadoc( + "Get a singleton instance of the original class which caused generation of this" + + " class.\n") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONTEXT_CLASSNAME, "context") + .returns(providerClass.className()) + .addComment( + "This can't be constructed with the field declaration as sometimes these classes" + + " need arguments") + .beginControlFlow("if (providerClass == null)") + .beginControlFlow( + "synchronized ($T.class)", + getInternalProviderClassName(generatorContext, providerClass)) + .beginControlFlow("if (providerClass == null)") + .addStatement( + "this.providerClass = new $T($L)", + providerClass.className(), + providerClassConstructorParameters) + .endControlFlow() + .endControlFlow() + .endControlFlow() + .addStatement("return providerClass") + .build()); + + addCallMethod(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addCallMethod(TypeSpec.Builder classBuilder) { + CodeBlock.Builder methodCode = CodeBlock.builder(); + + for (CrossProfileTypeInfo crossProfileType : providerClass.allCrossProfileTypes()) { + addCrossProfileTypeDispatcher(methodCode, crossProfileType); + } + + methodCode.addStatement( + "throw new $T(\"Unknown type identifier \" + crossProfileTypeIdentifier)", + IllegalArgumentException.class); + + classBuilder.addMethod( + MethodSpec.methodBuilder("call") + .addModifiers(Modifier.PUBLIC) + .returns(PARCEL_CLASSNAME) + .addParameter(CONTEXT_CLASSNAME, "context") + .addParameter(long.class, "crossProfileTypeIdentifier") + .addParameter(int.class, "methodIdentifier") + .addParameter(PARCEL_CLASSNAME, "params") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addCode(methodCode.build()) + .addJavadoc( + "Call the {@code call} method on the internal type referenced by the {@code" + + " crossProfileTypeIdentifier}.\n\n" + + "@return A {@link $1T} which contains the return value (if a synchronous" + + " call) or is empty\n (if asynchronous). This {@link $1T} must be recycled" + + " after use.\n", + PARCEL_CLASSNAME) + .build()); + } + + private void addCrossProfileTypeDispatcher( + CodeBlock.Builder methodCode, CrossProfileTypeInfo crossProfileType) { + methodCode.beginControlFlow( + "if (crossProfileTypeIdentifier == $LL)", crossProfileType.identifier()); + methodCode.addStatement( + "return $T.instance().call(context, methodIdentifier, params, callback)", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + methodCode.endControlFlow(); + } + + static ClassName getInternalProviderClassName( + GeneratorContext generatorContext, ProviderClassInfo providerClass) { + PackageElement originalPackage = + generatorContext.elements().getPackageOf(providerClass.providerClassElement()); + String internalProviderClassName = + String.format("Profile_%s_Internal", providerClass.simpleName()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), internalProviderClassName); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java new file mode 100644 index 0000000..4b55483 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/LateValidator.java @@ -0,0 +1,213 @@ +/* + * 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.annotationdiscovery.AnnotationFinder.validationMessageFormatterFor; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.validationMessageFormatterForClass; +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import com.google.common.collect.Streams; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import javax.lang.model.element.Element; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic.Kind; + +/** Validator to check that annotations have been used correctly before generating code. */ +final class LateValidator { + + private final GeneratorContext generatorContext; + + private static final String PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR = + "All @CROSS_PROFILE_ANNOTATION types provided by a provider class must use the same" + + " ProfileConnector"; + private static final String CONFIGURATION_DIFFERENT_CONNECTOR_ERROR = + "All @CROSS_PROFILE_ANNOTATION types specified in the same configuration must use the same" + + " ProfileConnector"; + private static final String INCORRECT_SERVICE_CLASS = + "The class specified by serviceClass must match the serviceClassName given by the" + + " ProfileConnector"; + private static final String STATICTYPES_ERROR = + "@CROSS_PROFILE_ANNOTATION classes referenced in @CROSS_PROFILE_PROVIDER_ANNOTATION" + + " staticTypes annotations must not have non-static @CROSS_PROFILE_ANNOTATION annotated" + + " methods"; + + LateValidator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + } + + /** + * Validate code. + * + * <p>This will show errors for all issues found. It will not terminate upon finding the first + * error. + * + * @return True if the state is valid + */ + boolean validate() { + return Stream.of( + validateProviderClasses(generatorContext.providers()), + validateConfigurations(generatorContext.configurations())) + .allMatch(b -> b); + } + + private boolean validateProviderClasses(Collection<ProviderClassInfo> providerClasses) { + boolean isValid = true; + + for (ProviderClassInfo providerClass : providerClasses) { + isValid = validateProviderClass(providerClass) && isValid; + } + + return isValid; + } + + private boolean validateProviderClass(ProviderClassInfo providerClass) { + boolean isValid = true; + + if (getConnectorQualifiedNamesUsedInProviderClass(providerClass).size() > 1) { + showError( + PROVIDER_CLASS_DIFFERENT_CONNECTOR_ERROR, + providerClass.providerClassElement(), + validationMessageFormatterForClass(providerClass.providerClassElement())); + isValid = false; + } + + for (CrossProfileTypeInfo crossProfileType : providerClass.staticTypes()) { + if (!crossProfileType.isStatic()) { + showError(STATICTYPES_ERROR, providerClass.providerClassElement()); + isValid = false; + } + } + + return isValid; + } + + private boolean validateConfigurations(Collection<CrossProfileConfigurationInfo> configurations) { + boolean isValid = true; + + for (CrossProfileConfigurationInfo configuration : configurations) { + isValid = validateConfiguration(configuration) && isValid; + } + + return isValid; + } + + private boolean validateConfiguration(CrossProfileConfigurationInfo configuration) { + boolean isValid = true; + + isValid = + validateCrossProfileTypesHaveUniqueIdentifiers( + configuration.providers().stream() + .flatMap(m -> m.allCrossProfileTypes().stream()) + .collect(toList())) + && isValid; + + if (getConnectorQualifiedNamesUsedInConfiguration(configuration).size() > 1) { + showError(CONFIGURATION_DIFFERENT_CONNECTOR_ERROR, configuration.configurationElement()); + isValid = false; + } + + if (configuration.serviceClass().isPresent() + && !configuration + .serviceClass() + .get() + .toString() + .equals(configuration.profileConnector().serviceName().toString())) { + showError(INCORRECT_SERVICE_CLASS, configuration.configurationElement()); + isValid = false; + } + + return isValid; + } + + private static Collection<String> getConnectorQualifiedNamesUsedInConfiguration( + CrossProfileConfigurationInfo configuration) { + Set<String> connectorQualifiedNames = + configuration.providers().stream() + .flatMap(m -> getConnectorQualifiedNamesUsedInProviderClass(m).stream()) + .collect(toSet()); + connectorQualifiedNames.add( + configuration.profileConnector().connectorElement().asType().toString()); + return connectorQualifiedNames; + } + + private boolean validateCrossProfileTypesHaveUniqueIdentifiers( + Collection<CrossProfileTypeInfo> crossProfileTypes) { + boolean isValid = true; + Map<Long, List<CrossProfileTypeInfo>> crossProfileTypeByIdentifier = + crossProfileTypes.stream().collect(groupingBy(CrossProfileTypeInfo::identifier)); + + for (long identifier : crossProfileTypeByIdentifier.keySet()) { + if (crossProfileTypeByIdentifier.get(identifier).size() > 1) { + isValid = false; + String crossProfileTypesString = + crossProfileTypeByIdentifier.get(identifier).stream() + .map(m -> m.className().toString()) + .collect(joining(",")); + showError( + "The following cross-profile types all share an identifier(" + + identifier + + "): " + + crossProfileTypesString, + crossProfileTypeByIdentifier.get(identifier).get(0).crossProfileTypeElement()); + } + } + + return isValid; + } + + private static Collection<String> getConnectorQualifiedNamesUsedInProviderClass( + ProviderClassInfo providerClass) { + return providerClass.allCrossProfileTypes().stream() + .map(CrossProfileTypeInfo::profileConnector) + .flatMap(Streams::stream) + .map(ProfileConnectorInfo::connectorElement) + .map(Element::asType) + .map(TypeMirror::toString) + .collect(toSet()); + } + + private void showError( + String errorText, + Element errorElement, + ValidationMessageFormatter validationMessageFormatter) { + showErrorPreformatted(validationMessageFormatter.format(errorText), errorElement); + } + + private void showError(String errorText, Element errorElement) { + showErrorPreformatted( + validationMessageFormatterFor(errorElement).format(errorText), errorElement); + } + + private void showErrorPreformatted(String errorText, Element errorElement) { + generatorContext + .processingEnv() + .getMessager() + .printMessage(Kind.ERROR, errorText, errorElement); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java new file mode 100644 index 0000000..38b8038 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/MultipleProfilesGenerator.java @@ -0,0 +1,392 @@ +/* + * 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.ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CALLBACK_MERGER_EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_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.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +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.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_MultipleProfiles} 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 MultipleProfilesGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + MultipleProfilesGenerator( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("MultipleProfilesGenerator#generate can only be called once"); + } + generated = true; + + generateMultipleProfilesClass(); + } + + private void generateMultipleProfilesClass() { + ClassName className = getMultipleProfilesClassName(generatorContext, crossProfileType); + + ClassName multipleSenderInterface = + InterfaceGenerator.getMultipleSenderInterfaceClassName(generatorContext, crossProfileType); + ClassName singleSenderCanThrowInterfaceName = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Default implementation of {@link $T}.\n\n" + + "<p>Wraps a number of {@link $T} instances and merges their return" + + " values.\n", + multipleSenderInterface, + singleSenderCanThrowInterfaceName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(multipleSenderInterface); + + ParameterizedTypeName senderMapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, singleSenderCanThrowInterfaceName); + + classBuilder.addField(senderMapType, "senders", Modifier.PRIVATE, Modifier.FINAL); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(senderMapType, "senders") + .addStatement("this.senders = senders") + .build()); + + 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") + .beginControlFlow("for ($T senderProfile : senders.keySet())", PROFILE_CLASSNAME) + .addStatement("senders.put(senderProfile, senders.get(senderProfile).timeout(timeout))") + .endControlFlow() + .addStatement("return this") + .build()); + + for (CrossProfileMethodInfo method : crossProfileType.crossProfileMethods()) { + if (method.isBlocking(generatorContext, crossProfileType)) { + generateBlockingMethodOnMultipleProfilesClass(classBuilder, method, crossProfileType); + } else if (method.isCrossProfileCallback(generatorContext)) { + generateCrossProfileCallbackMethodOnMultipleProfilesClass( + classBuilder, method, crossProfileType); + } else if (method.isFuture(crossProfileType)) { + generateFutureMethodOnMultipleProfilesClass(classBuilder, method, crossProfileType); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateBlockingMethodOnMultipleProfilesClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + if (!method.thrownExceptions().isEmpty()) { + // We don't add methods with exceptions to multiple profiles + return; + } + + TypeName returnType; + if (method.returnType().getKind().equals(TypeKind.VOID)) { + // void is a special case so we don't return a map + returnType = TypeName.VOID; + } else { + TypeName boxedMethodReturnType = + TypeName.get(generatorUtilities.boxIfNecessary(method.returnType())); + returnType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, boxedMethodReturnType); + } + + String methodName = method.simpleName(); + String params = + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS); + + CodeBlock methodCall = CodeBlock.of("$L($L)", methodName, params); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(methodName) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(returnType) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + if (method.returnType().getKind().equals(TypeKind.VOID)) { + methodBuilder.beginControlFlow( + "for ($T sender : senders.values())", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + methodBuilder.beginControlFlow("try"); + methodBuilder.addStatement("sender.$L", methodCall); + methodBuilder.nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addComment( + "If the profile is not available we just don't include it in results"); + methodBuilder.endControlFlow(); + methodBuilder.endControlFlow(); + } else { + methodBuilder.addStatement("$T returnValue = new $T<>()", returnType, HashMap.class); + methodBuilder.beginControlFlow( + "for ($T senderIdentifier : senders.keySet())", PROFILE_CLASSNAME); + methodBuilder.addStatement( + "$T sender = senders.get(senderIdentifier)", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + methodBuilder.beginControlFlow("try"); + methodBuilder.addStatement("returnValue.put(senderIdentifier, sender.$L)", methodCall); + methodBuilder.nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addComment( + "If the profile is not available we just don't include it in results"); + methodBuilder.endControlFlow(); + methodBuilder.endControlFlow(); + methodBuilder.addStatement("return returnValue"); + } + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateCrossProfileCallbackMethodOnMultipleProfilesClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + if (!method.isSimpleCrossProfileCallback(generatorContext)) { + // Non-simple callbacks can't be used with multiple profiles + return; + } + + String methodName = method.simpleName(); + + VariableElement callbackParameter = method.getCrossProfileCallbackParam(generatorContext).get(); + TypeElement callbackType = + generatorContext.elements().getTypeElement(callbackParameter.asType().toString()); + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create(callbackType); + + List<ParameterSpec> parameters = + convertCallbackParametersIntoMulti( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS), + callbackParameter, + callbackInterface); + + TypeMirror paramType = + callbackInterface.methods().get(0).getParameters().isEmpty() + ? generatorContext.elements().getTypeElement("java.lang.Void").asType() + : callbackInterface.methods().get(0).getParameters().get(0).asType(); + + if (paramType.getKind().isPrimitive()) { + PrimitiveType primitiveType = (PrimitiveType) paramType; + paramType = generatorContext.types().boxedClass(primitiveType).asType(); + } + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC) + .addParameters(parameters) + .addStatement( + "$1T mergedResultListener = new $1T($2L)", + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiMergerResultClassName( + generatorContext, callbackInterface), + callbackParameter.getSimpleName()) + .addStatement( + "$1T<$2T> merger = new $1T<>(senders.size(), mergedResultListener)", + ASYNC_CALLBACK_PARAM_MULTIMERGER_CLASSNAME, + paramType); + + String params = + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS, + (p) -> + callbackParameter.getSimpleName().contentEquals(p) + ? generateMergerInputConstructor(callbackInterface) + : p); + + methodBuilder.beginControlFlow( + "for ($T senderIdentifier : senders.keySet())", PROFILE_CLASSNAME); + methodBuilder.addStatement( + "$T sender = senders.get(senderIdentifier)", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + + methodBuilder.addStatement( + "sender.$L($L, new $T<$T>(senderIdentifier, merger))", + methodName, + params, + CALLBACK_MERGER_EXCEPTION_CALLBACK_CLASSNAME, + paramType); + methodBuilder.endControlFlow(); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateFutureMethodOnMultipleProfilesClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + + ClassName rawFutureType = TypeUtils.getRawTypeClassName(method.returnType()); + FutureWrapper futureWrapper = + crossProfileType + .supportedTypes() + .getType(TypeUtils.removeTypeArguments(method.returnType())) + .getFutureWrapper() + .get(); + // We assume all Futures are generic with a single generic type + TypeMirror wrappedType = TypeUtils.extractTypeArguments(method.returnType()).iterator().next(); + + TypeMirror boxedWrappedType = generatorUtilities.boxIfNecessary(wrappedType); + + TypeName mapType = + ParameterizedTypeName.get( + ClassName.get(Map.class), PROFILE_CLASSNAME, ClassName.get(boxedWrappedType)); + + ParameterizedTypeName returnType = ParameterizedTypeName.get(rawFutureType, mapType); + + String methodName = method.simpleName(); + String params = + method.commaSeparatedParameters( + crossProfileType.supportedTypes(), REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS); + + CodeBlock methodCall = CodeBlock.of("$L($L)", methodName, params); + + MethodSpec.Builder methodBuilder = + MethodSpec.methodBuilder(methodName) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(returnType) + .addParameters( + GeneratorUtilities.extractParametersFromMethod( + crossProfileType.supportedTypes(), + method.methodElement(), + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS)); + + methodBuilder.addStatement( + "$T<$T, $T> results = new $T<>()", + Map.class, + PROFILE_CLASSNAME, + method.returnType(), + HashMap.class); + methodBuilder.beginControlFlow( + "for ($T senderIdentifier : senders.keySet())", PROFILE_CLASSNAME); + methodBuilder.addStatement( + "$T sender = senders.get(senderIdentifier)", + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType)); + methodBuilder.addStatement("results.put(senderIdentifier, sender.$L)", methodCall); + methodBuilder.endControlFlow(); + methodBuilder.addStatement("return $T.groupResults(results)", futureWrapper.wrapperClassName()); + + classBuilder.addMethod(methodBuilder.build()); + } + + private List<ParameterSpec> convertCallbackParametersIntoMulti( + List<ParameterSpec> parameters, + VariableElement callbackParameter, + CrossProfileCallbackInterfaceInfo callbackInterface) { + return parameters.stream() + .map( + e -> + e.name.equals(callbackParameter.getSimpleName().toString()) + ? convertCallbackToMulti(e, callbackInterface) + : e) + .collect(toList()); + } + + private String generateMergerInputConstructor( + CrossProfileCallbackInterfaceInfo callbackInterface) { + return CodeBlock.of( + "new $T(senderIdentifier, merger)", + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiMergerInputClassName( + generatorContext, callbackInterface)) + .toString(); + } + + private ParameterSpec convertCallbackToMulti( + ParameterSpec parameter, CrossProfileCallbackInterfaceInfo callbackInterface) { + return ParameterSpec.builder( + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackMultiInterfaceClassName( + generatorContext, callbackInterface), + parameter.name) + .addModifiers(parameter.modifiers) + .addAnnotations(parameter.annotations) + .build(); + } + + static ClassName getMultipleProfilesClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_MultipleProfiles"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java new file mode 100644 index 0000000..ace2f9e --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/OtherProfileGenerator.java @@ -0,0 +1,379 @@ +/* + * 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.EXCEPTION_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.LOCAL_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PROFILE_CONNECTOR_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.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +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.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the {@code Profile_*_OtherProfile} 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 OtherProfileGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileTypeInfo crossProfileType; + + OtherProfileGenerator(GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.crossProfileType = checkNotNull(crossProfileType); + } + + void generate() { + if (generated) { + throw new IllegalStateException("OtherProfileGenerator#generate can only be called once"); + } + generated = true; + + generateOtherProfileClass(); + } + + private void generateOtherProfileClass() { + ClassName className = getOtherProfileClassName(generatorContext, crossProfileType); + + ClassName singleSenderCanThrowInterface = + InterfaceGenerator.getSingleSenderCanThrowInterfaceClassName( + generatorContext, crossProfileType); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Implementation of {@link $T} used when interacting with the other profile.\n", + singleSenderCanThrowInterface) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addSuperinterface(singleSenderCanThrowInterface); + + ClassName connectorClassName = + crossProfileType.profileConnector().isPresent() + ? crossProfileType.profileConnector().get().connectorClassName() + : PROFILE_CONNECTOR_CLASSNAME; + + classBuilder.addField(connectorClassName, "connector", Modifier.PRIVATE, Modifier.FINAL); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(connectorClassName, "connector") + .beginControlFlow("if (connector == null)") + .addStatement("throw new $T()", NullPointerException.class) + .endControlFlow() + .addStatement("this.connector = connector") + .build()); + + classBuilder.addField( + FieldSpec.builder(long.class, "timeout") + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()) + .addModifiers(Modifier.PRIVATE) + .initializer("$L", CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET) + .build()); + + 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("this.timeout = 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)) { + generateBlockingMethodOnOtherProfileClass(classBuilder, method, crossProfileType); + } else if (method.isCrossProfileCallback(generatorContext)) { + generateCrossProfileCallbackMethodOnOtherProfileClass( + classBuilder, method, crossProfileType); + } else if (method.isFuture(crossProfileType)) { + generateFutureMethodOnOtherProfileClass(classBuilder, method, crossProfileType); + } else { + throw new IllegalStateException("Unknown method type: " + method); + } + } + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateBlockingMethodOnOtherProfileClass( + 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)); + + methodBuilder.addStatement( + "$1T internalCrossProfileClass = $1T.instance()", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + + // parcel is recycled in this method + methodBuilder.addStatement("$1T params = $1T.obtain()", PARCEL_CLASSNAME); + for (VariableElement param : method.methodElement().getParameters()) { + if (crossProfileType.supportedTypes().isAutomaticallyResolved(param.asType())) { + continue; + } + methodBuilder.addStatement( + "internalCrossProfileClass.bundler().writeToParcel(params, $1L, $2L, /* flags= */ 0)", + param.getSimpleName(), + TypeUtils.generateBundlerType(param.asType())); + } + + if (method.thrownExceptions().isEmpty()) { + methodBuilder.addStatement( + "$1T returnParcel = connector.crossProfileSender().call($2LL, $3L, params)", + PARCEL_CLASSNAME, + crossProfileType.identifier(), + method.identifier()); + } else { + methodBuilder.addStatement("$1T returnParcel", PARCEL_CLASSNAME); + methodBuilder.beginControlFlow("try"); + methodBuilder.addStatement( + "returnParcel = connector.crossProfileSender().callWithExceptions($1LL, $2L, params)", + crossProfileType.identifier(), + method.identifier()); + methodBuilder.nextControlFlow("catch ($T e)", UNAVAILABLE_PROFILE_EXCEPTION_CLASSNAME); + methodBuilder.addStatement("throw e"); + + for (TypeName exception : method.thrownExceptions()) { + methodBuilder.nextControlFlow("catch ($T e)", exception); + methodBuilder.addStatement("throw e"); + } + + methodBuilder.nextControlFlow("catch ($T e)", Throwable.class); + methodBuilder.addStatement( + "throw new $T($S)", IllegalStateException.class, "Unexpected exception thrown"); + methodBuilder.endControlFlow(); + } + + methodBuilder.addStatement("params.recycle()"); + + if (!method.returnType().getKind().equals(TypeKind.VOID)) { + methodBuilder.addStatement( + CodeBlock.of( + "@SuppressWarnings(\"unchecked\") $1T returnValue = ($1T)" + + " internalCrossProfileClass.bundler().readFromParcel(returnParcel," + + " $2L)", + method.returnType(), + TypeUtils.generateBundlerType(method.returnType()))); + methodBuilder.addStatement("returnParcel.recycle()"); + methodBuilder.addStatement("return returnValue"); + } else { + methodBuilder.addStatement("returnParcel.recycle()"); + } + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateCrossProfileCallbackMethodOnOtherProfileClass( + TypeSpec.Builder classBuilder, + CrossProfileMethodInfo method, + CrossProfileTypeInfo crossProfileType) { + VariableElement callbackParameter = method.getCrossProfileCallbackParam(generatorContext).get(); + TypeElement callbackType = + generatorContext.elements().getTypeElement(callbackParameter.asType().toString()); + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create(callbackType); + + 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"); + + methodBuilder.addStatement( + "$1T internalCrossProfileClass = $1T.instance()", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + + // parcel is passed into callAsync where it will be cached and recycled afterwards + methodBuilder.addStatement("$1T params = $1T.obtain()", PARCEL_CLASSNAME); + + for (VariableElement param : method.methodElement().getParameters()) { + if (crossProfileType.supportedTypes().isAutomaticallyResolved(param.asType())) { + continue; + } + if (param.getSimpleName().equals(callbackParameter.getSimpleName())) { + continue; + } + methodBuilder.addStatement( + "internalCrossProfileClass.bundler().writeToParcel(params, $1L, $2L, /* flags= */ 0)", + param.getSimpleName(), + TypeUtils.generateBundlerType(param.asType())); + } + + methodBuilder.addStatement( + "$1T sender = new $2T($3L, exceptionCallback, internalCrossProfileClass.bundler())", + LOCAL_CALLBACK_CLASSNAME, + CrossProfileCallbackCodeGenerator.getCrossProfileCallbackSenderClassName( + generatorContext, callbackInterface), + callbackParameter.getSimpleName()); + + // Suppress GoodTime warning for unboxing Duration. + methodBuilder.addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()); + methodBuilder.addStatement( + "connector.crossProfileSender().callAsync($1LL, $2L, params, sender, timeout ==" + + " $3L ? $4L : timeout)", + crossProfileType.identifier(), + method.identifier(), + CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET, + method.timeoutMillis()); + + methodBuilder.addComment( + "We don't recycle the params as they will be stored for the async call and recycled" + + " afterwards"); + + classBuilder.addMethod(methodBuilder.build()); + } + + private void generateFutureMethodOnOtherProfileClass( + 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)); + + methodBuilder.addStatement( + "$1T internalCrossProfileClass = $1T.instance()", + InternalCrossProfileClassGenerator.getInternalCrossProfileClassName( + generatorContext, crossProfileType)); + + // parcel is passed into callAsync where it will be cached and recycled afterwards + methodBuilder.addStatement("$1T params = $1T.obtain()", PARCEL_CLASSNAME); + for (VariableElement param : method.methodElement().getParameters()) { + if (crossProfileType.supportedTypes().isAutomaticallyResolved(param.asType())) { + continue; + } + methodBuilder.addStatement( + "internalCrossProfileClass.bundler().writeToParcel(params, $1L, $2L, /* flags= */ 0)", + param.getSimpleName(), + TypeUtils.generateBundlerType(param.asType())); + } + + TypeMirror rawFutureType = TypeUtils.removeTypeArguments(method.returnType()); + + FutureWrapper futureWrapper = + crossProfileType.supportedTypes().getType(rawFutureType).getFutureWrapper().get(); + // This assumes every Future is generic with one type argument + TypeMirror wrappedReturnType = + TypeUtils.extractTypeArguments(method.returnType()).iterator().next(); + methodBuilder.addStatement( + "$1T<$2T> futureWrapper = $1T.create(internalCrossProfileClass.bundler(), $3L)", + futureWrapper.wrapperClassName(), + wrappedReturnType, + TypeUtils.generateBundlerType(wrappedReturnType)); + + methodBuilder.addAnnotation( + AnnotationSpec.builder(SuppressWarnings.class) + .addMember("value", "$S", "GoodTime") + .build()); + methodBuilder.addStatement( + "connector.crossProfileSender().callAsync($1LL, $2L, params, futureWrapper," + + " timeout == $3L ? $4L : timeout)", + crossProfileType.identifier(), + method.identifier(), + CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET, + method.timeoutMillis()); + + methodBuilder.addComment( + "We don't recycle the params as they will be stored for the async call and recycled" + + " afterwards"); + + methodBuilder.addStatement("return futureWrapper.getFuture()"); + + classBuilder.addMethod(methodBuilder.build()); + } + + static ClassName getOtherProfileClassName( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + return GeneratorUtilities.appendToClassName( + crossProfileType.profileClassName(), "_OtherProfile"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java new file mode 100644 index 0000000..dfcea33 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ParcelableWrappersGenerator.java @@ -0,0 +1,154 @@ +/* + * 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.common.base.Preconditions.checkNotNull; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper.WrapperType; +import com.google.android.enterprise.connectedapps.processor.containers.Type; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.util.Collection; +import java.util.Optional; +import javax.tools.JavaFileObject; + +/** + * Generate the {@code Parcelable*} classes for every used compatible type. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <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 ParcelableWrappersGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + + ParcelableWrappersGenerator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ParcelableWrappersGenerator#generate can only be called once"); + } + generated = true; + + generateParcelableWrappers(); + } + + private void generateParcelableWrappers() { + Collection<ParcelableWrapper> parcelableWrappersToGenerate = + generatorContext.crossProfileTypes().stream() + .map(CrossProfileTypeInfo::supportedTypes) + .flatMap(s -> s.usableTypes().stream()) + .filter(s -> s.getParcelableWrapper().isPresent()) + .map(Type::getParcelableWrapper) + .map(Optional::get) + .collect(toSet()); + + generateDefaultParcelableWrappers(parcelableWrappersToGenerate); + generateProtoParcelableWrappers(parcelableWrappersToGenerate); + } + + private void generateDefaultParcelableWrappers(Collection<ParcelableWrapper> parcelableWrappers) { + Collection<ParcelableWrapper> defaultParcelableWrappersToGenerate = + parcelableWrappers.stream() + .filter(f -> f.wrapperType() == WrapperType.DEFAULT) + .collect(toSet()); + + for (ParcelableWrapper parcelableWrapper : defaultParcelableWrappersToGenerate) { + if (generatorContext + .elements() + .getTypeElement(parcelableWrapper.wrapperClassName().toString()) + != null) { + // We don't generate things which already exist + return; + } + generateDefaultParcelableWrapper(parcelableWrapper); + } + } + + private void generateDefaultParcelableWrapper(ParcelableWrapper parcelableWrapper) { + String parcelableWrapperSimpleName = parcelableWrapper.defaultWrapperClassName().simpleName(); + + String contents; + InputStream in = + ParcelableWrappersGenerator.class.getResourceAsStream( + "/parcelablewrappers/" + parcelableWrapperSimpleName + ".java"); + + try (BufferedReader br = + new BufferedReader(new InputStreamReader(in, Charset.defaultCharset()))) { + contents = br.lines().collect(joining(System.lineSeparator())); + } catch (IOException e) { + throw new IllegalStateException( + "Could not read parcelablewrapper file for " + parcelableWrapperSimpleName, e); + } + + contents = + contents.replace( + parcelableWrapper.defaultWrapperClassName().packageName(), + parcelableWrapper.wrapperClassName().packageName()); + contents = + contents.replace( + parcelableWrapper.defaultWrapperClassName().simpleName(), + parcelableWrapper.wrapperClassName().simpleName()); + + JavaFileObject builderFile; + try { + builderFile = + generatorContext + .processingEnv() + .getFiler() + .createSourceFile( + parcelableWrapper.wrapperClassName().packageName() + + "." + + parcelableWrapper.wrapperClassName().simpleName()); + } catch (IOException e) { + throw new IllegalStateException( + "Could not write parcelablewrapper for " + parcelableWrapperSimpleName, e); + } + + try (PrintWriter out = new PrintWriter(builderFile.openWriter())) { + out.write(contents); + } catch (IOException e) { + throw new IllegalStateException( + "Could not write parcelablewrapper for " + parcelableWrapperSimpleName, e); + } + } + + private void generateProtoParcelableWrappers(Collection<ParcelableWrapper> parcelableWrappers) { + Collection<ParcelableWrapper> protoParcelableWrappersToGenerate = + parcelableWrappers.stream() + .filter(f -> f.wrapperType() == WrapperType.PROTO) + .collect(toSet()); + + for (ParcelableWrapper parcelableWrapper : protoParcelableWrappersToGenerate) { + new ProtoParcelableWrapperGenerator(generatorContext, parcelableWrapper).generate(); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java new file mode 100644 index 0000000..4948196 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/Processor.java @@ -0,0 +1,415 @@ +/* + * 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.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfile; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileCallback; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileConfiguration; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileConfigurations; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileProvider; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.elementsAnnotatedWithCrossProfileTest; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileProviderAnnotation; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper; +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTestInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorCrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorProviderClassInfo; +import com.google.auto.service.AutoService; +import java.util.Collection; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Processor for generation of cross-profile code. */ +@SupportedAnnotationTypes({ + "com.google.android.enterprise.connectedapps.annotations.CrossProfile", + "com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback", + "com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration", + "com.google.android.enterprise.connectedapps.annotations.CrossProfileConfigurations", + "com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider", + "com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest", + "com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector", + "com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector", + "com.google.android.enterprise.connectedapps.annotations.CrossUser", + "com.google.android.enterprise.connectedapps.annotations.CrossUserCallback", + "com.google.android.enterprise.connectedapps.annotations.CrossUserConfiguration", + "com.google.android.enterprise.connectedapps.annotations.CrossUserConfigurations", + "com.google.android.enterprise.connectedapps.annotations.CrossUserProvider", + "com.google.android.enterprise.connectedapps.testing.annotations.CrossUserTest", + "com.google.android.enterprise.connectedapps.annotations.CustomUserConnector", + "com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector", + "com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper", + "com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper" +}) +@AutoService(javax.annotation.processing.Processor.class) +public final class Processor extends AbstractProcessor { + + private Types types; + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latest(); + } + + @Override + public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { + Elements elements = processingEnv.getElementUtils(); + types = processingEnv.getTypeUtils(); + + Collection<ValidatorCrossProfileTestInfo> newCrossProfileTests = + findNewCrossProfileTests(roundEnv); + // Only new configurations need code generating - but we need to support types used by methods + // included in configurations under test + Collection<ValidatorCrossProfileConfigurationInfo> newConfigurations = + findNewConfigurations(roundEnv); + Collection<ValidatorCrossProfileConfigurationInfo> allConfigurations = + findAllConfigurations(newConfigurations, newCrossProfileTests); + + Collection<ValidatorProviderClassInfo> newProviderClasses = findNewProviderClasses(roundEnv); + Collection<ExecutableElement> newProviderMethods = findNewProviderMethods(roundEnv); + Collection<TypeElement> newGeneratedConnectors = findNewGeneratedConnectors(roundEnv); + Collection<TypeElement> newGeneratedUserConnectors = findNewGeneratedUserConnectors(roundEnv); + Collection<ExecutableElement> newCrossProfileMethods = findNewCrossProfileMethods(roundEnv); + Collection<ExecutableElement> allCrossProfileMethods = + findAllCrossProfileMethods( + processingEnv, + elements, + newCrossProfileMethods, + allConfigurations, + newProviderMethods, + newProviderClasses); + Collection<TypeElement> newCrossProfileCallbackInterfaces = + findNewCrossProfileCallbackInterfaces(roundEnv); + + Collection<ExecutableElement> methods = new HashSet<>(allCrossProfileMethods); + methods.addAll( + newCrossProfileCallbackInterfaces.stream() + .flatMap(i -> i.getEnclosedElements().stream()) + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind() == ElementKind.METHOD) + .collect(toSet())); + + Collection<TypeElement> newCustomParcelableWrappers = findNewParcelableWrappers(roundEnv); + Collection<TypeElement> newCustomFutureWrappers = findNewFutureWrappers(roundEnv); + + Collection<FutureWrapper> globalFutureWrappers = + FutureWrapper.createGlobalFutureWrappers(elements); + Collection<ParcelableWrapper> globalParcelableWrappers = + ParcelableWrapper.createGlobalParcelableWrappers(types, elements, methods); + + SupportedTypes globalSupportedTypes = + SupportedTypes.createFromMethods( + types, elements, globalParcelableWrappers, globalFutureWrappers, methods); + + Collection<ValidatorCrossProfileTypeInfo> newCrossProfileTypes = + findNewCrossProfileTypes(roundEnv, globalSupportedTypes); + Collection<ProfileConnectorInfo> newProfileConnectorInterfaces = + findNewProfileConnectorInterfaces(roundEnv, globalSupportedTypes); + Collection<UserConnectorInfo> newUserConnectorInterfaces = + findNewUserConnectorInterfaces(roundEnv, globalSupportedTypes); + + ValidatorContext validatorContext = + ValidatorContext.builder() + .setProcessingEnv(processingEnv) + .setElements(elements) + .setTypes(types) + .setGlobalSupportedTypes(globalSupportedTypes) + .setNewProfileConnectorInterfaces(newProfileConnectorInterfaces) + .setNewUserConnectorInterfaces(newUserConnectorInterfaces) + .setNewGeneratedProfileConnectors(newGeneratedConnectors) + .setNewGeneratedUserConnectors(newGeneratedUserConnectors) + .setNewConfigurations(newConfigurations) + .setNewCrossProfileTypes(newCrossProfileTypes) + .setNewCrossProfileMethods(newCrossProfileMethods) + .setNewProviderClasses(newProviderClasses) + .setNewProviderMethods(newProviderMethods) + .setNewCrossProfileCallbackInterfaces(newCrossProfileCallbackInterfaces) + .setNewCrossProfileTests(newCrossProfileTests) + .setNewCustomParcelableWrappers(newCustomParcelableWrappers) + .setNewCustomFutureWrappers(newCustomFutureWrappers) + .build(); + + boolean isValid = new EarlyValidator(validatorContext).validate(); + + if (!isValid) { + return false; + } + + GeneratorContext generatorContext = + GeneratorContext.createFromValidatorContext(validatorContext); + + isValid = new LateValidator(generatorContext).validate(); + + if (!isValid) { + return false; + } + + new CodeGenerator(generatorContext).generate(); + + return false; + } + + private Collection<ValidatorCrossProfileConfigurationInfo> findNewConfigurations( + RoundEnvironment roundEnv) { + Set<ValidatorCrossProfileConfigurationInfo> annotations = new HashSet<>(); + + elementsAnnotatedWithCrossProfileConfiguration(roundEnv) + .map( + element -> + ValidatorCrossProfileConfigurationInfo.createFromElement( + processingEnv, (TypeElement) element)) + .forEach(annotations::add); + + elementsAnnotatedWithCrossProfileConfigurations(roundEnv) + .map( + element -> + ValidatorCrossProfileConfigurationInfo.createMultipleFromElement( + processingEnv, (TypeElement) element)) + .forEach(annotations::addAll); + + return annotations; + } + + private Collection<ValidatorCrossProfileConfigurationInfo> findAllConfigurations( + Collection<ValidatorCrossProfileConfigurationInfo> newConfigurations, + Collection<ValidatorCrossProfileTestInfo> crossProfileTests) { + Set<ValidatorCrossProfileConfigurationInfo> allConfigurations = new HashSet<>(); + allConfigurations.addAll(newConfigurations); + allConfigurations.addAll( + crossProfileTests.stream() + .flatMap( + t -> + ValidatorCrossProfileConfigurationInfo.createMultipleFromElement( + processingEnv, t.configurationElement()) + .stream()) + .collect(toSet())); + return allConfigurations; + } + + private Collection<ValidatorProviderClassInfo> findNewProviderClasses(RoundEnvironment roundEnv) { + Set<ValidatorProviderClassInfo> annotatedClasses = + elementsAnnotatedWithCrossProfileProvider(roundEnv) + .filter(m -> m instanceof TypeElement) + .map(m -> (TypeElement) m) + .map(m -> ValidatorProviderClassInfo.create(processingEnv, m)) + .collect(toSet()); + + Set<ValidatorProviderClassInfo> unannotatedClasses = + elementsAnnotatedWithCrossProfileProvider(roundEnv) + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .map(Element::getEnclosingElement) + .map(m -> (TypeElement) m) + .filter(m -> !hasCrossProfileProviderAnnotation(m)) + .map(m -> ValidatorProviderClassInfo.create(processingEnv, m)) + .collect(toSet()); + + Collection<ValidatorProviderClassInfo> allProviders = new HashSet<>(); + allProviders.addAll(annotatedClasses); + allProviders.addAll(unannotatedClasses); + return allProviders; + } + + private Collection<ExecutableElement> findNewProviderMethods(RoundEnvironment roundEnv) { + return elementsAnnotatedWithCrossProfileProvider(roundEnv) + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .collect(toSet()); + } + + private Collection<ValidatorCrossProfileTypeInfo> findNewCrossProfileTypes( + RoundEnvironment roundEnv, SupportedTypes globalSupportedTypes) { + Collection<ValidatorCrossProfileTypeInfo> annotatedTypes = + elementsAnnotatedWithCrossProfile(roundEnv) + .filter(m -> m instanceof TypeElement) + .map(m -> (TypeElement) m) + .map(m -> ValidatorCrossProfileTypeInfo.create(processingEnv, m, globalSupportedTypes)) + .collect(toSet()); + + Collection<ValidatorCrossProfileTypeInfo> unannotatedTypes = + elementsAnnotatedWithCrossProfile(roundEnv) + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .map(ExecutableElement::getEnclosingElement) + .filter(m -> m instanceof TypeElement) + .map(m -> (TypeElement) m) + .map(m -> ValidatorCrossProfileTypeInfo.create(processingEnv, m, globalSupportedTypes)) + .collect(toSet()); + + Collection<ValidatorCrossProfileTypeInfo> allTypes = new HashSet<>(); + allTypes.addAll(annotatedTypes); + allTypes.addAll(unannotatedTypes); + return allTypes; + } + + private Collection<ExecutableElement> findNewCrossProfileMethods(RoundEnvironment roundEnv) { + return elementsAnnotatedWithCrossProfile(roundEnv) + .filter(m -> m instanceof ExecutableElement) + .map(m -> (ExecutableElement) m) + .collect(toSet()); + } + + private Collection<ProfileConnectorInfo> findNewProfileConnectorInterfaces( + RoundEnvironment roundEnv, SupportedTypes globalSupportedTypes) { + Collection<TypeElement> connectorInterfaces = + roundEnv.getElementsAnnotatedWith(CustomProfileConnector.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + + // We manually add the SDK-provided CrossProfileConnector as it won't be detected by roundEnv + connectorInterfaces.add( + processingEnv + .getElementUtils() + .getTypeElement("com.google.android.enterprise.connectedapps.CrossProfileConnector")); + + return connectorInterfaces.stream() + .map(t -> ProfileConnectorInfo.create(processingEnv, t, globalSupportedTypes)) + .collect(Collectors.toSet()); + } + + private Collection<UserConnectorInfo> findNewUserConnectorInterfaces( + RoundEnvironment roundEnv, SupportedTypes globalSupportedTypes) { + Collection<TypeElement> connectorInterfaces = + roundEnv.getElementsAnnotatedWith(CustomUserConnector.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + + // We manually add the SDK-provided CrossUserConnector as it won't be detected by roundEnv + connectorInterfaces.add( + processingEnv + .getElementUtils() + .getTypeElement("com.google.android.enterprise.connectedapps.CrossUserConnector")); + + return connectorInterfaces.stream() + .map(t -> UserConnectorInfo.create(processingEnv, t, globalSupportedTypes)) + .collect(Collectors.toSet()); + } + + private Collection<TypeElement> findNewGeneratedConnectors(RoundEnvironment roundEnv) { + Collection<TypeElement> connectorInterfaces = + roundEnv.getElementsAnnotatedWith(GeneratedProfileConnector.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + + return connectorInterfaces; + } + + private Collection<TypeElement> findNewGeneratedUserConnectors(RoundEnvironment roundEnv) { + Collection<TypeElement> connectorInterfaces = + roundEnv.getElementsAnnotatedWith(GeneratedUserConnector.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + + return connectorInterfaces; + } + + private static Collection<ExecutableElement> findAllCrossProfileMethods( + ProcessingEnvironment processingEnvironment, + Elements elements, + Collection<ExecutableElement> newCrossProfileMethods, + Collection<ValidatorCrossProfileConfigurationInfo> configurations, + Collection<ExecutableElement> newProviderMethods, + Collection<ValidatorProviderClassInfo> newProviderClasses) { + Collection<ExecutableElement> allCrossProfileMethods = new HashSet<>(newCrossProfileMethods); + + Collection<ValidatorProviderClassInfo> foundProviderClasses = + configurations.stream() + .flatMap(a -> a.providerClassElements().stream()) + .map(m -> ValidatorProviderClassInfo.create(processingEnvironment, m)) + .collect(toSet()); + + Collection<ExecutableElement> providerMethods = + foundProviderClasses.stream() + .flatMap( + m -> + GeneratorUtilities.findCrossProfileProviderMethodsInClass( + m.providerClassElement()) + .stream()) + .collect(toSet()); + + providerMethods.addAll(newProviderMethods); + + Collection<TypeElement> crossProfileTypes = + providerMethods.stream() + .map(e -> elements.getTypeElement(e.getReturnType().toString())) + .filter(Objects::nonNull) + .collect(toSet()); + crossProfileTypes.addAll( + foundProviderClasses.stream().flatMap(m -> m.staticTypes().stream()).collect(toSet())); + crossProfileTypes.addAll( + newProviderClasses.stream().flatMap(m -> m.staticTypes().stream()).collect(toSet())); + + Collection<ExecutableElement> foundCrossProfileMethods = + crossProfileTypes.stream() + .flatMap(t -> GeneratorUtilities.findCrossProfileMethodsInClass(t).stream()) + .collect(toSet()); + + allCrossProfileMethods.addAll(foundCrossProfileMethods); + return allCrossProfileMethods; + } + + private Collection<ValidatorCrossProfileTestInfo> findNewCrossProfileTests( + RoundEnvironment roundEnv) { + return elementsAnnotatedWithCrossProfileTest(roundEnv) + .map(e -> (TypeElement) e) + .map(e -> ValidatorCrossProfileTestInfo.create(processingEnv, e)) + .collect(toSet()); + } + + private Collection<TypeElement> findNewCrossProfileCallbackInterfaces(RoundEnvironment roundEnv) { + return elementsAnnotatedWithCrossProfileCallback(roundEnv) + .map(m -> (TypeElement) m) + .collect(toSet()); + } + + private Collection<TypeElement> findNewParcelableWrappers(RoundEnvironment roundEnv) { + return roundEnv.getElementsAnnotatedWith(CustomParcelableWrapper.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + } + + private Collection<TypeElement> findNewFutureWrappers(RoundEnvironment roundEnv) { + return roundEnv.getElementsAnnotatedWith(CustomFutureWrapper.class).stream() + .map(m -> (TypeElement) m) + .collect(toSet()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java new file mode 100644 index 0000000..75d3c12 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProcessorConfiguration.java @@ -0,0 +1,30 @@ +/* + * 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; + +/** General configuration. */ +public final class ProcessorConfiguration { + private ProcessorConfiguration() {} + + /** + * When {@code true}, will generate a copy of each parcelable and future wrapper for each type + * which requires it. + * + * <p>This is required to ensure that there are no conflicts due to duplicate classes being + * generated in separate targets. + */ + public static final boolean GENERATE_TYPE_SPECIFIC_WRAPPERS = true; +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java new file mode 100644 index 0000000..68c14f2 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProfileConnectorCodeGenerator.java @@ -0,0 +1,189 @@ +/* + * 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_PROFILE_CONNECTOR_BUILDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_PROFILE_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.AVAILABILITY_RESTRICTIONS_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONNECTION_BINDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.SCHEDULED_EXECUTOR_SERVICE_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.annotations.GeneratedProfileConnector; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code Generated*} class for a single {@link GeneratedProfileConnector} annotated + * class. + * + * <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. + */ +class ProfileConnectorCodeGenerator { + private boolean generated = false; + + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final ProfileConnectorInfo connector; + + ProfileConnectorCodeGenerator(GeneratorContext generatorContext, ProfileConnectorInfo connector) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.connector = checkNotNull(connector); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ProfileConnectorCodeGenerator#generate can only be called once"); + } + generated = true; + + generateProfileConnector(); + } + + private void generateProfileConnector() { + ClassName className = getGeneratedProfileConnectorClassName(generatorContext, connector); + ClassName builderClassName = + getGeneratedProfileConnectorBuilderClassName(generatorContext, connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Generated implementation of {@link $T}.\n\n" + + "<p>All logic is implemented by {@link $T}.\n", + connector.connectorClassName(), + ABSTRACT_PROFILE_CONNECTOR_CLASSNAME) + .addModifiers(Modifier.FINAL) + .addSuperinterface(connector.connectorClassName()) + .superclass(ABSTRACT_PROFILE_CONNECTOR_CLASSNAME); + + classBuilder.addMethod( + MethodSpec.methodBuilder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(builderClassName) + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("return new $T(context)", builderClassName) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(builderClassName, "builder") + .addStatement( + "super($1T.class, builder.profileConnectorBuilder)", connector.connectorClassName()) + .build()); + + generateProfileConnectorBuilder(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateProfileConnectorBuilder(TypeSpec.Builder profileConnector) { + ClassName profileConnectorClassName = + getGeneratedProfileConnectorClassName(generatorContext, connector); + ClassName builderClassName = + getGeneratedProfileConnectorBuilderClassName(generatorContext, connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(builderClassName) + .addJavadoc("Builder for {@link $T}.\n", profileConnectorClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); + + CodeBlock initialiser = + CodeBlock.of( + "new $T().setServiceClassName($S).setAvailabilityRestrictions($T.$L)", + ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME, + connector.serviceName().toString(), + AVAILABILITY_RESTRICTIONS_CLASSNAME, + connector.availabilityRestrictions().name()); + + if (connector.primaryProfile() != ProfileType.NONE) { + initialiser = + CodeBlock.of( + "$L.setPrimaryProfileType($T.$L)", + initialiser, + ProfileType.class, + connector.primaryProfile().name()); + } + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("profileConnectorBuilder.setContext(context)") + .build()); + + classBuilder.addField( + FieldSpec.builder(ABSTRACT_PROFILE_CONNECTOR_BUILDER_CLASSNAME, "profileConnectorBuilder") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .initializer(initialiser) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("setScheduledExecutorService") + .addModifiers(Modifier.PUBLIC) + .addParameter(SCHEDULED_EXECUTOR_SERVICE_CLASSNAME, "scheduledExecutorService") + .returns(builderClassName) + .addStatement( + "profileConnectorBuilder.setScheduledExecutorService(scheduledExecutorService)") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("setBinder") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONNECTION_BINDER_CLASSNAME, "binder") + .returns(builderClassName) + .addStatement("profileConnectorBuilder.setBinder(binder)") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("build") + .addModifiers(Modifier.PUBLIC) + .returns(profileConnectorClassName) + .addStatement("return new $1T(this)", profileConnectorClassName) + .build()); + + profileConnector.addType(classBuilder.build()); + } + + static ClassName getGeneratedProfileConnectorClassName( + GeneratorContext generatorContext, ProfileConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName(), + "Generated" + connector.connectorClassName().simpleName()); + } + + static ClassName getGeneratedProfileConnectorBuilderClassName( + GeneratorContext generatorContext, ProfileConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName() + + "." + + "Generated" + + connector.connectorClassName().simpleName(), + "Builder"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java new file mode 100644 index 0000000..302b420 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProtoParcelableWrapperGenerator.java @@ -0,0 +1,171 @@ +/* + * 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.BUNDLER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.BUNDLER_TYPE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.INVALID_PROTOCOL_BUFFER_EXCEPTION_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCELABLE_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.PARCEL_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper.PARCELABLE_WRAPPER_PACKAGE; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; + +/** + * Generate the Parcelable Wrapper for a single Proto. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <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. + */ +public final class ProtoParcelableWrapperGenerator { + + private static final String GENERATED_PARCELABLE_WRAPPER_PACKAGE = + PARCELABLE_WRAPPER_PACKAGE + ".generated"; + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final ParcelableWrapper parcelableWrapper; + + ProtoParcelableWrapperGenerator( + GeneratorContext generatorContext, ParcelableWrapper parcelableWrapper) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.parcelableWrapper = checkNotNull(parcelableWrapper); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ProtoParcelableWrapperGenerator#generate can only be called once"); + } + generated = true; + + generateProtoParcelableWrapper(); + } + + private void generateProtoParcelableWrapper() { + ClassName wrapperClassName = parcelableWrapper.wrapperClassName(); + + if (generatorContext.elements().getTypeElement(wrapperClassName.toString()) != null) { + // We don't generate things which already exist + return; + } + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(wrapperClassName) + .addModifiers(Modifier.PUBLIC) + .addSuperinterface(PARCELABLE_CLASSNAME) + .addJavadoc( + "Wrapper for reading & writing {@link $T} instances to and from {@link $T}" + + " instances.", + parcelableWrapper.wrappedType(), + PARCEL_CLASSNAME); + + classBuilder.addField( + FieldSpec.builder(ClassName.get(parcelableWrapper.wrappedType()), "proto", Modifier.PRIVATE) + .build()); + + classBuilder.addField( + FieldSpec.builder(int.class, "NULL_SIZE") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL, Modifier.STATIC) + .initializer("-1") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("of") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addJavadoc( + "Create a wrapper for the given {@link $T}.\n", parcelableWrapper.wrappedType()) + .returns(parcelableWrapper.wrapperClassName()) + .addParameter(BUNDLER_CLASSNAME, "bundler") + .addParameter(BUNDLER_TYPE_CLASSNAME, "type") + .addParameter(ClassName.get(parcelableWrapper.wrappedType()), "proto") + .addStatement( + "return new $T(bundler, type, proto)", parcelableWrapper.wrapperClassName()) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("get") + .addModifiers(Modifier.PUBLIC) + .returns(ClassName.get(parcelableWrapper.wrappedType())) + .addStatement("return proto") + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(BUNDLER_CLASSNAME, "bundler") + .addParameter(BUNDLER_TYPE_CLASSNAME, "type") + .addParameter(ClassName.get(parcelableWrapper.wrappedType()), "proto") + .addStatement("this.proto = proto") + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(PARCEL_CLASSNAME, "in") + .addStatement("int size = in.readInt()") + .beginControlFlow("if (size == NULL_SIZE)") + .addStatement("proto = null") + .addStatement("return") + .endControlFlow() + .addStatement("byte[] protoBytes = new byte[size]") + .addStatement("in.readByteArray(protoBytes)") + .beginControlFlow("try") + .addStatement("proto = $T.parseFrom(protoBytes)", parcelableWrapper.wrappedType()) + .nextControlFlow("catch ($T e)", INVALID_PROTOCOL_BUFFER_EXCEPTION_CLASSNAME) + .addComment("TODO: Deal with exception") + .endControlFlow() + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("writeToParcel") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(PARCEL_CLASSNAME, "dest") + .addParameter(int.class, "flags") + .beginControlFlow("if (proto == null)") + .addStatement("dest.writeInt(NULL_SIZE)") + .addStatement("return") + .endControlFlow() + .addStatement("byte[] protoBytes = proto.toByteArray()") + .addStatement("dest.writeInt(protoBytes.length)") + .addStatement("dest.writeByteArray(protoBytes)") + .build()); + + generatorUtilities.addDefaultParcelableMethods( + classBuilder, parcelableWrapper.wrapperClassName()); + + generatorUtilities.writeClassToFile( + parcelableWrapper.wrapperClassName().packageName(), classBuilder); + } + + public static ClassName getGeneratedProtoWrapperClassName(TypeMirror type) { + String simpleName = type.toString().substring(type.toString().lastIndexOf(".") + 1); + return ClassName.get(GENERATED_PARCELABLE_WRAPPER_PACKAGE, simpleName + "Wrapper"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java new file mode 100644 index 0000000..06f6b71 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ProviderClassCodeGenerator.java @@ -0,0 +1,53 @@ +/* + * 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.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; + +/** Generator of code for a single provider class. */ +class ProviderClassCodeGenerator { + private boolean generated = false; + + private final GeneratorContext generatorContext; + private final InternalProviderClassGenerator internalProviderClassGenerator; + private final ProviderClassInfo providerClass; + + ProviderClassCodeGenerator(GeneratorContext generatorContext, ProviderClassInfo providerClass) { + this.generatorContext = checkNotNull(generatorContext); + this.providerClass = checkNotNull(providerClass); + this.internalProviderClassGenerator = + new InternalProviderClassGenerator(generatorContext, providerClass); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ProviderClassCodeGenerator#generate can only be called once"); + } + generated = true; + + internalProviderClassGenerator.generate(); + + for (CrossProfileTypeInfo crossProfileType : providerClass.allCrossProfileTypes()) { + new CrossProfileTypeCodeGenerator(generatorContext, providerClass, crossProfileType) + .generate(); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java new file mode 100644 index 0000000..28fa128 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ServiceGenerator.java @@ -0,0 +1,178 @@ +/* + * 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.BINDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSSPROFILESERVICE_STUB_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CROSS_PROFILE_CALLBACK_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.INTENT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.DispatcherGenerator.getDispatcherClassName; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.squareup.javapoet.ArrayTypeName; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code *_Service} class for a single {@link CrossProfileConfiguration} annotated + * class. + * + * <p>This class includes the dispatch of calls to providers. + * + * <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 ServiceGenerator { + + private boolean generated = false; + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final CrossProfileConfigurationInfo configuration; + + ServiceGenerator(GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.configuration = checkNotNull(configuration); + } + + void generate() { + if (generated) { + throw new IllegalStateException("ServiceGenerator#generate can only be called once"); + } + generated = true; + + if (configuration.serviceClass().isPresent()) { + // Using a pre-existing service + return; + } + + generateServiceClass(); + } + + private void generateServiceClass() { + ClassName className = getConnectedAppsServiceClassName(generatorContext, configuration); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .superclass(configuration.serviceSuperclass()) + .addJavadoc( + "Generated Service for {@link $T}\n\n" + + "<p>This is bound to by {@link $T} to make cross-profile calls.\n\n" + + "<p>This primarily forwards calls to {@link $T}\n\n" + + "<p>This service must be exposed in a <service> tag in your" + + " AndroidManifest.xml\n", + configuration.configurationElement(), + configuration.profileConnector().connectorClassName(), + getDispatcherClassName(generatorContext, configuration)); + + addBinder(classBuilder); + + classBuilder.addMethod( + MethodSpec.methodBuilder("onBind") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .returns(BINDER_CLASSNAME) + .addParameter(INTENT_CLASSNAME, "intent") + .addStatement("return binder") + .build()); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void addBinder(TypeSpec.Builder classBuilder) { + TypeSpec.Builder binderBuilder = + TypeSpec.anonymousClassBuilder("") + .addSuperinterface(CROSSPROFILESERVICE_STUB_CLASSNAME) + .addField( + FieldSpec.builder( + getDispatcherClassName(generatorContext, configuration), "dispatcher") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .initializer( + "new $T()", getDispatcherClassName(generatorContext, configuration)) + .build()); + + addPrepareCallMethod(binderBuilder); + addCallMethod(binderBuilder); + addFetchResponseMethod(binderBuilder); + + classBuilder.addField( + FieldSpec.builder(CROSSPROFILESERVICE_STUB_CLASSNAME, "binder", Modifier.PRIVATE) + .initializer("$L", binderBuilder.build()) + .build()); + } + + private static void addPrepareCallMethod(TypeSpec.Builder classBuilder) { + MethodSpec prepareCallMethod = + MethodSpec.methodBuilder("prepareCall") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .addParameter(int.class, "numBytes") + .addParameter(ArrayTypeName.of(byte.class), "paramBytes") + .addStatement( + "dispatcher.prepareCall(getApplicationContext(), callId, blockId, numBytes," + + " paramBytes)") + .build(); + classBuilder.addMethod(prepareCallMethod); + } + + private static void addCallMethod(TypeSpec.Builder classBuilder) { + MethodSpec callMethod = + MethodSpec.methodBuilder("call") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(ArrayTypeName.of(byte.class)) + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .addParameter(long.class, "crossProfileTypeIdentifier") + .addParameter(int.class, "methodIdentifier") + .addParameter(ArrayTypeName.of(byte.class), "paramBytes") + .addParameter(CROSS_PROFILE_CALLBACK_CLASSNAME, "callback") + .addStatement( + "return dispatcher.call(getApplicationContext()," + + "callId, blockId, crossProfileTypeIdentifier, methodIdentifier, paramBytes," + + " callback)") + .build(); + classBuilder.addMethod(callMethod); + } + + private static void addFetchResponseMethod(TypeSpec.Builder classBuilder) { + MethodSpec prepareCallMethod = + MethodSpec.methodBuilder("fetchResponse") + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .addParameter(long.class, "callId") + .addParameter(int.class, "blockId") + .returns(ArrayTypeName.of(byte.class)) + .addStatement( + "return dispatcher.fetchResponse(getApplicationContext(), callId, blockId)") + .build(); + classBuilder.addMethod(prepareCallMethod); + } + + static ClassName getConnectedAppsServiceClassName( + GeneratorContext generatorContext, CrossProfileConfigurationInfo configuration) { + return configuration.profileConnector().serviceName(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java new file mode 100644 index 0000000..1238b4b --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/SupportedTypes.java @@ -0,0 +1,844 @@ +/* + * 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.annotationdiscovery.AnnotationFinder.hasCrossProfileCallbackAnnotation; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackInterfaceInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileMethodInfo; +import com.google.android.enterprise.connectedapps.processor.containers.FutureWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.ParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.containers.Type; +import com.google.android.enterprise.connectedapps.processor.containers.ValidatorContext; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableMap; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Utility methods for generating code related to valid types for use with the Connected Apps SDK. + */ +public final class SupportedTypes { + + @Override + public String toString() { + return "SupportedTypes{" + + "usableTypes=" + usableTypes + + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SupportedTypes that = (SupportedTypes) o; + return usableTypes.equals(that.usableTypes); + } + + @Override + public int hashCode() { + return Objects.hash(usableTypes); + } + + /** Record of the current context for type checking. */ + @AutoValue + public abstract static class TypeCheckContext { + + /** True if we are checking inside a generic type or an array. */ + public abstract boolean isWrapped(); + + public abstract boolean isOnCrossProfileCallbackInterface(); + + public abstract Builder toBuilder(); + + public static TypeCheckContext create() { + return new AutoValue_SupportedTypes_TypeCheckContext.Builder() + .setWrapped(false) + .setOnCrossProfileCallbackInterface(false) + .build(); + } + + public static TypeCheckContext createForCrossProfileCallbackInterface() { + return new AutoValue_SupportedTypes_TypeCheckContext.Builder() + .setWrapped(false) + .setOnCrossProfileCallbackInterface(true) + .build(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setWrapped(boolean wrapped); + + abstract Builder setOnCrossProfileCallbackInterface(boolean onCrossProfileCallbackInterface); + + abstract TypeCheckContext build(); + } + } + + private final ImmutableMap<String, Type> usableTypes; + + public boolean isFuture(TypeMirror type) { + Type supportedType = get(type); + return supportedType != null && supportedType.isFuture(); + } + + boolean isValidReturnType(TypeMirror type) { + return isValidReturnType(type, TypeCheckContext.create()); + } + + private boolean isValidReturnType(TypeMirror type, TypeCheckContext context) { + if (TypeUtils.isArray(type)) { + TypeMirror wrappedType = TypeUtils.extractTypeFromArray(type); + if (TypeUtils.isGeneric(wrappedType)) { + return false; // We don't support generic arrays + } + if (wrappedType.getKind().isPrimitive()) { + return false; // We don't support primitive arrays + } + if (TypeUtils.isArray(wrappedType)) { + return false; // We don't support multidimensional arrays + } + return isValidReturnType(wrappedType, context); + } + + return TypeUtils.isGeneric(type) + ? isValidGenericReturnType(type, context) + : isValidReturnType(get(type), context); + } + + private static boolean isValidReturnType(@Nullable Type supportedType, TypeCheckContext context) { + if (supportedType == null) { + return false; + } + + if (context.isWrapped() && !supportedType.isSupportedInsideWrapper()) { + return false; + } + + return supportedType.isAcceptableReturnType(); + } + + private boolean isValidGenericReturnType(TypeMirror type, TypeCheckContext context) { + TypeMirror genericType = TypeUtils.removeTypeArguments(type); + Type supportedType = get(genericType); + + if (supportedType == null) { + return false; + } + + if (!supportedType.isSupportedWithAnyGenericType()) { + // We need to recursively check all type arguments + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) { + if (!isValidReturnType(typeArgument, context.toBuilder().setWrapped(true).build())) { + return false; + } + } + } + + return isValidReturnType(supportedType, context); + } + + /** + * Returns true if this type is automatically resolved. + * + * <p>An automatically resolved type does not need to have a value provided by the developer at + * runtime, and should instead use the value of + * {@link #getAutomaticallyResolvedReplacement(TypeMirror)}. + */ + public boolean isAutomaticallyResolved(TypeMirror type) { + Type supportedType = get(type); + return supportedType != null && supportedType.getAutomaticallyResolvedReplacement().isPresent(); + } + + public String getAutomaticallyResolvedReplacement(TypeMirror type) { + Type supportedType = get(type); + return supportedType.getAutomaticallyResolvedReplacement().get(); + } + + boolean isValidParameterType(TypeMirror type) { + return isValidParameterType(type, TypeCheckContext.create()); + } + + boolean isValidParameterType(TypeMirror type, TypeCheckContext context) { + if (TypeUtils.isArray(type)) { + TypeMirror wrappedType = TypeUtils.extractTypeFromArray(type); + if (TypeUtils.isGeneric(wrappedType)) { + return false; // We don't support generic arrays + } + if (wrappedType.getKind().isPrimitive()) { + return false; // We don't support primitive arrays + } + if (TypeUtils.isArray(wrappedType)) { + return false; // We don't support multidimensional arrays + } + return isValidParameterType(wrappedType, context.toBuilder().setWrapped(true).build()); + } + + Type supportedType = get(TypeUtils.removeTypeArguments(type)); + if (context.isOnCrossProfileCallbackInterface()) { + if (supportedType != null && !supportedType.isSupportedInsideCrossProfileCallback()) { + return false; + } + } + + if (context.isWrapped()) { + if (supportedType == null || !supportedType.isSupportedInsideWrapper()) { + return false; + } + } + + return TypeUtils.isGeneric(type) + ? isValidGenericParameterType(type, context) + : isValidParameterType(get(type)); + } + + private static boolean isValidParameterType(Type supportedType) { + return supportedType != null && supportedType.isAcceptableParameterType(); + } + + private boolean isValidGenericParameterType(TypeMirror type, TypeCheckContext context) { + TypeMirror genericType = TypeUtils.removeTypeArguments(type); + Type supportedType = get(genericType); + + if (supportedType == null) { + return false; + } + + if (!supportedType.isSupportedWithAnyGenericType()) { + // We need to recursively check all type arguments + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) { + if (!isValidParameterType(typeArgument, context.toBuilder().setWrapped(true).build())) { + return false; + } + } + } + + return isValidParameterType(supportedType); + } + + ImmutableCollection<Type> usableTypes() { + return usableTypes.values(); + } + + private Type get(TypeMirror type) { + return usableTypes.getOrDefault(type.toString(), null); + } + + CodeBlock generateWriteToParcelCode(String parcelName, Type type, String valueCode) { + if (type.getWriteToParcelCode().isPresent()) { + return CodeBlock.of(type.getWriteToParcelCode().get(), parcelName, valueCode); + } + + throw new IllegalArgumentException( + String.format("%s can not write to parcel", type.getQualifiedName())); + } + + CodeBlock generateReadFromParcelCode(String parcelName, Type type) { + if (type.getReadFromParcelCode().isPresent()) { + return CodeBlock.of(type.getReadFromParcelCode().get(), parcelName); + } + + throw new IllegalArgumentException( + String.format("%s can not read from parcel", type.getQualifiedName())); + } + + public Type getType(TypeMirror type) { + String typeName = type.toString(); + if (!usableTypes.containsKey(typeName)) { + throw new IllegalArgumentException(String.format("%s type not loaded", type)); + } + + return get(type); + } + + private SupportedTypes(Map<String, Type> usableTypes) { + this.usableTypes = ImmutableMap.copyOf(usableTypes); + } + + public static SupportedTypes createFromMethods( + Types types, + Elements elements, + Collection<ParcelableWrapper> parcelableWrappers, + Collection<FutureWrapper> futureWrappers, + Collection<ExecutableElement> methods) { + Map<String, Type> usableTypes = new HashMap<>(); + + addDefaultTypes(types, elements, usableTypes); + addParcelableWrapperTypes(usableTypes, parcelableWrappers); + addFutureWrapperTypes(usableTypes, futureWrappers); + addSupportForUsedTypes(types, elements, usableTypes, methods); + + return new SupportedTypes(usableTypes); + } + + private static void addSupportForUsedTypes( + Types types, + Elements elements, + Map<String, Type> usableTypes, + Collection<ExecutableElement> methods) { + for (ExecutableElement method : methods) { + addSupportForUsedType(types, elements, usableTypes, method.getReturnType()); + + for (VariableElement parameter : method.getParameters()) { + addSupportForUsedType(types, elements, usableTypes, parameter.asType()); + } + } + } + + private static void addSupportForUsedType( + Types types, Elements elements, Map<String, Type> usableTypes, TypeMirror type) { + if (TypeUtils.isArray(type)) { + addSupportForUsedType(types, elements, usableTypes, TypeUtils.extractTypeFromArray(type)); + if (!TypeUtils.extractTypeFromArray(type).getKind().isPrimitive()) { + type = types.getArrayType(elements.getTypeElement("java.lang.Object").asType()); + } + } + + + if (TypeUtils.isGeneric(type)) { + addSupportForGenericUsedType(types, elements, usableTypes, type); + return; + } + Optional<Type> optionalSupportedType = getSupportedType(types, elements, usableTypes, type); + if (!optionalSupportedType.isPresent()) { + // The type isn't supported + return; + } + + Type supportedType = optionalSupportedType.get(); + + // We don't support generic callbacks so any callback interfaces can be picked up here + if (supportedType.isCrossProfileCallbackInterface()) { + for (TypeMirror typeMirror : + supportedType.getCrossProfileCallbackInterface().get().argumentTypes()) { + addSupportForUsedType(types, elements, usableTypes, typeMirror); + } + } + + addUsableType(usableTypes, supportedType); + } + + private static void addSupportForGenericUsedType( + Types types, Elements elements, Map<String, Type> usableTypes, TypeMirror type) { + TypeMirror genericType = TypeUtils.removeTypeArguments(type); + + Optional<Type> optionalSupportedType = + getSupportedType(types, elements, usableTypes, genericType); + if (!optionalSupportedType.isPresent()) { + // The base type isn't supported + return; + } + + Type supportedType = optionalSupportedType.get(); + + addUsableType(usableTypes, supportedType); + + if (!supportedType.isSupportedWithAnyGenericType()) { + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) { + addSupportForUsedType(types, elements, usableTypes, typeArgument); + } + } + } + + private static Optional<Type> getSupportedType( + Types types, Elements elements, Map<String, Type> usableTypes, TypeMirror type) { + if (usableTypes.containsKey(type.toString())) { + return Optional.of(usableTypes.get(type.toString())); + } + + TypeMirror parcelable = elements.getTypeElement("android.os.Parcelable").asType(); + if (types.isAssignable(type, parcelable)) { + return Optional.of(createParcelableType(type)); + } + + TypeMirror serializable = elements.getTypeElement("java.io.Serializable").asType(); + if (types.isAssignable(type, serializable)) { + return Optional.of(createSerializableType(type)); + } + + TypeElement element = elements.getTypeElement(type.toString()); + + if (element != null && hasCrossProfileCallbackAnnotation(element)) { + return Optional.of(createCrossProfileCallbackType(element)); + } + + // We don't support this type - it will error in a later stage + return Optional.empty(); + } + + private static Type createCrossProfileCallbackType(TypeElement type) { + return Type.builder() + .setTypeMirror(type.asType()) + .setAcceptableReturnType(false) + .setAcceptableParameterType(true) + .setSupportedInsideWrapper(false) + .setSupportedInsideCrossProfileCallback(false) + .setCrossProfileCallbackInterface(CrossProfileCallbackInterfaceInfo.create(type)) + .build(); + } + + private static Type createParcelableType(TypeMirror typeMirror) { + return Type.builder() + .setTypeMirror(typeMirror) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeParcelable($L, flags)") + .setReadFromParcelCode("$L.readParcelable(Bundler.class.getClassLoader())") + // Parcelables must take care of their own generic types + .setSupportedWithAnyGenericType(true) + .build(); + } + + private static Type createSerializableType(TypeMirror typeMirror) { + return Type.builder() + .setTypeMirror(typeMirror) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeSerializable($L)") + .setReadFromParcelCode("$L.readSerializable()") + // Serializables must take care of their own generic types + .setSupportedWithAnyGenericType(true) + .build(); + } + + /** Create a {@link Builder} to create a new {@link SupportedTypes} with modified entries. */ + public Builder asBuilder() { + return new Builder(usableTypes); + } + + private static void addDefaultTypes( + Types types, Elements elements, Map<String, Type> usableTypes) { + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getNoType(TypeKind.VOID)) + .setAcceptableReturnType(true) + .setReadFromParcelCode("null") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(elements.getTypeElement("java.lang.Void").asType()) + .setAcceptableReturnType(true) + .setReadFromParcelCode("null") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(elements.getTypeElement("java.lang.String").asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeString($L)") + .setReadFromParcelCode("$L.readString()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.BYTE)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeByte($L)") + .setReadFromParcelCode("$L.readByte()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.BYTE)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeByte($L)") + .setReadFromParcelCode("$L.readByte()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.SHORT)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("(short)$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.SHORT)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("(short)$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.INT)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.INT)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.LONG)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeLong($L)") + .setReadFromParcelCode("$L.readLong()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.LONG)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeLong($L)") + .setReadFromParcelCode("$L.readLong()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.FLOAT)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeFloat($L)") + .setReadFromParcelCode("$L.readFloat()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.FLOAT)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeFloat($L)") + .setReadFromParcelCode("$L.readFloat()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.DOUBLE)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeDouble($L)") + .setReadFromParcelCode("$L.readDouble()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.DOUBLE)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeDouble($L)") + .setReadFromParcelCode("$L.readDouble()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.CHAR)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("(char)$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.CHAR)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L)") + .setReadFromParcelCode("(char)$L.readInt()") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.getPrimitiveType(TypeKind.BOOLEAN)) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L ? 1 : 0)") + .setReadFromParcelCode("($L.readInt() == 1)") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(types.boxedClass(types.getPrimitiveType(TypeKind.BOOLEAN)).asType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeInt($L ? 1 : 0)") + .setReadFromParcelCode("($L.readInt() == 1)") + .build()); + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(elements.getTypeElement("android.content.Context").asType()) + .setAcceptableParameterType(true) + .setAutomaticallyResolvedReplacement("context") + .setAcceptableReturnType(false) + .setSupportedInsideWrapper(false) + .setSupportedInsideCrossProfileCallback(false) + .build()); + } + + private static void addUsableType(Map<String, Type> usableTypes, Type type) { + usableTypes.put(type.getQualifiedName(), type); + } + + private static void addParcelableWrapperTypes( + Map<String, Type> usableTypes, Collection<ParcelableWrapper> parcelableWrappers) { + for (ParcelableWrapper parcelableWrapper : parcelableWrappers) { + addParcelableWrapperType(usableTypes, parcelableWrapper); + } + } + + private static void addParcelableWrapperType( + Map<String, Type> usableTypes, ParcelableWrapper parcelableWrapper) { + String createParcelableCode = parcelableWrapper.wrapperClassName() + ".of(this, valueType, $L)"; + // "this" will be a Bundler as this code is only run within a Bundler + + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(parcelableWrapper.wrappedType()) + .setAcceptableReturnType(true) + .setAcceptableParameterType(true) + .setWriteToParcelCode("$L.writeParcelable(" + createParcelableCode + ", flags)") + .setReadFromParcelCode( + "((" + + parcelableWrapper.wrapperClassName() + + ") $L.readParcelable(Bundler.class.getClassLoader())).get()") + .setParcelableWrapper(parcelableWrapper) + .build()); + } + + private static void addFutureWrapperTypes( + Map<String, Type> usableTypes, Collection<FutureWrapper> futureWrappers) { + for (FutureWrapper futureWrapper : futureWrappers) { + addFutureWrapperType(usableTypes, futureWrapper); + } + } + + private static void addFutureWrapperType( + Map<String, Type> usableTypes, FutureWrapper futureWrapper) { + addUsableType( + usableTypes, + Type.builder() + .setTypeMirror(futureWrapper.wrappedType()) + .setAcceptableReturnType(true) + .setSupportedInsideWrapper(false) + .setFutureWrapper(futureWrapper) + .build()); + } + + public static final class Builder { + + private Map<String, Type> usableTypes; + + private Builder(Map<String, Type> usableTypes) { + this.usableTypes = usableTypes; + } + + /** Filtering to only include used types. */ + public Builder filterUsed( + ValidatorContext context, Collection<CrossProfileMethodInfo> methods) { + + Map<String, Type> usedTypes = new HashMap<>(); + + for (CrossProfileMethodInfo method : methods) { + copySupportedTypesForMethod(context, usedTypes, method); + } + + this.usableTypes = usedTypes; + + return this; + } + + private void copySupportedTypesForMethod( + ValidatorContext context, Map<String, Type> usedTypes, CrossProfileMethodInfo method) { + copySupportedType(context, usedTypes, method.returnType()); + for (TypeMirror argumentType : method.parameterTypes()) { + copySupportedType(context, usedTypes, argumentType); + } + } + + private void copySupportedType( + ValidatorContext context, Map<String, Type> usedTypes, TypeMirror type) { + if (TypeUtils.isGeneric(type)) { + copySupportedGenericType(context, usedTypes, type); + return; + } + + if (TypeUtils.isArray(type)) { + copySupportedType(context, usedTypes, TypeUtils.extractTypeFromArray(type)); + if (!TypeUtils.extractTypeFromArray(type).getKind().isPrimitive()) { + type = + context + .types() + .getArrayType(context.elements().getTypeElement("java.lang.Object").asType()); + } + } + + // The type must have been seen in when constructing the original so this should not + // be null + Type supportedType = usableTypes.get(type.toString()); + + // We don't support generic callbacks so any callback interfaces can be picked up here + if (supportedType.isCrossProfileCallbackInterface()) { + for (TypeMirror typeMirror : + supportedType.getCrossProfileCallbackInterface().get().argumentTypes()) { + copySupportedType(context, usedTypes, typeMirror); + } + } + + copySupportedType(usedTypes, supportedType); + } + + private void copySupportedType(Map<String, Type> usedTypes, Type supportedType) { + addUsableType(usedTypes, supportedType); + } + + private void copySupportedGenericType( + ValidatorContext context, Map<String, Type> usedTypes, TypeMirror type) { + TypeMirror genericType = TypeUtils.removeTypeArguments(type); + + // The type must have been seen in when constructing the oldSupportedTypes so this should not + // be null + Type supportedType = usableTypes.get(genericType.toString()); + + if (!supportedType.isSupportedWithAnyGenericType()) { + // We need to recursively copy all type arguments + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(type)) { + copySupportedType(context, usedTypes, typeArgument); + } + } + + copySupportedType(usedTypes, supportedType); + } + + /** Add additianal parcelable wrappers. */ + public Builder addParcelableWrappers(Collection<ParcelableWrapper> parcelableWrappers) { + Map<String, Type> newUsableTypes = new HashMap<>(usableTypes); + + addParcelableWrapperTypes(newUsableTypes, parcelableWrappers); + + usableTypes = newUsableTypes; + + return this; + } + + /** Add additianal future wrappers. */ + public Builder addFutureWrappers(Collection<FutureWrapper> futureWrappers) { + Map<String, Type> newUsableTypes = new HashMap<>(usableTypes); + + addFutureWrapperTypes(newUsableTypes, futureWrappers); + + usableTypes = newUsableTypes; + + return this; + } + + public Builder replaceWrapperPrefix(ClassName prefix) { + Map<String, Type> newUsableTypes = new HashMap<>(); + + for (Type usableType : usableTypes.values()) { + if (usableType.getParcelableWrapper().isPresent()) { + replaceParcelableWrapperPrefix(newUsableTypes, prefix, usableType); + } else if (usableType.getFutureWrapper().isPresent()) { + replaceFutureWrapperPrefix(newUsableTypes, prefix, usableType); + } else { + addUsableType(newUsableTypes, usableType); + } + } + + usableTypes = newUsableTypes; + + return this; + } + + private void replaceParcelableWrapperPrefix( + Map<String, Type> newUsableTypes, ClassName prefix, Type usableType) { + ParcelableWrapper parcelableWrapper = usableType.getParcelableWrapper().get(); + + if (parcelableWrapper.wrapperType().equals(ParcelableWrapper.WrapperType.CUSTOM)) { + // Custom types never get prefixed + addUsableType(newUsableTypes, usableType); + return; + } + + addParcelableWrapperType( + newUsableTypes, + ParcelableWrapper.create( + parcelableWrapper.wrappedType(), + parcelableWrapper.defaultWrapperClassName(), + prefix(prefix, parcelableWrapper.wrapperClassName()), + parcelableWrapper.wrapperType())); + } + + private void replaceFutureWrapperPrefix( + Map<String, Type> newUsableTypes, ClassName prefix, Type usableType) { + FutureWrapper futureWrapper = usableType.getFutureWrapper().get(); + + if (futureWrapper.wrapperType().equals(FutureWrapper.WrapperType.CUSTOM)) { + // Custom types never get prefixed + addUsableType(newUsableTypes, usableType); + return; + } + + addFutureWrapperType( + newUsableTypes, + FutureWrapper.create( + futureWrapper.wrappedType(), + futureWrapper.defaultWrapperClassName(), + prefix(prefix, futureWrapper.wrapperClassName()), + futureWrapper.wrapperType())); + } + + private ClassName prefix(ClassName prefix, ClassName finalName) { + return ClassName.get( + prefix.packageName(), prefix.simpleName() + "_" + finalName.simpleName()); + } + + /** Build a new {@link SupportedTypes}. */ + public SupportedTypes build() { + return new SupportedTypes(usableTypes); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java new file mode 100644 index 0000000..77483f5 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TestCodeGenerator.java @@ -0,0 +1,97 @@ +/* + * 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.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTestInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTypeInfo; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.ProfileConnectorInfo; +import com.google.android.enterprise.connectedapps.processor.containers.ProviderClassInfo; +import java.util.HashSet; +import java.util.Set; + +/** + * Generator of cross-profile test code. + * + * <p>This is intended to be initialised and used once, which will generate all needed code. + * + * <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 TestCodeGenerator { + private boolean generated = false; + private final GeneratorContext generatorContext; + private final Set<CrossProfileTypeInfo> fakedTypes = new HashSet<>(); + private final Set<ProfileConnectorInfo> fakedConnectors = new HashSet<>(); + + TestCodeGenerator(GeneratorContext generatorContext) { + this.generatorContext = checkNotNull(generatorContext); + } + + void generate() { + if (generated) { + throw new IllegalStateException("TestCodeGenerator#generate can only be called once"); + } + generated = true; + + collectTestTypes(); + generateFakes(); + } + + private void generateFakes() { + for (ProfileConnectorInfo connector : fakedConnectors) { + new FakeProfileConnectorGenerator(generatorContext, connector).generate(); + } + + for (CrossProfileTypeInfo type : fakedTypes) { + new FakeCrossProfileTypeGenerator(generatorContext, type).generate(); + new FakeOtherGenerator(generatorContext, type).generate(); + } + } + + private void collectTestTypes() { + for (CrossProfileTestInfo crossProfileTest : generatorContext.crossProfileTests()) { + collectTestTypes(crossProfileTest); + } + } + + private void collectTestTypes(CrossProfileTestInfo crossProfileTest) { + for (CrossProfileConfigurationInfo configuration : crossProfileTest.configurations()) { + collectTestTypes(configuration); + } + } + + private void collectTestTypes(CrossProfileConfigurationInfo configuration) { + for (ProviderClassInfo provider : configuration.providers()) { + collectTestTypes(provider); + } + + fakedConnectors.add(configuration.profileConnector()); + } + + private void collectTestTypes(ProviderClassInfo provider) { + for (CrossProfileTypeInfo type : provider.allCrossProfileTypes()) { + collectTestTypes(type); + } + } + + private void collectTestTypes(CrossProfileTypeInfo type) { + fakedTypes.add(type); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java new file mode 100644 index 0000000..6d5d073 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/TypeUtils.java @@ -0,0 +1,121 @@ +/* + * 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.BUNDLER_TYPE_CLASSNAME; +import static java.util.stream.Collectors.toList; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import java.util.ArrayList; +import java.util.List; +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +/** Utilities for manipulating {@link TypeMirror} instances. */ +public class TypeUtils { + + public static boolean isArray(TypeMirror type) { + return type instanceof ArrayType; + } + + /** + * Extract a type from an array. + * + * <p>Assumes that {@code type} represents an array. + */ + public static TypeMirror extractTypeFromArray(TypeMirror type) { + return ((ArrayType) type).getComponentType(); + } + + public static boolean isGeneric(TypeMirror type) { + if (type instanceof DeclaredType) { + return !((DeclaredType) type).getTypeArguments().isEmpty(); + } + return false; + } + + public static TypeMirror removeTypeArguments(TypeMirror type) { + if (type instanceof DeclaredType) { + return ((DeclaredType) type).asElement().asType(); + } + return type; + } + + public static List<TypeMirror> extractTypeArguments(TypeMirror type) { + if (!(type instanceof DeclaredType)) { + return null; + } + + return new ArrayList<>(((DeclaredType) type).getTypeArguments()); + } + + static ClassName getRawTypeClassName(TypeMirror type) { + String rawTypeQualifiedName = getRawTypeQualifiedName(type); + + if (!rawTypeQualifiedName.contains(".")) { + return ClassName.get("", rawTypeQualifiedName); + } + + String packageName = rawTypeQualifiedName.substring(0, rawTypeQualifiedName.lastIndexOf(".")); + String simpleName = rawTypeQualifiedName.substring(rawTypeQualifiedName.lastIndexOf(".") + 1); + + return ClassName.get(packageName, simpleName); + } + + static String getRawTypeQualifiedName(TypeMirror type) { + // This converts e.g. java.util.List<String> into java.util.List + return type.toString().split("<", 2)[0]; + } + + static CodeBlock generateBundlerType(TypeMirror type) { + if (isArray(type)) { + return generateArrayBundlerType(type); + } + if (isGeneric(type)) { + return generateGenericBundlerType(type); + } + return CodeBlock.of("$T.of($S)", BUNDLER_TYPE_CLASSNAME, getRawTypeQualifiedName(type)); + } + + private static CodeBlock generateArrayBundlerType(TypeMirror type) { + TypeMirror arrayType = extractTypeFromArray(type); + + return CodeBlock.of( + "$T.of($S, $L)", + BUNDLER_TYPE_CLASSNAME, + "java.lang.Object[]", + generateBundlerType(arrayType)); + } + + private static CodeBlock generateGenericBundlerType(TypeMirror type) { + CodeBlock.Builder typeArgs = CodeBlock.builder(); + + List<CodeBlock> typeArgBlocks = + extractTypeArguments(type).stream().map(TypeUtils::generateBundlerType).collect(toList()); + + typeArgs.add(typeArgBlocks.get(0)); + for (CodeBlock typeArgBlock : typeArgBlocks.subList(1, typeArgBlocks.size())) { + typeArgs.add(", $L", typeArgBlock); + } + + return CodeBlock.of( + "$T.of($S, $L)", BUNDLER_TYPE_CLASSNAME, getRawTypeQualifiedName(type), typeArgs.build()); + } + + private TypeUtils() {} +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java new file mode 100644 index 0000000..415765b --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/UserConnectorCodeGenerator.java @@ -0,0 +1,178 @@ +/* + * 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_USER_CONNECTOR_BUILDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.ABSTRACT_USER_CONNECTOR_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.AVAILABILITY_RESTRICTIONS_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONNECTION_BINDER_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.CONTEXT_CLASSNAME; +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.SCHEDULED_EXECUTOR_SERVICE_CLASSNAME; +import static com.google.common.base.Preconditions.checkNotNull; + +import com.google.android.enterprise.connectedapps.annotations.GeneratedUserConnector; +import com.google.android.enterprise.connectedapps.processor.containers.GeneratorContext; +import com.google.android.enterprise.connectedapps.processor.containers.UserConnectorInfo; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.TypeSpec; +import javax.lang.model.element.Modifier; + +/** + * Generate the {@code Generated*} class for a single {@link GeneratedUserConnector} annotated + * class. + * + * <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. + */ +class UserConnectorCodeGenerator { + private boolean generated = false; + + private final GeneratorContext generatorContext; + private final GeneratorUtilities generatorUtilities; + private final UserConnectorInfo connector; + + UserConnectorCodeGenerator(GeneratorContext generatorContext, UserConnectorInfo connector) { + this.generatorContext = checkNotNull(generatorContext); + this.generatorUtilities = new GeneratorUtilities(generatorContext); + this.connector = checkNotNull(connector); + } + + void generate() { + if (generated) { + throw new IllegalStateException( + "ProfileConnectorCodeGenerator#generate can only be called once"); + } + generated = true; + + generateUserConnector(); + } + + private void generateUserConnector() { + ClassName className = getGeneratedUserConnectorClassName(generatorContext, connector); + ClassName builderClassName = + getGeneratedUserConnectorBuilderClassName(generatorContext, connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(className) + .addJavadoc( + "Generated implementation of {@link $T}.\n\n" + + "<p>All logic is implemented by {@link $T}.\n", + connector.connectorClassName(), + ABSTRACT_USER_CONNECTOR_CLASSNAME) + .addModifiers(Modifier.FINAL) + .addSuperinterface(connector.connectorClassName()) + .superclass(ABSTRACT_USER_CONNECTOR_CLASSNAME); + + classBuilder.addMethod( + MethodSpec.methodBuilder("builder") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(builderClassName) + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("return new $T(context)", builderClassName) + .build()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addModifiers(Modifier.PRIVATE) + .addParameter(builderClassName, "builder") + .addStatement( + "super($1T.class, builder.profileConnectorBuilder)", connector.connectorClassName()) + .build()); + + generateUserConnectorBuilder(classBuilder); + + generatorUtilities.writeClassToFile(className.packageName(), classBuilder); + } + + private void generateUserConnectorBuilder(TypeSpec.Builder profileConnector) { + ClassName connectorClassName = getGeneratedUserConnectorClassName(generatorContext, connector); + ClassName builderClassName = + getGeneratedUserConnectorBuilderClassName(generatorContext, connector); + + TypeSpec.Builder classBuilder = + TypeSpec.classBuilder(builderClassName) + .addJavadoc("Builder for {@link $T}.\n", connectorClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC); + + CodeBlock initialiser = + CodeBlock.of( + "new $T().setServiceClassName($S).setAvailabilityRestrictions($T.$L)", + ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME, + connector.serviceName().toString(), + AVAILABILITY_RESTRICTIONS_CLASSNAME, + connector.availabilityRestrictions().name()); + + classBuilder.addMethod( + MethodSpec.constructorBuilder() + .addParameter(CONTEXT_CLASSNAME, "context") + .addStatement("profileConnectorBuilder.setContext(context)") + .build()); + + classBuilder.addField( + FieldSpec.builder(ABSTRACT_USER_CONNECTOR_BUILDER_CLASSNAME, "profileConnectorBuilder") + .addModifiers(Modifier.PRIVATE, Modifier.FINAL) + .initializer(initialiser) + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("setScheduledExecutorService") + .addModifiers(Modifier.PUBLIC) + .addParameter(SCHEDULED_EXECUTOR_SERVICE_CLASSNAME, "scheduledExecutorService") + .returns(builderClassName) + .addStatement( + "profileConnectorBuilder.setScheduledExecutorService(scheduledExecutorService)") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("setBinder") + .addModifiers(Modifier.PUBLIC) + .addParameter(CONNECTION_BINDER_CLASSNAME, "binder") + .returns(builderClassName) + .addStatement("profileConnectorBuilder.setBinder(binder)") + .addStatement("return this") + .build()); + + classBuilder.addMethod( + MethodSpec.methodBuilder("build") + .addModifiers(Modifier.PUBLIC) + .returns(connectorClassName) + .addStatement("return new $1T(this)", connectorClassName) + .build()); + + profileConnector.addType(classBuilder.build()); + } + + static ClassName getGeneratedUserConnectorClassName( + GeneratorContext generatorContext, UserConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName(), + "Generated" + connector.connectorClassName().simpleName()); + } + + static ClassName getGeneratedUserConnectorBuilderClassName( + GeneratorContext generatorContext, UserConnectorInfo connector) { + return ClassName.get( + connector.connectorClassName().packageName() + + "." + + "Generated" + + connector.connectorClassName().simpleName(), + "Builder"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java new file mode 100644 index 0000000..e5f4a01 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/ValidationMessageFormatter.java @@ -0,0 +1,49 @@ +/* + * 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 com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationNames; + +/** Formats annotation validation messages with the provided names of the annotation set. */ +public final class ValidationMessageFormatter { + + private final AnnotationNames annotationNames; + + public static ValidationMessageFormatter forAnnotations(AnnotationNames annotationNames) { + return new ValidationMessageFormatter(annotationNames); + } + + private ValidationMessageFormatter(AnnotationNames annotationNames) { + this.annotationNames = annotationNames; + } + + /** + * Supports the replacement strings CROSS_PROFILE_ANNOTATION, CROSS_PROFILE_CALLBACK_ANNOTATION, + * CROSS_PROFILE_CONFIGURATION_ANNOTATION, CROSS_PROFILE_CONFIGURATIONS_ANNOTATION, + * CROSS_PROFILE_PROVIDER_ANNOTATION, and CROSS_PROFILE_TEST_ANNOTATION. + */ + String format(String message) { + return message + .replace("CROSS_PROFILE_ANNOTATION", annotationNames.crossProfile()) + .replace("CROSS_PROFILE_CALLBACK_ANNOTATION", annotationNames.crossProfileCallback()) + .replace( + "CROSS_PROFILE_CONFIGURATION_ANNOTATION", annotationNames.crossProfileConfiguration()) + .replace( + "CROSS_PROFILE_CONFIGURATIONS_ANNOTATION", annotationNames.crossProfileConfigurations()) + .replace("CROSS_PROFILE_PROVIDER_ANNOTATION", annotationNames.crossProfileProvider()) + .replace("CROSS_PROFILE_TEST_ANNOTATION", annotationNames.crossProfileTest()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java new file mode 100644 index 0000000..1616838 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationClasses.java @@ -0,0 +1,42 @@ +/* + * 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.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.annotations.CrossUser; +import com.google.android.enterprise.connectedapps.annotations.CrossUserCallback; +import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider; +import java.lang.annotation.Annotation; + +/** + * A set of parallel annotation classes. + * + * <p>For example, a valid instance could return {@link CrossUser}, {@link CrossUserCallback}, + * {@link CrossUserProvider}, etc. + */ +public interface AnnotationClasses { + + Class<? extends Annotation> crossProfileAnnotationClass(); + + Class<? extends Annotation> crossProfileCallbackAnnotationClass(); + + Class<? extends Annotation> crossProfileConfigurationAnnotationClass(); + + Class<? extends Annotation> crossProfileConfigurationsAnnotationClass(); + + Class<? extends Annotation> crossProfileProviderAnnotationClass(); + + Class<? extends Annotation> crossProfileTestAnnotationClass(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java new file mode 100644 index 0000000..4923567 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationFinder.java @@ -0,0 +1,271 @@ +/* + * 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.annotationdiscovery; + +import static java.util.stream.Collectors.toSet; +import static javax.lang.model.element.ElementKind.METHOD; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfigurations; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; +import com.google.android.enterprise.connectedapps.annotations.CrossUser; +import com.google.android.enterprise.connectedapps.annotations.CrossUserCallback; +import com.google.android.enterprise.connectedapps.annotations.CrossUserConfiguration; +import com.google.android.enterprise.connectedapps.annotations.CrossUserConfigurations; +import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider; +import com.google.android.enterprise.connectedapps.processor.ValidationMessageFormatter; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationsAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileProviderAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTestAnnotationInfo; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossUserTest; +import com.google.common.collect.ImmutableList; +import java.lang.annotation.Annotation; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Helper methods to discover all cross-profile annotations of a specific type on elements. */ +public final class AnnotationFinder { + + private static final AnnotationStrings CROSS_PROFILE_ANNOTATION_STRINGS = + AnnotationStrings.builder() + .setCrossProfileAnnotationClass(CrossProfile.class) + .setCrossProfileCallbackAnnotationClass(CrossProfileCallback.class) + .setCrossProfileConfigurationAnnotationClass(CrossProfileConfiguration.class) + .setCrossProfileConfigurationsAnnotationClass(CrossProfileConfigurations.class) + .setCrossProfileProviderAnnotationClass(CrossProfileProvider.class) + .setCrossProfileTestAnnotationClass(CrossProfileTest.class) + .build(); + + private static final AnnotationStrings CROSS_USER_ANNOTATION_STRINGS = + AnnotationStrings.builder() + .setCrossProfileAnnotationClass(CrossUser.class) + .setCrossProfileCallbackAnnotationClass(CrossUserCallback.class) + .setCrossProfileProviderAnnotationClass(CrossUserProvider.class) + .setCrossProfileConfigurationAnnotationClass(CrossUserConfiguration.class) + .setCrossProfileConfigurationsAnnotationClass(CrossUserConfigurations.class) + .setCrossProfileTestAnnotationClass(CrossUserTest.class) + .build(); + + private static final ImmutableList<AnnotationStrings> SUPPORTED_ANNOTATIONS = + ImmutableList.of(CROSS_PROFILE_ANNOTATION_STRINGS, CROSS_USER_ANNOTATION_STRINGS); + + private static final AnnotationStrings DEFAULT_ANNOTATIONS = CROSS_PROFILE_ANNOTATION_STRINGS; + + private static final Set<Class<? extends Annotation>> crossProfileAnnotations = + annotationsOfType(AnnotationClasses::crossProfileAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileCallbackAnnotations = + annotationsOfType(AnnotationClasses::crossProfileCallbackAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileConfigurationAnnotations = + annotationsOfType(AnnotationClasses::crossProfileConfigurationAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileConfigurationsAnnotations = + annotationsOfType(AnnotationClasses::crossProfileConfigurationsAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileProviderAnnotations = + annotationsOfType(AnnotationClasses::crossProfileProviderAnnotationClass); + + private static final Set<Class<? extends Annotation>> crossProfileTestAnnotations = + annotationsOfType(AnnotationClasses::crossProfileTestAnnotationClass); + + public static Iterable<AnnotationStrings> annotationStrings() { + return SUPPORTED_ANNOTATIONS; + } + + public static AnnotationNames crossProfileAnnotationNames() { + return CROSS_PROFILE_ANNOTATION_STRINGS; + } + + public static AnnotationNames crossUserAnnotationNames() { + return CROSS_USER_ANNOTATION_STRINGS; + } + + public static ValidationMessageFormatter validationMessageFormatterFor(Element element) { + return ValidationMessageFormatter.forAnnotations(annotationNamesFor(element)); + } + + private static AnnotationNames annotationNamesFor(Element element) { + for (AnnotationStrings annotationStrings : SUPPORTED_ANNOTATIONS) { + if (hasAnyAnnotationsOfClass(element, annotationStrings)) { + return annotationStrings; + } + } + + return DEFAULT_ANNOTATIONS; + } + + public static ValidationMessageFormatter validationMessageFormatterForClass( + TypeElement typeElement) { + return ValidationMessageFormatter.forAnnotations(annotationNamesForClass(typeElement)); + } + + public static AnnotationNames annotationNamesForClass(TypeElement typeElement) { + for (AnnotationStrings annotationStrings : SUPPORTED_ANNOTATIONS) { + if (hasAnyAnnotationsOfClass(typeElement, annotationStrings)) { + return annotationStrings; + } + + for (ExecutableElement method : + typeElement.getEnclosedElements().stream() + .filter(element -> element.getKind() == METHOD) + .map(element -> (ExecutableElement) element) + .collect(toSet())) { + if (hasAnyAnnotationsOfClass(method, annotationStrings)) { + return annotationStrings; + } + } + } + + return DEFAULT_ANNOTATIONS; + } + + private static boolean hasAnyAnnotationsOfClass( + Element element, AnnotationClasses annotationClasses) { + return hasAnnotationOfClass(element, annotationClasses.crossProfileAnnotationClass()) + || hasAnnotationOfClass(element, annotationClasses.crossProfileCallbackAnnotationClass()) + || hasAnnotationOfClass(element, annotationClasses.crossProfileProviderAnnotationClass()) + || hasAnnotationOfClass( + element, annotationClasses.crossProfileConfigurationAnnotationClass()) + || hasAnnotationOfClass( + element, annotationClasses.crossProfileConfigurationsAnnotationClass()) + || hasAnnotationOfClass(element, annotationClasses.crossProfileTestAnnotationClass()); + } + + private static boolean hasAnnotationOfClass( + Element element, Class<? extends Annotation> annotationClass) { + return element.getAnnotation(annotationClass) != null; + } + + public static CrossProfileAnnotationInfo extractCrossProfileAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileCallbackAnnotationInfo extractCrossProfileCallbackAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileCallbackAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileConfigurationAnnotationInfo + extractCrossProfileConfigurationAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileConfigurationAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileConfigurationsAnnotationInfo + extractCrossProfileConfigurationsAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileConfigurationsAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileProviderAnnotationInfo extractCrossProfileProviderAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileProviderAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static CrossProfileTestAnnotationInfo extractCrossProfileTestAnnotationInfo( + Element annotatedElement, Types types, Elements elements) { + return new CrossProfileTestAnnotationInfoExtractor() + .extractAnnotationInfo(SUPPORTED_ANNOTATIONS, annotatedElement, types, elements); + } + + public static boolean hasCrossProfileAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileAnnotations); + } + + public static boolean hasCrossProfileCallbackAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileCallbackAnnotations); + } + + public static boolean hasCrossProfileConfigurationAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileConfigurationAnnotations); + } + + public static boolean hasCrossProfileConfigurationsAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileConfigurationsAnnotations); + } + + public static boolean hasCrossProfileProviderAnnotation(Element element) { + return hasAnyAnnotations(element, crossProfileProviderAnnotations); + } + + private static boolean hasAnyAnnotations( + Element element, Set<Class<? extends Annotation>> annotations) { + return annotations.stream().anyMatch(annotation -> element.getAnnotation(annotation) != null); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfile( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileCallback( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileCallbackAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileConfiguration( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileConfigurationAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileConfigurations( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileConfigurationsAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileProvider( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileProviderAnnotations); + } + + public static Stream<? extends Element> elementsAnnotatedWithCrossProfileTest( + RoundEnvironment roundEnv) { + return findElementsContainingAnnotations(roundEnv, crossProfileTestAnnotations); + } + + private static Stream<? extends Element> findElementsContainingAnnotations( + RoundEnvironment roundEnv, Set<Class<? extends Annotation>> annotations) { + return annotations.stream() + .flatMap(annotation -> roundEnv.getElementsAnnotatedWith(annotation).stream()); + } + + private static Set<Class<? extends Annotation>> annotationsOfType( + Function<AnnotationClasses, Class<? extends Annotation>> annotationClassGetter) { + return SUPPORTED_ANNOTATIONS.stream().map(annotationClassGetter).collect(toSet()); + } + + private AnnotationFinder() {} +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java new file mode 100644 index 0000000..08022c4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInfoExtractor.java @@ -0,0 +1,92 @@ +/* + * 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.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; +import com.google.android.enterprise.connectedapps.annotations.CrossUserProvider; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileProviderAnnotation; +import java.lang.annotation.Annotation; +import java.lang.reflect.Proxy; +import javax.lang.model.element.Element; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** + * An extractor which generates {@link AnnotationInfoT} for elements annotated with annotations that + * conform to {@link AnnotationInterfaceT}. + */ +abstract class AnnotationInfoExtractor<AnnotationInfoT, AnnotationInterfaceT> { + + private final Class<AnnotationInterfaceT> annotationInterfaceClass; + + AnnotationInfoExtractor(Class<AnnotationInterfaceT> annotationInterfaceClass) { + this.annotationInterfaceClass = annotationInterfaceClass; + } + + /** + * Returns the {@link AnnotationInfoT} that can be extracted from the first supported annotation + * on {@code annotatedElement}, or a default instance otherwise. + */ + AnnotationInfoT extractAnnotationInfo( + Iterable<? extends AnnotationClasses> availableAnnotations, + Element annotatedElement, + Types types, + Elements elements) { + for (AnnotationClasses annotationClasses : availableAnnotations) { + Annotation annotation = + annotatedElement.getAnnotation(supportedAnnotationClass(annotationClasses)); + + if (annotation != null) { + return annotationInfoFromAnnotation( + wrapAnnotationWithInterface(annotationInterfaceClass, annotation), types); + } + } + + return emptyAnnotationInfo(elements); + } + + /** + * Returns the class of the annotation type that this extractor generates {@link AnnotationInfoT} + * for. + * + * <p>For example, if supporting {@link CrossProfileProvider} and {@link CrossUserProvider} + * annotations, return the value of {@link + * AnnotationClasses#crossProfileProviderAnnotationClass()}. + */ + protected abstract Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses); + + protected abstract AnnotationInfoT annotationInfoFromAnnotation( + AnnotationInterfaceT annotation, Types types); + + protected abstract AnnotationInfoT emptyAnnotationInfo(Elements elements); + + /** + * Wraps any annotation of a specific type (e.g. {@link CrossProfileProvider} and {@link + * CrossUserProvider}) with its interface (in that case {@link CrossProfileProviderAnnotation}). + * + * <p>Java does not allow annotation subclassing so we use Java proxies to treat these different + * annotations with identical interfaces polymorphically. + */ + protected static <T> T wrapAnnotationWithInterface( + Class<T> annotationInterfaceClass, Annotation annotation) { + return annotationInterfaceClass.cast( + Proxy.newProxyInstance( + annotationInterfaceClass.getClassLoader(), + new Class<?>[] {annotationInterfaceClass}, + new AnnotationInvocationHandler(annotation))); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java new file mode 100644 index 0000000..0179dd9 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationInvocationHandler.java @@ -0,0 +1,49 @@ +/* + * 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.annotationdiscovery; + +import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Given an annotation, forwards method calls from a mock instance of its interface to its actual + * instance. + * + * <p>This allows us to treat separate annotations with identical interfaces polymorphically. + */ +class AnnotationInvocationHandler implements InvocationHandler { + + private final Annotation annotation; + + AnnotationInvocationHandler(Annotation annotation) { + this.annotation = annotation; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + return invokeAndUnwrapExceptions(annotation.annotationType().getMethod(method.getName())); + } + + private Object invokeAndUnwrapExceptions(Method method) throws Throwable { + try { + return method.invoke(annotation); + } catch (InvocationTargetException e) { + throw e.getCause(); + } + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java new file mode 100644 index 0000000..37c3961 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationNames.java @@ -0,0 +1,37 @@ +/* + * 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.annotationdiscovery; + +/** + * A set of parallel annotation names. + * + * <p>For example, a valid instance could return "CrossUser", "CrossUserCallback", and + * "CrossUserProvider", etc. + */ +public interface AnnotationNames { + + String crossProfile(); + + String crossProfileCallback(); + + String crossProfileConfiguration(); + + String crossProfileConfigurations(); + + String crossProfileProvider(); + + String crossProfileTest(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java new file mode 100644 index 0000000..adf1662 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationPrinter.java @@ -0,0 +1,52 @@ +/* + * 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.annotationdiscovery; + +/** Prints annotations as they would appear in source code. */ +public interface AnnotationPrinter { + + String crossProfileAsAnnotation(); + + String crossProfileAsAnnotation(String content); + + String crossProfileQualifiedName(); + + String crossProfileCallbackAsAnnotation(); + + String crossProfileCallbackAsAnnotation(String content); + + String crossProfileCallbackQualifiedName(); + + String crossProfileConfigurationAsAnnotation(); + + String crossProfileConfigurationAsAnnotation(String content); + + String crossProfileConfigurationQualifiedName(); + + String crossProfileConfigurationsAsAnnotation(String content); + + String crossProfileConfigurationsQualifiedName(); + + String crossProfileProviderAsAnnotation(); + + String crossProfileProviderAsAnnotation(String content); + + String crossProfileProviderQualifiedName(); + + String crossProfileTestAsAnnotation(String content); + + String crossProfileTestQualifiedName(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java new file mode 100644 index 0000000..35ce81a --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/AnnotationStrings.java @@ -0,0 +1,188 @@ +/* + * 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.annotationdiscovery; + +import com.google.auto.value.AutoValue; +import java.lang.annotation.Annotation; + +/** Provides the raw string names for cross-profile annotations. */ +@AutoValue +public abstract class AnnotationStrings + implements AnnotationNames, AnnotationPrinter, AnnotationClasses { + + @Override + public String crossProfile() { + return crossProfileAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileAsAnnotation() { + return asAnnotation(crossProfile()); + } + + @Override + public String crossProfileAsAnnotation(String content) { + return asAnnotationWithContent(crossProfile(), content); + } + + @Override + public String crossProfileQualifiedName() { + return crossProfileAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileCallback() { + return crossProfileCallbackAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileCallbackAsAnnotation() { + return asAnnotation(crossProfileCallback()); + } + + @Override + public String crossProfileCallbackAsAnnotation(String content) { + return crossProfileCallbackAsAnnotation() + "(" + content + ")"; + } + + @Override + public String crossProfileCallbackQualifiedName() { + return crossProfileCallbackAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileConfiguration() { + return crossProfileConfigurationAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileConfigurationAsAnnotation() { + return asAnnotation(crossProfileConfiguration()); + } + + @Override + public String crossProfileConfigurationAsAnnotation(String content) { + return asAnnotationWithContent(crossProfileConfiguration(), content); + } + + @Override + public String crossProfileConfigurationQualifiedName() { + return crossProfileConfigurationAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileConfigurations() { + return crossProfileConfigurationsAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileConfigurationsAsAnnotation(String content) { + return asAnnotationWithContent(crossProfileProvider(), content); + } + + @Override + public String crossProfileConfigurationsQualifiedName() { + return crossProfileConfigurationsAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileProvider() { + return crossProfileProviderAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileProviderAsAnnotation() { + return asAnnotation(crossProfileProvider()); + } + + @Override + public String crossProfileProviderAsAnnotation(String content) { + return asAnnotationWithContent(crossProfileProvider(), content); + } + + @Override + public String crossProfileProviderQualifiedName() { + return crossProfileProviderAnnotationClass().getCanonicalName(); + } + + @Override + public String crossProfileTest() { + return crossProfileTestAnnotationClass().getSimpleName(); + } + + @Override + public String crossProfileTestAsAnnotation(String content) { + return asAnnotationWithContent(crossProfileTest(), content); + } + + @Override + public String crossProfileTestQualifiedName() { + return crossProfileTestAnnotationClass().getCanonicalName(); + } + + @Override + public final String toString() { + return crossProfile() + " AnnotationStrings"; + } + + private static String asAnnotation(String annotationName) { + return "@" + annotationName; + } + + private static String asAnnotationWithContent(String annotationName, String content) { + return "@" + annotationName + "(" + content + ")"; + } + + @Override + public abstract Class<? extends Annotation> crossProfileAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileCallbackAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileConfigurationAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileConfigurationsAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileProviderAnnotationClass(); + + @Override + public abstract Class<? extends Annotation> crossProfileTestAnnotationClass(); + + static Builder builder() { + return new AutoValue_AnnotationStrings.Builder(); + } + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setCrossProfileAnnotationClass(Class<? extends Annotation> value); + + abstract Builder setCrossProfileCallbackAnnotationClass(Class<? extends Annotation> value); + + abstract Builder setCrossProfileConfigurationAnnotationClass(Class<? extends Annotation> value); + + abstract Builder setCrossProfileConfigurationsAnnotationClass( + Class<? extends Annotation> value); + + abstract Builder setCrossProfileProviderAnnotationClass(Class<? extends Annotation> value); + + abstract Builder setCrossProfileTestAnnotationClass(Class<? extends Annotation> value); + + abstract AnnotationStrings build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java new file mode 100644 index 0000000..49e737d --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileAnnotationInfoExtractor.java @@ -0,0 +1,78 @@ +/* + * 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.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileAnnotationInfo; +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Annotation; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileAnnotationInfoExtractor + extends AnnotationInfoExtractor<CrossProfileAnnotationInfo, CrossProfileAnnotation> { + + CrossProfileAnnotationInfoExtractor() { + super(CrossProfileAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileAnnotationClass(); + } + + @Override + protected CrossProfileAnnotationInfo annotationInfoFromAnnotation( + CrossProfileAnnotation annotation, Types types) { + CrossProfileAnnotationInfo.Builder builder = + CrossProfileAnnotationInfo.builder() + .setConnectorClass( + GeneratorUtilities.extractClassFromAnnotation(types, annotation::connector)) + .setProfileClassName(annotation.profileClassName()) + .setParcelableWrapperClasses( + ImmutableSet.copyOf( + GeneratorUtilities.extractClassesFromAnnotation( + types, annotation::parcelableWrappers))) + .setFutureWrapperClasses( + ImmutableSet.copyOf( + GeneratorUtilities.extractClassesFromAnnotation( + types, annotation::futureWrappers))) + .setIsStatic(annotation.isStatic()); + + long timeoutMillis = annotation.timeoutMillis(); + + if (timeoutMillis != CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET) { + builder.setTimeoutMillis(timeoutMillis); + } + + return builder.build(); + } + + @Override + protected CrossProfileAnnotationInfo emptyAnnotationInfo(Elements elements) { + return CrossProfileAnnotationInfo.builder() + .setConnectorClass( + elements.getTypeElement( + "com.google.android.enterprise.connectedapps.annotations.CrossProfile")) + .setProfileClassName("") + .setParcelableWrapperClasses(ImmutableSet.of()) + .setFutureWrapperClasses(ImmutableSet.of()) + .setIsStatic(false) + .build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java new file mode 100644 index 0000000..37016e8 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileCallbackAnnotationInfoExtractor.java @@ -0,0 +1,48 @@ +/* + * 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.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileCallbackAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileCallbackAnnotationInfo; +import java.lang.annotation.Annotation; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileCallbackAnnotationInfoExtractor + extends AnnotationInfoExtractor< + CrossProfileCallbackAnnotationInfo, CrossProfileCallbackAnnotation> { + + CrossProfileCallbackAnnotationInfoExtractor() { + super(CrossProfileCallbackAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileCallbackAnnotationClass(); + } + + @Override + protected CrossProfileCallbackAnnotationInfo annotationInfoFromAnnotation( + CrossProfileCallbackAnnotation annotation, Types types) { + return CrossProfileCallbackAnnotationInfo.builder().setSimple(annotation.simple()).build(); + } + + @Override + protected CrossProfileCallbackAnnotationInfo emptyAnnotationInfo(Elements elements) { + return CrossProfileCallbackAnnotationInfo.builder().setSimple(false).build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java new file mode 100644 index 0000000..3b76469 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationAnnotationInfoExtractor.java @@ -0,0 +1,69 @@ +/* + * 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.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileConfigurationAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationAnnotationInfo; +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Annotation; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileConfigurationAnnotationInfoExtractor + extends AnnotationInfoExtractor< + CrossProfileConfigurationAnnotationInfo, CrossProfileConfigurationAnnotation> { + + CrossProfileConfigurationAnnotationInfoExtractor() { + super(CrossProfileConfigurationAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileConfigurationAnnotationClass(); + } + + @Override + protected CrossProfileConfigurationAnnotationInfo annotationInfoFromAnnotation( + CrossProfileConfigurationAnnotation annotation, Types types) { + return CrossProfileConfigurationAnnotationInfo.builder() + .setConnector(GeneratorUtilities.extractClassFromAnnotation(types, annotation::connector)) + .setProviderClasses( + ImmutableSet.copyOf( + GeneratorUtilities.extractClassesFromAnnotation(types, annotation::providers))) + .setServiceClass( + GeneratorUtilities.extractClassFromAnnotation(types, annotation::serviceClass)) + .setServiceSuperclass( + GeneratorUtilities.extractClassFromAnnotation(types, annotation::serviceSuperclass)) + .build(); + } + + @Override + protected CrossProfileConfigurationAnnotationInfo emptyAnnotationInfo(Elements elements) { + TypeElement crossProfileConfiguration = + elements.getTypeElement( + "com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration"); + + return CrossProfileConfigurationAnnotationInfo.builder() + .setConnector(crossProfileConfiguration) + .setProviderClasses(ImmutableSet.of()) + .setServiceClass(crossProfileConfiguration) + .setServiceSuperclass(crossProfileConfiguration) + .build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java new file mode 100644 index 0000000..99eb04d --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileConfigurationsAnnotationInfoExtractor.java @@ -0,0 +1,69 @@ +/* + * 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.annotationdiscovery; + +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static java.util.Arrays.stream; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileConfigurationAnnotation; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileConfigurationsAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationAnnotationInfo; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileConfigurationsAnnotationInfo; +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Annotation; +import java.util.Set; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileConfigurationsAnnotationInfoExtractor + extends AnnotationInfoExtractor< + CrossProfileConfigurationsAnnotationInfo, CrossProfileConfigurationsAnnotation> { + + CrossProfileConfigurationsAnnotationInfoExtractor() { + super(CrossProfileConfigurationsAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileConfigurationsAnnotationClass(); + } + + @Override + protected CrossProfileConfigurationsAnnotationInfo annotationInfoFromAnnotation( + CrossProfileConfigurationsAnnotation annotation, Types types) { + CrossProfileConfigurationAnnotationInfoExtractor innerExtractor = + new CrossProfileConfigurationAnnotationInfoExtractor(); + + Set<CrossProfileConfigurationAnnotationInfo> annotationInfos = + stream(annotation.value()) + .map( + configurationAnnotation -> + wrapAnnotationWithInterface( + CrossProfileConfigurationAnnotation.class, configurationAnnotation)) + .map( + configurationAnnotation -> + innerExtractor.annotationInfoFromAnnotation(configurationAnnotation, types)) + .collect(toImmutableSet()); + + return CrossProfileConfigurationsAnnotationInfo.create(ImmutableSet.copyOf(annotationInfos)); + } + + @Override + protected CrossProfileConfigurationsAnnotationInfo emptyAnnotationInfo(Elements elements) { + return CrossProfileConfigurationsAnnotationInfo.create(ImmutableSet.of()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java new file mode 100644 index 0000000..1fb79c4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileProviderAnnotationInfoExtractor.java @@ -0,0 +1,52 @@ +/* + * 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.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileProviderAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileProviderAnnotationInfo; +import com.google.common.collect.ImmutableSet; +import java.lang.annotation.Annotation; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileProviderAnnotationInfoExtractor + extends AnnotationInfoExtractor< + CrossProfileProviderAnnotationInfo, CrossProfileProviderAnnotation> { + + CrossProfileProviderAnnotationInfoExtractor() { + super(CrossProfileProviderAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileProviderAnnotationClass(); + } + + @Override + protected CrossProfileProviderAnnotationInfo annotationInfoFromAnnotation( + CrossProfileProviderAnnotation annotation, Types types) { + return CrossProfileProviderAnnotationInfo.create( + ImmutableSet.copyOf( + GeneratorUtilities.extractClassesFromAnnotation(types, annotation::staticTypes))); + } + + @Override + protected CrossProfileProviderAnnotationInfo emptyAnnotationInfo(Elements elements) { + return CrossProfileProviderAnnotationInfo.create(ImmutableSet.of()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java new file mode 100644 index 0000000..4a119de --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/CrossProfileTestAnnotationInfoExtractor.java @@ -0,0 +1,51 @@ +/* + * 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.annotationdiscovery; + +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileTestAnnotation; +import com.google.android.enterprise.connectedapps.processor.containers.CrossProfileTestAnnotationInfo; +import java.lang.annotation.Annotation; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +final class CrossProfileTestAnnotationInfoExtractor + extends AnnotationInfoExtractor<CrossProfileTestAnnotationInfo, CrossProfileTestAnnotation> { + + CrossProfileTestAnnotationInfoExtractor() { + super(CrossProfileTestAnnotation.class); + } + + @Override + protected Class<? extends Annotation> supportedAnnotationClass( + AnnotationClasses annotationClasses) { + return annotationClasses.crossProfileTestAnnotationClass(); + } + + @Override + protected CrossProfileTestAnnotationInfo annotationInfoFromAnnotation( + CrossProfileTestAnnotation annotation, Types types) { + return CrossProfileTestAnnotationInfo.builder() + .setConfiguration( + GeneratorUtilities.extractClassFromAnnotation(types, annotation::configuration)) + .build(); + } + + @Override + protected CrossProfileTestAnnotationInfo emptyAnnotationInfo(Elements elements) { + throw new UnsupportedOperationException("Annotations of type CrossProfileTest cannot be empty"); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java new file mode 100644 index 0000000..8e76d9c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileAnnotation.java @@ -0,0 +1,36 @@ +/* + * 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.annotationdiscovery.interfaces; + +/** Elements that can be populated on annotations of type CrossProfile. */ +public interface CrossProfileAnnotation { + + long DEFAULT_TIMEOUT_MILLIS = 10000; + + long TIMEOUT_MILLIS_NOT_SET = -1; + + String profileClassName(); + + Class<?> connector(); + + Class<?>[] parcelableWrappers(); + + Class<?>[] futureWrappers(); + + boolean isStatic(); + + long timeoutMillis(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java new file mode 100644 index 0000000..34e4619 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileCallbackAnnotation.java @@ -0,0 +1,22 @@ +/* + * 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.annotationdiscovery.interfaces; + +/* Elements that can be populated on annotations of type CrossProfileCallback. */ +public interface CrossProfileCallbackAnnotation { + + boolean simple(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java new file mode 100644 index 0000000..94b5205 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationAnnotation.java @@ -0,0 +1,28 @@ +/* + * 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.annotationdiscovery.interfaces; + +/** Elements that can be populated on annotations of type CrossProfileConfiguration. */ +public interface CrossProfileConfigurationAnnotation { + + Class<?>[] providers(); + + Class<?> serviceSuperclass(); + + Class<?> serviceClass(); + + Class<?> connector(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java new file mode 100644 index 0000000..17acd78 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileConfigurationsAnnotation.java @@ -0,0 +1,24 @@ +/* + * 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.annotationdiscovery.interfaces; + +import java.lang.annotation.Annotation; + +/** Elements that can be populated on annotations of type CrossProfileConfigurations. */ +public interface CrossProfileConfigurationsAnnotation { + + Annotation[] value(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java new file mode 100644 index 0000000..b478d36 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileProviderAnnotation.java @@ -0,0 +1,22 @@ +/* + * 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.annotationdiscovery.interfaces; + +/* Elements that can be populated on annotations of type CrossProfileProvider. */ +public interface CrossProfileProviderAnnotation { + + Class<?>[] staticTypes(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java new file mode 100644 index 0000000..542748b --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/annotationdiscovery/interfaces/CrossProfileTestAnnotation.java @@ -0,0 +1,22 @@ +/* + * 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.annotationdiscovery.interfaces; + +/* Elements that can be populated on annotations of type CrossProfileTest. */ +public interface CrossProfileTestAnnotation { + + Class<?> configuration(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java new file mode 100644 index 0000000..e3b17e7 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Context.java @@ -0,0 +1,29 @@ +/* + * 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.containers; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** A container for a validator or generator context. */ +public abstract class Context { + public abstract ProcessingEnvironment processingEnv(); + + public abstract Elements elements(); + + public abstract Types types(); +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java new file mode 100644 index 0000000..f083a69 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileAnnotationInfo.java @@ -0,0 +1,72 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import java.util.Optional; +import javax.lang.model.element.TypeElement; + +/** Wrapper around information contained in an annotation of type {@link CrossProfile}. */ +@AutoValue +public abstract class CrossProfileAnnotationInfo { + + public static final String DEFAULT_CONNECTOR_NAME = + "com.google.android.enterprise.connectedapps.annotations.CrossProfile"; + + public abstract TypeElement connectorClass(); + + public abstract String profileClassName(); + + public abstract Optional<Long> timeoutMillis(); + + public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + public abstract boolean isStatic(); + + public boolean connectorIsDefault() { + return connectorClass().asType().toString().equals(DEFAULT_CONNECTOR_NAME); + } + + public boolean isProfileClassNameDefault() { + return profileClassName().isEmpty(); + } + + public static Builder builder() { + return new AutoValue_CrossProfileAnnotationInfo.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setConnectorClass(TypeElement value); + + public abstract Builder setProfileClassName(String value); + + public abstract Builder setTimeoutMillis(Long value); + + public abstract Builder setParcelableWrapperClasses(ImmutableCollection<TypeElement> value); + + public abstract Builder setFutureWrapperClasses(ImmutableCollection<TypeElement> value); + + public abstract Builder setIsStatic(boolean value); + + public abstract CrossProfileAnnotationInfo build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java new file mode 100644 index 0000000..080b726 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackAnnotationInfo.java @@ -0,0 +1,38 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.auto.value.AutoValue; + +/** Wrapper around information contained in an annotation of type {@link CrossProfileCallback}. */ +@AutoValue +public abstract class CrossProfileCallbackAnnotationInfo { + + public abstract boolean simple(); + + public static Builder builder() { + return new AutoValue_CrossProfileCallbackAnnotationInfo.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setSimple(boolean value); + + public abstract CrossProfileCallbackAnnotationInfo build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java new file mode 100644 index 0000000..ea6f068 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileCallbackInterfaceInfo.java @@ -0,0 +1,72 @@ +/* + * 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.containers; + +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.auto.value.AutoValue; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +/** Wrapper of a {@link CrossProfileCallback} annotated interface. */ +@AutoValue +public abstract class CrossProfileCallbackInterfaceInfo { + + public abstract TypeElement interfaceElement(); + + public Name simpleName() { + return interfaceElement().getSimpleName(); + } + + public boolean isSimple() { + List<ExecutableElement> methods = methods(); + return methods.size() == 1 && methods.get(0).getParameters().size() < 2; + } + + public List<ExecutableElement> methods() { + return interfaceElement().getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind() == ElementKind.METHOD) + .sorted(Comparator.comparing(e -> e.getSimpleName().toString())) + .collect(toList()); + } + + public int getIdentifier(ExecutableElement method) { + return methods().indexOf(method); + } + + /** Get all types used by methods on this interface. */ + public Set<TypeMirror> argumentTypes() { + return methods().stream() + .flatMap(m -> m.getParameters().stream()) + .map(Element::asType) + .collect(Collectors.toSet()); + } + + public static CrossProfileCallbackInterfaceInfo create(TypeElement interfaceElement) { + return new AutoValue_CrossProfileCallbackInterfaceInfo(interfaceElement); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java new file mode 100644 index 0000000..a39c2e3 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationAnnotationInfo.java @@ -0,0 +1,54 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import javax.lang.model.element.TypeElement; + +/** + * Wrapper around information contained in an annotation of type {@link CrossProfileConfiguration}. + */ +@AutoValue +public abstract class CrossProfileConfigurationAnnotationInfo { + + public abstract ImmutableCollection<TypeElement> providerClasses(); + + public abstract TypeElement serviceSuperclass(); + + public abstract TypeElement serviceClass(); + + public abstract TypeElement connector(); + + public static Builder builder() { + return new AutoValue_CrossProfileConfigurationAnnotationInfo.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setProviderClasses(ImmutableCollection<TypeElement> value); + + public abstract Builder setServiceSuperclass(TypeElement value); + + public abstract Builder setServiceClass(TypeElement value); + + public abstract Builder setConnector(TypeElement value); + + public abstract CrossProfileConfigurationAnnotationInfo build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java new file mode 100644 index 0000000..2d4ed43 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationInfo.java @@ -0,0 +1,119 @@ +/* + * 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.containers; + +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Streams; +import com.squareup.javapoet.ClassName; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +/** Wrapper of a {@link CrossProfileConfiguration} annotated class. */ +@AutoValue +public abstract class CrossProfileConfigurationInfo { + + public static final String CROSS_PROFILE_CONNECTOR_QUALIFIED_NAME = + "com.google.android.enterprise.connectedapps.CrossProfileConnector"; + + public abstract TypeElement configurationElement(); + + public abstract ImmutableCollection<ProviderClassInfo> providers(); + + public abstract ClassName serviceSuperclass(); + + public abstract Optional<TypeElement> serviceClass(); + + public String simpleName() { + return configurationElement().getSimpleName().toString(); + } + + public ClassName className() { + return ClassName.get(configurationElement()); + } + + public abstract ProfileConnectorInfo profileConnector(); + + public static CrossProfileConfigurationInfo create( + ValidatorContext context, ValidatorCrossProfileConfigurationInfo configuration) { + Collection<ProviderClassInfo> providerClasses = + configuration.providerClassElements().stream() + .map( + m -> + ProviderClassInfo.create( + context, ValidatorProviderClassInfo.create(context.processingEnv(), m))) + .collect(toSet()); + + ProfileConnectorInfo profileConnectorInfo = + providerClasses.stream() + .flatMap(m -> m.allCrossProfileTypes().stream()) + .map(CrossProfileTypeInfo::profileConnector) + .flatMap(Streams::stream) + .findFirst() + .orElseGet( + () -> + ProfileConnectorInfo.create( + context.processingEnv(), + getConfiguredConnectorOrDefault(context, configuration), + context.globalSupportedTypes())); + + return new AutoValue_CrossProfileConfigurationInfo( + configuration.configurationElement(), + ImmutableSet.copyOf(providerClasses), + configuration.serviceSuperclass(), + configuration.serviceClass(), + profileConnectorInfo); + } + + private static TypeElement getConfiguredConnectorOrDefault( + ValidatorContext context, ValidatorCrossProfileConfigurationInfo configuration) { + return configuration + .connector() + .orElseGet(() -> context.elements().getTypeElement(CROSS_PROFILE_CONNECTOR_QUALIFIED_NAME)); + } + + private static Collection<Type> convertTypeMirrorToSupportedTypes( + SupportedTypes supportedTypes, TypeMirror typeMirror) { + if (TypeUtils.isGeneric(typeMirror)) { + return convertGenericTypeMirrorToSupportedTypes(supportedTypes, typeMirror); + } + return Collections.singleton(supportedTypes.getType(typeMirror)); + } + + private static Collection<Type> convertGenericTypeMirrorToSupportedTypes( + SupportedTypes supportedTypes, TypeMirror typeMirror) { + Collection<Type> types = new HashSet<>(); + TypeMirror genericType = TypeUtils.removeTypeArguments(typeMirror); + Type supportedType = supportedTypes.getType(genericType); + if (!supportedType.isSupportedWithAnyGenericType()) { + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(typeMirror)) { + types.addAll(convertTypeMirrorToSupportedTypes(supportedTypes, typeArgument)); + } + } + types.add(supportedTypes.getType(genericType)); + return types; + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java new file mode 100644 index 0000000..1060d68 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileConfigurationsAnnotationInfo.java @@ -0,0 +1,34 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfigurations; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; + +/** + * Wrapper around information contained in an annotation of type {@link CrossProfileConfigurations}. + */ +@AutoValue +public abstract class CrossProfileConfigurationsAnnotationInfo { + + public abstract ImmutableSet<CrossProfileConfigurationAnnotationInfo> configurations(); + + public static CrossProfileConfigurationsAnnotationInfo create( + ImmutableSet<CrossProfileConfigurationAnnotationInfo> configurations) { + return new AutoValue_CrossProfileConfigurationsAnnotationInfo(configurations); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java new file mode 100644 index 0000000..72831a9 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileMethodInfo.java @@ -0,0 +1,231 @@ +/* + * 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.containers; + +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileAnnotation; +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileCallbackAnnotation; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.annotations.CrossProfileCallback; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.auto.value.AutoValue; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.TypeName; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Function; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; + +/** Wrapper of a {@link CrossProfile} annotated method. */ +@AutoValue +public abstract class CrossProfileMethodInfo { + + public abstract ExecutableElement methodElement(); + + public abstract int identifier(); + + public abstract boolean isStatic(); + + public String simpleName() { + return methodElement().getSimpleName().toString(); + } + + public TypeMirror returnType() { + return methodElement().getReturnType(); + } + + public TypeName returnTypeTypeName() { + return ClassName.get(returnType()); + } + + public Collection<TypeName> thrownExceptions() { + return methodElement().getThrownTypes().stream() + .map(ClassName::get) + .collect(toSet()); + } + + public Collection<TypeMirror> automaticallyResolvedParameterTypes(SupportedTypes supportedTypes) { + return parameterTypes().stream() + .filter(supportedTypes::isAutomaticallyResolved) + .collect(toSet()); + } + + /** + * The number of milliseconds to timeout async calls. This is either set on the method, the type, + * or defaults to {@link CrossProfileAnnotation#DEFAULT_TIMEOUT_MILLIS}. + */ + public abstract long timeoutMillis(); + + /** + * Specify behaviour when encountering parameters of a type which is automatically resolved by the + * SDK. + */ + public enum AutomaticallyResolvedParameterFilterBehaviour { + /** Do not change the parameters. */ + LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS, + /** Remove the parameters and act as if they are not present. */ + REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS, + /** Replace the parameter with the variable specified in the type configuration. */ + REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS + } + + /** + * A string of parameter names separated by commas. + * + * <p>This is useful when generating a call for this method, using the same parameter names. + * + * <p>Parameters which are automatically resolved will be removed. + */ + public String commaSeparatedParameters( + SupportedTypes supportedTypes, + AutomaticallyResolvedParameterFilterBehaviour filterBehaviour) { + return commaSeparatedParameters(supportedTypes, filterBehaviour, Function.identity()); + } + + /** + * A string of parameter names separated by commas. + * + * <p>This is useful when generating a call for this method, using the same parameter names. + * + * <p>Parameters which are automatically resolved will be removed. + */ + public String commaSeparatedParameters( + SupportedTypes supportedTypes, + AutomaticallyResolvedParameterFilterBehaviour filterBehaviour, + Function<String, String> map) { + if (filterBehaviour + == AutomaticallyResolvedParameterFilterBehaviour.REMOVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return methodElement().getParameters().stream() + .filter(p -> !supportedTypes.isAutomaticallyResolved(p.asType())) + .map(p -> p.getSimpleName().toString()) + .map(map) + .collect(joining(", ")); + } else if (filterBehaviour + == AutomaticallyResolvedParameterFilterBehaviour + .REPLACE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return methodElement().getParameters().stream() + .map( + p -> + supportedTypes.isAutomaticallyResolved(p.asType()) + ? supportedTypes.getAutomaticallyResolvedReplacement(p.asType()) + : p.getSimpleName().toString()) + .map(map) + .collect(joining(", ")); + } else if (filterBehaviour + == AutomaticallyResolvedParameterFilterBehaviour.LEAVE_AUTOMATICALLY_RESOLVED_PARAMETERS) { + return methodElement().getParameters().stream() + .map(p -> p.getSimpleName().toString()) + .map(map) + .collect(joining(", ")); + } + throw new IllegalArgumentException("Invalid filter behaviour: " + filterBehaviour); + } + + /** An unordered collection of the types used in the parameters of this method. */ + public Collection<TypeMirror> parameterTypes() { + return methodElement().getParameters().stream().map(Element::asType).collect(toSet()); + } + + /** + * True if both {@link #isCrossProfileCallback(GeneratorContext)} and {@link + * #isFuture(CrossProfileTypeInfo)} are {@code False}. + */ + public boolean isBlocking(GeneratorContext context, CrossProfileTypeInfo type) { + return !isCrossProfileCallback(context) && !isFuture(type); + } + + /** True if any argument is annotated with {@link CrossProfileCallback}. */ + public boolean isCrossProfileCallback(GeneratorContext generatorContext) { + return getCrossProfileCallbackParam(generatorContext).isPresent(); + } + + /** True if there is only a single {@link CrossProfileCallback} argument and it is simple. */ + public boolean isSimpleCrossProfileCallback(GeneratorContext generatorContext) { + Optional<VariableElement> param = getCrossProfileCallbackParam(generatorContext); + + if (param.isPresent()) { + CrossProfileCallbackInterfaceInfo callbackInterface = + CrossProfileCallbackInterfaceInfo.create( + (TypeElement) generatorContext.types().asElement(param.get().asType())); + return callbackInterface.isSimple(); + } + + return false; + } + + /** True if the return type is a supported {@code Future} type. */ + public boolean isFuture(CrossProfileTypeInfo type) { + return isFuture(type.supportedTypes(), methodElement()); + } + + public static boolean isFuture(SupportedTypes supportedTypes, ExecutableElement method) { + return supportedTypes.isFuture(TypeUtils.removeTypeArguments(method.getReturnType())); + } + + /** Return the {@link CrossProfileCallback} annotated parameter, if any. */ + public Optional<VariableElement> getCrossProfileCallbackParam(GeneratorContext generatorContext) { + return getCrossProfileCallbackParam(generatorContext.elements(), methodElement()); + } + + public static Optional<VariableElement> getCrossProfileCallbackParam( + Elements elements, ExecutableElement method) { + return method.getParameters().stream() + .filter(v -> isCrossProfileCallbackInterface(elements, v.asType())) + .findFirst() + .map(e -> (VariableElement) e); + } + + private static boolean isCrossProfileCallbackInterface(Elements elements, TypeMirror type) { + TypeElement typeElement = elements.getTypeElement(type.toString()); + return typeElement != null && hasCrossProfileCallbackAnnotation(typeElement); + } + + public static CrossProfileMethodInfo create( + int identifier, + ValidatorCrossProfileTypeInfo type, + ExecutableElement methodElement, + Context context) { + return new AutoValue_CrossProfileMethodInfo( + methodElement, + identifier, + methodElement.getModifiers().contains(Modifier.STATIC), + findTimeoutMillis(type, methodElement, context)); + } + + private static long findTimeoutMillis( + ValidatorCrossProfileTypeInfo type, ExecutableElement methodElement, Context context) { + if (hasCrossProfileAnnotation(methodElement)) { + return AnnotationFinder.extractCrossProfileAnnotationInfo( + methodElement, context.types(), context.elements()) + .timeoutMillis() + .filter(timeout -> timeout > 0) + .orElse(type.timeoutMillis()); + } + + return type.timeoutMillis(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java new file mode 100644 index 0000000..ab05fff --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileProviderAnnotationInfo.java @@ -0,0 +1,33 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileProvider; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import javax.lang.model.element.TypeElement; + +/** Wrapper around information contained in an annotation of type {@link CrossProfileProvider}. */ +@AutoValue +public abstract class CrossProfileProviderAnnotationInfo { + + public abstract ImmutableCollection<TypeElement> staticTypes(); + + public static CrossProfileProviderAnnotationInfo create( + ImmutableCollection<TypeElement> staticTypes) { + return new AutoValue_CrossProfileProviderAnnotationInfo(staticTypes); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java new file mode 100644 index 0000000..f96a153 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestAnnotationInfo.java @@ -0,0 +1,39 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.auto.value.AutoValue; +import javax.lang.model.element.TypeElement; + +/** Wrapper around information contained in an annotation of type {@link CrossProfileTest}. */ +@AutoValue +public abstract class CrossProfileTestAnnotationInfo { + + public abstract TypeElement configuration(); + + public static Builder builder() { + return new AutoValue_CrossProfileTestAnnotationInfo.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setConfiguration(TypeElement value); + + public abstract CrossProfileTestAnnotationInfo build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java new file mode 100644 index 0000000..a2219be --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTestInfo.java @@ -0,0 +1,47 @@ +/* + * 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.containers; + +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import java.util.Set; +import javax.lang.model.element.TypeElement; + +/** Wrapper of a {@link CrossProfileTest} annotated class. */ +@AutoValue +public abstract class CrossProfileTestInfo { + + public abstract TypeElement crossProfileTestElement(); + + public abstract ImmutableSet<CrossProfileConfigurationInfo> configurations(); + + public static CrossProfileTestInfo create( + ValidatorContext context, ValidatorCrossProfileTestInfo validatorCrossProfileTest) { + + Set<CrossProfileConfigurationInfo> configurations = + ValidatorCrossProfileConfigurationInfo.createMultipleFromElement( + context.processingEnv(), validatorCrossProfileTest.configurationElement()) + .stream() + .map(b -> CrossProfileConfigurationInfo.create(context, b)) + .collect(toSet()); + + return new AutoValue_CrossProfileTestInfo( + validatorCrossProfileTest.crossProfileTestElement(), ImmutableSet.copyOf(configurations)); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java new file mode 100644 index 0000000..fffe4a1 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/CrossProfileTypeInfo.java @@ -0,0 +1,171 @@ +/* + * 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.containers; + +import static com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder.hasCrossProfileAnnotation; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.ProcessorConfiguration; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.google.common.hash.Hashing; +import com.squareup.javapoet.ClassName; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; + +/** Wrapper of a {@link CrossProfile} type. */ +@AutoValue +public abstract class CrossProfileTypeInfo { + + public abstract TypeElement crossProfileTypeElement(); + + public abstract ImmutableCollection<CrossProfileMethodInfo> crossProfileMethods(); + + public abstract SupportedTypes supportedTypes(); + + public abstract Optional<ProfileConnectorInfo> profileConnector(); + + public abstract ClassName profileClassName(); + + /** + * The specified timeout for async calls, or {@link CrossProfileAnnotation#DEFAULT_TIMEOUT_MILLIS} + * if unspecified. + */ + public abstract long timeoutMillis(); + + public String simpleName() { + return crossProfileTypeElement().getSimpleName().toString(); + } + + public ClassName className() { + return ClassName.get(crossProfileTypeElement()); + } + + public boolean isStatic() { + return crossProfileMethods().stream().allMatch(CrossProfileMethodInfo::isStatic); + } + + /** + * Get a numeric identifier for the cross-profile type. + * + * <p>This identifier is based on the type's qualified name, and will not change between runs. + */ + public long identifier() { + // Stored in a 64 bit long, with ~200 cross-profile types, chance of collision is 1 in 10^15 + return Hashing.murmur3_128() + .hashString(crossProfileTypeElement().getQualifiedName().toString(), StandardCharsets.UTF_8) + .asLong(); + } + + public static CrossProfileTypeInfo create( + ValidatorContext context, ValidatorCrossProfileTypeInfo crossProfileType) { + TypeElement crossProfileTypeElement = crossProfileType.crossProfileTypeElement(); + + List<ExecutableElement> crossProfileMethodElements = crossProfileType.crossProfileMethods(); + + Collection<CrossProfileMethodInfo> crossProfileMethods = + IntStream.range(0, crossProfileMethodElements.size()) + .mapToObj( + t -> + CrossProfileMethodInfo.create( + t, crossProfileType, crossProfileMethodElements.get(t), context)) + .collect(toSet()); + + SupportedTypes.Builder supportedTypesBuilder = crossProfileType.supportedTypes().asBuilder(); + + supportedTypesBuilder.filterUsed(context, crossProfileMethods); + + if (ProcessorConfiguration.GENERATE_TYPE_SPECIFIC_WRAPPERS) { + supportedTypesBuilder.replaceWrapperPrefix( + ClassName.bestGuess( + crossProfileType.crossProfileTypeElement().getQualifiedName().toString())); + } + + return new AutoValue_CrossProfileTypeInfo( + crossProfileTypeElement, + ImmutableSet.copyOf(crossProfileMethods), + supportedTypesBuilder.build(), + crossProfileType.profileConnector(), + findProfileClassName(context, crossProfileTypeElement, crossProfileType), + crossProfileType.timeoutMillis()); + } + + private static ClassName findProfileClassName( + ValidatorContext context, + TypeElement typeElement, + ValidatorCrossProfileTypeInfo crossProfileType) { + return hasCrossProfileAnnotation(typeElement) + ? findAnnotatedProfileClassName(context, typeElement, crossProfileType) + : createDefaultProfileClassName(context, typeElement); + } + + private static ClassName createDefaultProfileClassName( + ValidatorContext context, TypeElement typeElement) { + PackageElement originalPackage = context.elements().getPackageOf(typeElement); + String profileAwareClassName = + String.format("Profile%s", typeElement.getSimpleName().toString()); + + return ClassName.get(originalPackage.getQualifiedName().toString(), profileAwareClassName); + } + + private static ClassName findAnnotatedProfileClassName( + ValidatorContext context, + TypeElement typeElement, + ValidatorCrossProfileTypeInfo crossProfileType) { + String profileClassName = crossProfileType.profileClassName(); + if (!profileClassName.isEmpty()) { + return ClassName.bestGuess(profileClassName); + } + + return createDefaultProfileClassName(context, typeElement); + } + + private static Collection<Type> convertTypeMirrorToSupportedTypes( + SupportedTypes supportedTypes, TypeMirror typeMirror) { + if (TypeUtils.isGeneric(typeMirror)) { + return convertGenericTypeMirrorToSupportedTypes(supportedTypes, typeMirror); + } + return Collections.singleton(supportedTypes.getType(typeMirror)); + } + + private static Collection<Type> convertGenericTypeMirrorToSupportedTypes( + SupportedTypes supportedTypes, TypeMirror typeMirror) { + Collection<Type> types = new HashSet<>(); + TypeMirror genericType = TypeUtils.removeTypeArguments(typeMirror); + Type supportedType = supportedTypes.getType(genericType); + if (!supportedType.isSupportedWithAnyGenericType()) { + for (TypeMirror typeArgument : TypeUtils.extractTypeArguments(typeMirror)) { + types.addAll(convertTypeMirrorToSupportedTypes(supportedTypes, typeArgument)); + } + } + types.add(supportedTypes.getType(genericType)); + return types; + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java new file mode 100644 index 0000000..5208e7c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapper.java @@ -0,0 +1,139 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper; +import com.google.auto.value.AutoValue; +import com.squareup.javapoet.ClassName; +import java.util.ArrayList; +import java.util.Collection; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Information about future wrapper. */ +@AutoValue +public abstract class FutureWrapper { + + /** The type of the Wrapper. This controls how supporting code is generated. */ + public enum WrapperType { + DEFAULT, // Copied from a resource + CUSTOM // Included in classpath + } + + public static final String FUTURE_WRAPPER_PACKAGE = + "com.google.android.enterprise.connectedapps.futurewrappers"; + + public abstract TypeMirror wrappedType(); + + public abstract ClassName defaultWrapperClassName(); + + public abstract ClassName wrapperClassName(); + + public abstract WrapperType wrapperType(); + + private static FutureWrapper create( + TypeMirror wrappedType, ClassName defaultWrapperClassName, WrapperType wrapperType) { + return create(wrappedType, defaultWrapperClassName, defaultWrapperClassName, wrapperType); + } + + public static FutureWrapper create( + TypeMirror wrappedType, + ClassName defaultWrapperClassName, + ClassName wrapperClassName, + WrapperType wrapperType) { + return new AutoValue_FutureWrapper( + wrappedType, defaultWrapperClassName, wrapperClassName, wrapperType); + } + + public static Collection<FutureWrapper> createGlobalFutureWrappers(Elements elements) { + Collection<FutureWrapper> wrappers = new ArrayList<>(); + + addDefaultFutureWrappers(elements, wrappers); + + return wrappers; + } + + private static void addDefaultFutureWrappers( + Elements elements, Collection<FutureWrapper> wrappers) { + tryAddWrapper( + elements, + wrappers, + "com.google.common.util.concurrent.ListenableFuture", + ClassName.get(FUTURE_WRAPPER_PACKAGE, "ListenableFutureWrapper"), + WrapperType.DEFAULT); + } + + public static Collection<FutureWrapper> createCustomFutureWrappers( + Types types, Elements elements, Collection<TypeElement> customFutureWrappers) { + Collection<FutureWrapper> wrappers = new ArrayList<>(); + + addCustomFutureWrappers(types, elements, wrappers, customFutureWrappers); + + return wrappers; + } + + private static void addCustomFutureWrappers( + Types types, + Elements elements, + Collection<FutureWrapper> wrappers, + Collection<TypeElement> customFutureWrappers) { + for (TypeElement customFutureWrapper : customFutureWrappers) { + addCustomFutureWrapper(types, elements, wrappers, customFutureWrapper); + } + } + + private static void addCustomFutureWrapper( + Types types, + Elements elements, + Collection<FutureWrapper> wrappers, + TypeElement customFutureWrapper) { + CustomFutureWrapper customFutureWrapperAnnotation = + customFutureWrapper.getAnnotation(CustomFutureWrapper.class); + + if (customFutureWrapperAnnotation == null) { + // This will be dealt with as part of early validation + return; + } + + tryAddWrapper( + elements, + wrappers, + FutureWrapperAnnotationInfo.extractFromFutureWrapperAnnotation( + types, customFutureWrapperAnnotation) + .originalType() + .toString(), + ClassName.get(customFutureWrapper), + WrapperType.CUSTOM); + } + + private static void tryAddWrapper( + Elements elements, + Collection<FutureWrapper> wrappers, + String typeQualifiedName, + ClassName wrapperClassName, + WrapperType wrapperType) { + TypeElement typeElement = elements.getTypeElement(typeQualifiedName); + + if (typeElement == null) { + // The type isn't supported at compile-time - so won't be included in this app + return; + } + + wrappers.add(FutureWrapper.create(typeElement.asType(), wrapperClassName, wrapperType)); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java new file mode 100644 index 0000000..18fb34b --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/FutureWrapperAnnotationInfo.java @@ -0,0 +1,45 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper; +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.auto.value.AutoValue; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Types; + +/** + * Wrapper around information contained in a {@link + * com.google.android.enterprise.connectedapps.annotations.CustomFutureWrapper} annotation. + */ +@AutoValue +public abstract class FutureWrapperAnnotationInfo { + + public abstract TypeElement originalType(); + + public static FutureWrapperAnnotationInfo extractFromFutureWrapperAnnotation( + Types types, CustomFutureWrapper customFutureWrapperAnnotation) { + if (customFutureWrapperAnnotation == null) { + throw new NullPointerException("customFutureWrapperAnnotation must not be null"); + } + + TypeElement originalType = + GeneratorUtilities.extractClassFromAnnotation( + types, customFutureWrapperAnnotation::originalType); + + return new AutoValue_FutureWrapperAnnotationInfo(originalType); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java new file mode 100644 index 0000000..ac47218 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/GeneratorContext.java @@ -0,0 +1,142 @@ +/* + * 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.containers; + +import static java.util.stream.Collectors.toSet; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Context for connected apps code generators. */ +@AutoValue +public abstract class GeneratorContext extends Context { + + public static GeneratorContext createFromValidatorContext(ValidatorContext validatorContext) { + Collection<CrossProfileConfigurationInfo> configurations = + validatorContext.newConfigurations().stream() + .map(a -> CrossProfileConfigurationInfo.create(validatorContext, a)) + .collect(toSet()); + + Collection<ProviderClassInfo> providers = + validatorContext.newProviderClasses().stream() + .map(m -> ProviderClassInfo.create(validatorContext, m)) + .collect(toSet()); + + Collection<CrossProfileTypeInfo> crossProfileTypes = + validatorContext.newCrossProfileTypes().stream() + .map(m -> CrossProfileTypeInfo.create(validatorContext, m)) + .collect(toSet()); + + Collection<CrossProfileCallbackInterfaceInfo> crossProfileCallbackInterfaces = + validatorContext.newCrossProfileCallbackInterfaces().stream() + .map(CrossProfileCallbackInterfaceInfo::create) + .collect(toSet()); + + Collection<ProfileConnectorInfo> generatedProfileConnectors = + validatorContext.newGeneratedProfileConnectors().stream() + .map( + t -> + ProfileConnectorInfo.create( + validatorContext.processingEnv(), + t, + validatorContext.globalSupportedTypes())) + .collect(toSet()); + + Collection<UserConnectorInfo> generatedUserConnectors = + validatorContext.newGeneratedUserConnectors().stream() + .map( + t -> + UserConnectorInfo.create( + validatorContext.processingEnv(), + t, + validatorContext.globalSupportedTypes())) + .collect(toSet()); + + Collection<CrossProfileTestInfo> crossProfileTests = + validatorContext.newCrossProfileTests().stream() + .map(t -> CrossProfileTestInfo.create(validatorContext, t)) + .collect(toSet()); + + return GeneratorContext.builder() + .setProcessingEnv(validatorContext.processingEnv()) + .setElements(validatorContext.elements()) + .setTypes(validatorContext.types()) + .setConfigurations(configurations) + .setGeneratedProfileConnectors(generatedProfileConnectors) + .setGeneratedUserConnectors(generatedUserConnectors) + .setProviders(providers) + .setCrossProfileTypes(crossProfileTypes) + .setCrossProfileMethods(validatorContext.newCrossProfileMethods()) + .setCrossProfileCallbackInterfaces(crossProfileCallbackInterfaces) + .setCrossProfileTests(crossProfileTests) + .build(); + } + + static Builder builder() { + return new AutoValue_GeneratorContext.Builder(); + } + + public abstract ImmutableSet<CrossProfileConfigurationInfo> configurations(); + + public abstract ImmutableSet<ProfileConnectorInfo> generatedProfileConnectors(); + + public abstract ImmutableSet<UserConnectorInfo> generatedUserConnectors(); + + public abstract ImmutableSet<ProviderClassInfo> providers(); + + public abstract ImmutableSet<CrossProfileTypeInfo> crossProfileTypes(); + + public abstract ImmutableSet<ExecutableElement> crossProfileMethods(); + + public abstract ImmutableSet<CrossProfileCallbackInterfaceInfo> crossProfileCallbackInterfaces(); + + public abstract ImmutableSet<CrossProfileTestInfo> crossProfileTests(); + + @AutoValue.Builder + abstract static class Builder { + abstract Builder setProcessingEnv(ProcessingEnvironment processingEnv); + + abstract Builder setElements(Elements elements); + + abstract Builder setTypes(Types types); + + abstract Builder setConfigurations(Collection<CrossProfileConfigurationInfo> configurations); + + abstract Builder setGeneratedProfileConnectors( + Collection<ProfileConnectorInfo> generatedProfileConnectors); + + abstract Builder setGeneratedUserConnectors( + Collection<UserConnectorInfo> generatedUserConnectors); + + abstract Builder setProviders(Collection<ProviderClassInfo> providers); + + abstract Builder setCrossProfileTypes(Collection<CrossProfileTypeInfo> crossProfileTypes); + + abstract Builder setCrossProfileMethods(Collection<ExecutableElement> crossProfileMethods); + + abstract Builder setCrossProfileCallbackInterfaces( + Collection<CrossProfileCallbackInterfaceInfo> crossProfileCallbackInterfaces); + + abstract Builder setCrossProfileTests(Collection<CrossProfileTestInfo> crossProfileTests); + + abstract GeneratorContext build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java new file mode 100644 index 0000000..114abfb --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapper.java @@ -0,0 +1,274 @@ +/* + * 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.containers; + +import static com.google.android.enterprise.connectedapps.processor.ProtoParcelableWrapperGenerator.getGeneratedProtoWrapperClassName; +import static java.util.stream.Collectors.toSet; + +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.auto.value.AutoValue; +import com.squareup.javapoet.ClassName; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** Information about a Parcelable Wrapper. */ +@AutoValue +public abstract class ParcelableWrapper { + + /** The type of the Wrapper. This controls how supporting code is generated. */ + public enum WrapperType { + DEFAULT, // Copied from a resource + PROTO, // Generated by ProtoParcelableWrapperGenerator + CUSTOM // Included in classpath + } + + public static final String PARCELABLE_WRAPPER_PACKAGE = + "com.google.android.enterprise.connectedapps.parcelablewrappers"; + + public abstract TypeMirror wrappedType(); + + public abstract ClassName defaultWrapperClassName(); + + public abstract ClassName wrapperClassName(); + + public abstract WrapperType wrapperType(); + + private static ParcelableWrapper create( + TypeMirror wrappedType, ClassName defaultWrapperClassName, WrapperType wrapperType) { + return create(wrappedType, defaultWrapperClassName, defaultWrapperClassName, wrapperType); + } + + public static ParcelableWrapper create( + TypeMirror wrappedType, + ClassName defaultWrapperClassName, + ClassName wrapperClassName, + WrapperType wrapperType) { + return new AutoValue_ParcelableWrapper( + wrappedType, defaultWrapperClassName, wrapperClassName, wrapperType); + } + + public static Collection<ParcelableWrapper> createCustomParcelableWrappers( + Types types, Elements elements, Collection<TypeElement> customParcelableWrappers) { + Collection<ParcelableWrapper> wrappers = new ArrayList<>(); + + addCustomParcelableWrappers(types, wrappers, customParcelableWrappers); + + return wrappers; + } + + public static Collection<ParcelableWrapper> createGlobalParcelableWrappers( + Types types, Elements elements, Collection<ExecutableElement> methods) { + Collection<ParcelableWrapper> wrappers = new ArrayList<>(); + + addDefaultParcelableWrappers(types, elements, wrappers); + + Collection<TypeMirror> usedTypes = extractTypesFromMethods(methods); + + addGeneratedProtoParcelableWrappers(types, elements, wrappers, usedTypes); + + return wrappers; + } + + private static Collection<TypeMirror> extractTypesFromMethods( + Collection<ExecutableElement> methods) { + return methods.stream() + .flatMap(m -> extractReturnTypeAndParameters(m).stream()) + .flatMap(t -> extractTypeArgumentsIfWrapped(t).stream()) + .collect(toSet()); + } + + private static Collection<TypeMirror> extractReturnTypeAndParameters(ExecutableElement method) { + Collection<TypeMirror> types = new HashSet<>(); + types.add(method.getReturnType()); + types.addAll(method.getParameters().stream().map(Element::asType).collect(toSet())); + return types; + } + + private static Collection<TypeMirror> extractTypeArgumentsIfWrapped(TypeMirror type) { + if (TypeUtils.isGeneric(type)) { + return extractTypeArgumentsFromGeneric(type); + } + if (TypeUtils.isArray(type)) { + return extractTypeArgumentsIfWrapped(TypeUtils.extractTypeFromArray(type)); + } + + return Collections.singleton(type); + } + + private static Collection<TypeMirror> extractTypeArgumentsFromGeneric(TypeMirror type) { + Collection<TypeMirror> types = new HashSet<>(); + types.add(TypeUtils.removeTypeArguments(type)); + + types.addAll( + TypeUtils.extractTypeArguments(type).stream() + .flatMap(t -> extractTypeArgumentsIfWrapped(t).stream()) + .collect(toSet())); + return types; + } + + private static void addCustomParcelableWrappers( + Types types, + Collection<ParcelableWrapper> wrappers, + Collection<TypeElement> customParcelableWrappers) { + for (TypeElement parcelableWrapper : customParcelableWrappers) { + addCustomParcelableWrapper(types, wrappers, parcelableWrapper); + } + } + + private static void addCustomParcelableWrapper( + Types types, Collection<ParcelableWrapper> wrappers, TypeElement parcelableWrapper) { + + CustomParcelableWrapper customParcelableWrapperAnnotation = + parcelableWrapper.getAnnotation(CustomParcelableWrapper.class); + + if (customParcelableWrapperAnnotation == null) { + // This will be dealt with as part of early validation + return; + } + + ParcelableWrapperAnnotationInfo annotationInfo = + ParcelableWrapperAnnotationInfo.extractFromParcelableWrapperAnnotation( + types, customParcelableWrapperAnnotation); + wrappers.add( + ParcelableWrapper.create( + annotationInfo.originalType().asType(), + ClassName.get(parcelableWrapper), + WrapperType.CUSTOM)); + } + + private static void addDefaultParcelableWrappers( + Types types, Elements elements, Collection<ParcelableWrapper> wrappers) { + tryAddWrapper( + elements, + wrappers, + "java.util.Collection", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableCollection")); + + tryAddWrapper( + elements, + wrappers, + "java.util.List", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableList")); + + tryAddWrapper( + elements, + wrappers, + "java.util.Map", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableMap")); + + tryAddWrapper( + elements, + wrappers, + "java.util.Set", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableSet")); + + tryAddWrapper( + elements, + wrappers, + "java.util.Optional", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableOptional")); + + tryAddWrapper( + elements, + wrappers, + "com.google.common.base.Optional", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableGuavaOptional")); + + tryAddWrapper( + elements, + wrappers, + "com.google.common.collect.ImmutableMap", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableImmutableMap")); + + tryAddWrapper( + elements, + wrappers, + "android.util.Pair", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelablePair")); + + tryAddWrapper( + elements, + wrappers, + "android.graphics.Bitmap", + ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableBitmap")); + + addArrayWrappers(types, elements, wrappers); + } + + private static void addGeneratedProtoParcelableWrappers( + Types types, + Elements elements, + Collection<ParcelableWrapper> wrappers, + Collection<TypeMirror> usedTypes) { + TypeElement protoElement = elements.getTypeElement("com.google.protobuf.MessageLite"); + if (protoElement == null) { + // Protos are not included at compile-time + return; + } + TypeMirror proto = protoElement.asType(); + + Collection<TypeMirror> protoTypes = + usedTypes.stream() + // <any> is the value when the compiler encounters a type which isn't accessible + // or does not exist. This passes the types.isAssignable filter, which makes such + // bugs hard to debug. This will already fail because the Java compiler won't allow + // it - so this is just to suppress strange test failures + .filter(t -> !t.toString().equals("<any>")) + .filter(t -> types.isAssignable(t, proto)) + .collect(toSet()); + + for (TypeMirror protoType : protoTypes) { + wrappers.add( + ParcelableWrapper.create( + protoType, getGeneratedProtoWrapperClassName(protoType), WrapperType.PROTO)); + } + } + + private static void addArrayWrappers( + Types types, Elements elements, Collection<ParcelableWrapper> wrappers) { + TypeElement typeElement = elements.getTypeElement("java.lang.Object"); + TypeMirror typeMirror = types.getArrayType(typeElement.asType()); + + ClassName wrapperClassName = ClassName.get(PARCELABLE_WRAPPER_PACKAGE, "ParcelableArray"); + + wrappers.add(ParcelableWrapper.create(typeMirror, wrapperClassName, WrapperType.DEFAULT)); + } + + private static void tryAddWrapper( + Elements elements, + Collection<ParcelableWrapper> wrappers, + String typeQualifiedName, + ClassName wrapperClassName) { + TypeElement typeElement = elements.getTypeElement(typeQualifiedName); + + if (typeElement == null) { + // The type isn't supported at compile-time - so won't be included in this app + return; + } + + wrappers.add( + ParcelableWrapper.create(typeElement.asType(), wrapperClassName, WrapperType.DEFAULT)); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java new file mode 100644 index 0000000..d9e7949 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ParcelableWrapperAnnotationInfo.java @@ -0,0 +1,42 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.CustomParcelableWrapper; +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.auto.value.AutoValue; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Types; + +/** Wrapper around information contained in a {@link CustomParcelableWrapper} annotation. */ +@AutoValue +public abstract class ParcelableWrapperAnnotationInfo { + + public abstract TypeElement originalType(); + + public static ParcelableWrapperAnnotationInfo extractFromParcelableWrapperAnnotation( + Types types, CustomParcelableWrapper customParcelableWrapperAnnotation) { + if (customParcelableWrapperAnnotation == null) { + throw new NullPointerException("parcelableWrapperAnnotation must not be null"); + } + + TypeElement originalType = + GeneratorUtilities.extractClassFromAnnotation( + types, customParcelableWrapperAnnotation::originalType); + + return new AutoValue_ParcelableWrapperAnnotationInfo(originalType); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java new file mode 100644 index 0000000..9a68099 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProfileConnectorInfo.java @@ -0,0 +1,160 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector; +import com.google.android.enterprise.connectedapps.annotations.CustomProfileConnector.ProfileType; +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; + +/** Wrapper of an interface used as a profile connector. */ +@AutoValue +public abstract class ProfileConnectorInfo { + + @AutoValue + abstract static class CustomProfileConnectorAnnotationInfo { + abstract ProfileType primaryProfile(); + + abstract ClassName serviceName(); + + abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + abstract ImmutableCollection<TypeElement> importsClasses(); + + abstract AvailabilityRestrictions availabilityRestrictions(); + } + + public abstract TypeElement connectorElement(); + + public ClassName connectorClassName() { + return ClassName.get(connectorElement()); + } + + public abstract ProfileType primaryProfile(); + + public abstract ClassName serviceName(); + + public abstract SupportedTypes supportedTypes(); + + public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> importsClasses(); + + public abstract AvailabilityRestrictions availabilityRestrictions(); + + public static ProfileConnectorInfo create( + ProcessingEnvironment processingEnv, + TypeElement connectorElement, + SupportedTypes globalSupportedTypes) { + + Elements elements = processingEnv.getElementUtils(); + + CustomProfileConnectorAnnotationInfo annotationInfo = + extractFromCustomProfileConnectorAnnotation(processingEnv, elements, connectorElement); + + Set<TypeElement> parcelableWrappers = new HashSet<>(annotationInfo.parcelableWrapperClasses()); + Set<TypeElement> futureWrappers = new HashSet<>(annotationInfo.futureWrapperClasses()); + + for (TypeElement importConnectorClass : annotationInfo.importsClasses()) { + ProfileConnectorInfo importConnector = + ProfileConnectorInfo.create(processingEnv, importConnectorClass, globalSupportedTypes); + parcelableWrappers.addAll(importConnector.parcelableWrapperClasses()); + futureWrappers.addAll(importConnector.futureWrapperClasses()); + } + + return new AutoValue_ProfileConnectorInfo( + connectorElement, + annotationInfo.primaryProfile(), + annotationInfo.serviceName(), + globalSupportedTypes + .asBuilder() + .addParcelableWrappers( + ParcelableWrapper.createCustomParcelableWrappers( + processingEnv.getTypeUtils(), + processingEnv.getElementUtils(), + parcelableWrappers)) + .addFutureWrappers( + FutureWrapper.createCustomFutureWrappers( + processingEnv.getTypeUtils(), processingEnv.getElementUtils(), futureWrappers)) + .build(), + ImmutableSet.copyOf(parcelableWrappers), + ImmutableSet.copyOf(futureWrappers), + annotationInfo.importsClasses(), + annotationInfo.availabilityRestrictions()); + } + + private static CustomProfileConnectorAnnotationInfo extractFromCustomProfileConnectorAnnotation( + ProcessingEnvironment processingEnv, Elements elements, TypeElement connectorElement) { + CustomProfileConnector customProfileConnector = + connectorElement.getAnnotation(CustomProfileConnector.class); + + if (customProfileConnector == null) { + return new AutoValue_ProfileConnectorInfo_CustomProfileConnectorAnnotationInfo( + ProfileType.NONE, + getDefaultServiceName(elements, connectorElement), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableSet.of(), + AvailabilityRestrictions.DEFAULT); + } + + Collection<TypeElement> parcelableWrappers = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customProfileConnector::parcelableWrappers); + Collection<TypeElement> futureWrappers = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customProfileConnector::futureWrappers); + Collection<TypeElement> imports = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customProfileConnector::imports); + + String serviceClassName = customProfileConnector.serviceClassName(); + + return new AutoValue_ProfileConnectorInfo_CustomProfileConnectorAnnotationInfo( + customProfileConnector.primaryProfile(), + serviceClassName.isEmpty() + ? getDefaultServiceName(elements, connectorElement) + : ClassName.bestGuess(serviceClassName), + ImmutableSet.copyOf(parcelableWrappers), + ImmutableSet.copyOf(futureWrappers), + ImmutableSet.copyOf(imports), + customProfileConnector.availabilityRestrictions()); + } + + public static ClassName getDefaultServiceName(Elements elements, TypeElement connectorElement) { + PackageElement originalPackage = elements.getPackageOf(connectorElement); + + return ClassName.get( + originalPackage.getQualifiedName().toString(), + String.format("%s_Service", connectorElement.getSimpleName().toString())); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java new file mode 100644 index 0000000..02adf0c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ProviderClassInfo.java @@ -0,0 +1,130 @@ +/* + * 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.containers; + +import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.findCrossProfileProviderMethodsInClass; +import static java.util.stream.Collectors.toSet; + +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.util.Elements; + +/** Wrapper of a cross-profile provider class. */ +@AutoValue +public abstract class ProviderClassInfo { + + public abstract TypeElement providerClassElement(); + + public ImmutableCollection<CrossProfileTypeInfo> allCrossProfileTypes() { + Set<CrossProfileTypeInfo> types = new HashSet<>(); + types.addAll(nonStaticTypes()); + types.addAll(staticTypes()); + return ImmutableSet.copyOf(types); + } + + public abstract ImmutableCollection<CrossProfileTypeInfo> nonStaticTypes(); + + public abstract ImmutableCollection<CrossProfileTypeInfo> staticTypes(); + + public String simpleName() { + return providerClassElement().getSimpleName().toString(); + } + + public ClassName className() { + return ClassName.get(providerClassElement()); + } + + public ImmutableCollection<VariableElement> publicConstructorArgumentTypes() { + return ImmutableList.copyOf( + providerClassElement().getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter(e -> e.getKind().equals(ElementKind.CONSTRUCTOR)) + .filter(e -> e.getModifiers().contains(Modifier.PUBLIC)) + .findFirst() + .get() + .getParameters()); + } + + public ExecutableElement findProviderMethodFor( + GeneratorContext generatorContext, CrossProfileTypeInfo crossProfileType) { + if (!nonStaticTypes().contains(crossProfileType)) { + throw new IllegalArgumentException("This provider class does not provide this type"); + } + + return providerClassElement().getEnclosedElements().stream() + .filter(e -> e instanceof ExecutableElement) + .map(e -> (ExecutableElement) e) + .filter( + e -> + generatorContext + .types() + .isSameType( + e.getReturnType(), crossProfileType.crossProfileTypeElement().asType())) + .findFirst() + .get(); + } + + public static ProviderClassInfo create( + ValidatorContext context, ValidatorProviderClassInfo provider) { + Set<CrossProfileTypeInfo> nonStaticTypes = + extractCrossProfileTypeElementsFromReturnValues( + context.elements(), provider.providerClassElement()) + .stream() + .map( + crossProfileTypeElement -> + ValidatorCrossProfileTypeInfo.create( + context.processingEnv(), + crossProfileTypeElement, + context.globalSupportedTypes())) + .map(crossProfileType -> CrossProfileTypeInfo.create(context, crossProfileType)) + .collect(toSet()); + + Set<CrossProfileTypeInfo> staticTypes = + provider.staticTypes().stream() + .map( + crossProfileTypeElement -> + ValidatorCrossProfileTypeInfo.create( + context.processingEnv(), + crossProfileTypeElement, + context.globalSupportedTypes())) + .map(crossProfileType -> CrossProfileTypeInfo.create(context, crossProfileType)) + .collect(toSet()); + + return new AutoValue_ProviderClassInfo( + provider.providerClassElement(), + ImmutableSet.copyOf(nonStaticTypes), + ImmutableSet.copyOf(staticTypes)); + } + + public static Collection<TypeElement> extractCrossProfileTypeElementsFromReturnValues( + Elements elements, TypeElement providerClassElement) { + return findCrossProfileProviderMethodsInClass(providerClassElement).stream() + .map(e -> elements.getTypeElement(e.getReturnType().toString())) + .collect(toSet()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java new file mode 100644 index 0000000..d81f6cd --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/Type.java @@ -0,0 +1,136 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.processor.TypeUtils; +import com.google.auto.value.AutoValue; +import java.util.Optional; +import javax.lang.model.type.TypeMirror; + +/** A type which may be supported by a given {@code CrossProfileConfiguration}. */ +@AutoValue +public abstract class Type { + public static Builder builder() { + return new AutoValue_Type.Builder() + .setAcceptableParameterType(false) + .setAcceptableReturnType(false) + .setSupportedWithAnyGenericType(false) + .setSupportedInsideWrapper(true) + .setSupportedInsideCrossProfileCallback(true); + } + + public abstract Builder toBuilder(); + + public abstract TypeMirror getTypeMirror(); + + public String getQualifiedName() { + return getTypeMirror().toString(); + } + + public abstract boolean isAcceptableReturnType(); + + public abstract boolean isAcceptableParameterType(); + + public abstract Optional<String> getAutomaticallyResolvedReplacement(); + + public boolean isArray() { + return TypeUtils.isArray(getTypeMirror()); + } + + public boolean canBeBundled() { + return getWriteToParcelCode().isPresent() && getReadFromParcelCode().isPresent(); + } + + public boolean isPrimitive() { + return getTypeMirror().getKind().isPrimitive(); + } + + public boolean isGeneric() { + return TypeUtils.isGeneric(getTypeMirror()); + } + + /** + * If this is set, then type arguments will not validated. + * + * <p>This allows for Parcelables which take responsibility for their own generics and do not use + * Bundler. + */ + public abstract boolean isSupportedWithAnyGenericType(); + + /** + * Can this type be used inside a wrapper type? For example a List or an array. + * + * <p>This allows for async listeners to only be acceptable as parameter types but not type + * arguments + */ + public abstract boolean isSupportedInsideWrapper(); + + public abstract boolean isSupportedInsideCrossProfileCallback(); + + public abstract Optional<FutureWrapper> getFutureWrapper(); + + public boolean isFuture() { + return getFutureWrapper().isPresent(); + } + + public abstract Optional<CrossProfileCallbackInterfaceInfo> getCrossProfileCallbackInterface(); + + public boolean isCrossProfileCallbackInterface() { + return getCrossProfileCallbackInterface().isPresent(); + } + + // If this is a generated Parcelable Wrapper then this will be set to the simple name + // (e.g. ParcelableList) + public abstract Optional<ParcelableWrapper> getParcelableWrapper(); + + public abstract Optional<String> getWriteToParcelCode(); + + public abstract Optional<String> getReadFromParcelCode(); + + /** A builder for {@link Type}. */ + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setTypeMirror(TypeMirror typeMirror); + + public abstract Builder setAcceptableReturnType(boolean acceptableReturnType); + + public abstract Builder setAcceptableParameterType(boolean acceptableParameterType); + + public abstract Builder setAutomaticallyResolvedReplacement( + String automaticallyResolvedReplacement); + + public abstract Builder setSupportedWithAnyGenericType(boolean supportedWithAnyGenericType); + + public abstract Builder setSupportedInsideWrapper(boolean supportedInsideWrapper); + + public abstract Builder setSupportedInsideCrossProfileCallback( + boolean supportedInsideCrossProfileCallback); + + public abstract Builder setFutureWrapper(FutureWrapper futureWrapper); + + public abstract Builder setCrossProfileCallbackInterface( + CrossProfileCallbackInterfaceInfo crossProfileCallbackInterface); + + public abstract Builder setWriteToParcelCode(String writeToParcelCode); + + public abstract Builder setReadFromParcelCode(String readFromParcelCode); + + public abstract Builder setParcelableWrapper(ParcelableWrapper parcelableWrapper); + + public abstract Type build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java new file mode 100644 index 0000000..33a4d56 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/UserConnectorInfo.java @@ -0,0 +1,150 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.annotations.AvailabilityRestrictions; +import com.google.android.enterprise.connectedapps.annotations.CustomUserConnector; +import com.google.android.enterprise.connectedapps.processor.GeneratorUtilities; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; + +/** Wrapper of an interface used as a user connector. */ +@AutoValue +public abstract class UserConnectorInfo { + + @AutoValue + abstract static class CustomUserConnectorAnnotationInfo { + abstract ClassName serviceName(); + + abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + abstract ImmutableCollection<TypeElement> importsClasses(); + + abstract AvailabilityRestrictions availabilityRestrictions(); + } + + public abstract TypeElement connectorElement(); + + public ClassName connectorClassName() { + return ClassName.get(connectorElement()); + } + + public abstract ClassName serviceName(); + + public abstract SupportedTypes supportedTypes(); + + public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> importsClasses(); + + public abstract AvailabilityRestrictions availabilityRestrictions(); + + public static UserConnectorInfo create( + ProcessingEnvironment processingEnv, + TypeElement connectorElement, + SupportedTypes globalSupportedTypes) { + Elements elements = processingEnv.getElementUtils(); + CustomUserConnectorAnnotationInfo annotationInfo = + extractFromCustomUserConnectorAnnotation(processingEnv, elements, connectorElement); + + Set<TypeElement> parcelableWrappers = new HashSet<>(annotationInfo.parcelableWrapperClasses()); + Set<TypeElement> futureWrappers = new HashSet<>(annotationInfo.futureWrapperClasses()); + + for (TypeElement importConnectorClass : annotationInfo.importsClasses()) { + UserConnectorInfo importConnector = + UserConnectorInfo.create(processingEnv, importConnectorClass, globalSupportedTypes); + parcelableWrappers.addAll(importConnector.parcelableWrapperClasses()); + futureWrappers.addAll(importConnector.futureWrapperClasses()); + } + + return new AutoValue_UserConnectorInfo( + connectorElement, + annotationInfo.serviceName(), + globalSupportedTypes + .asBuilder() + .addParcelableWrappers( + ParcelableWrapper.createCustomParcelableWrappers( + processingEnv.getTypeUtils(), + processingEnv.getElementUtils(), + parcelableWrappers)) + .addFutureWrappers( + FutureWrapper.createCustomFutureWrappers( + processingEnv.getTypeUtils(), processingEnv.getElementUtils(), futureWrappers)) + .build(), + ImmutableSet.copyOf(parcelableWrappers), + ImmutableSet.copyOf(futureWrappers), + annotationInfo.importsClasses(), + annotationInfo.availabilityRestrictions()); + } + + private static CustomUserConnectorAnnotationInfo extractFromCustomUserConnectorAnnotation( + ProcessingEnvironment processingEnv, Elements elements, TypeElement connectorElement) { + CustomUserConnector customUserConnector = + connectorElement.getAnnotation(CustomUserConnector.class); + + if (customUserConnector == null) { + return new AutoValue_UserConnectorInfo_CustomUserConnectorAnnotationInfo( + getDefaultServiceName(elements, connectorElement), + ImmutableSet.of(), + ImmutableSet.of(), + ImmutableSet.of(), + AvailabilityRestrictions.DEFAULT); + } + + Collection<TypeElement> parcelableWrappers = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customUserConnector::parcelableWrappers); + Collection<TypeElement> futureWrappers = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customUserConnector::futureWrappers); + Collection<TypeElement> imports = + GeneratorUtilities.extractClassesFromAnnotation( + processingEnv.getTypeUtils(), customUserConnector::imports); + + String serviceClassName = customUserConnector.serviceClassName(); + + return new AutoValue_UserConnectorInfo_CustomUserConnectorAnnotationInfo( + serviceClassName.isEmpty() + ? getDefaultServiceName(elements, connectorElement) + : ClassName.bestGuess(serviceClassName), + ImmutableSet.copyOf(parcelableWrappers), + ImmutableSet.copyOf(futureWrappers), + ImmutableSet.copyOf(imports), + customUserConnector.availabilityRestrictions()); + } + + public static ClassName getDefaultServiceName(Elements elements, TypeElement connectorElement) { + PackageElement originalPackage = elements.getPackageOf(connectorElement); + + return ClassName.get( + originalPackage.getQualifiedName().toString(), + String.format("%s_Service", connectorElement.getSimpleName().toString())); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java new file mode 100644 index 0000000..e13f0c5 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorContext.java @@ -0,0 +1,120 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableSet; +import java.util.Collection; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +/** + * Context for connected apps code validators. + * + * <p>This is used to validate enough that a {@link GeneratorContext} can be created for further + * validation and generation. + */ +@AutoValue +public abstract class ValidatorContext extends Context { + + public static Builder builder() { + return new AutoValue_ValidatorContext.Builder(); + } + + public abstract SupportedTypes globalSupportedTypes(); + + public abstract ImmutableSet<ProfileConnectorInfo> newProfileConnectorInterfaces(); + + public abstract ImmutableSet<UserConnectorInfo> newUserConnectorInterfaces(); + + public abstract ImmutableSet<TypeElement> newGeneratedProfileConnectors(); + + public abstract ImmutableSet<TypeElement> newGeneratedUserConnectors(); + + public abstract ImmutableSet<ValidatorCrossProfileConfigurationInfo> newConfigurations(); + + public abstract ImmutableSet<ValidatorCrossProfileTypeInfo> newCrossProfileTypes(); + + public abstract ImmutableSet<ExecutableElement> newCrossProfileMethods(); + + public abstract ImmutableSet<ValidatorProviderClassInfo> newProviderClasses(); + + public abstract ImmutableSet<ExecutableElement> newProviderMethods(); + + public abstract ImmutableSet<TypeElement> newCrossProfileCallbackInterfaces(); + + public abstract ImmutableSet<ValidatorCrossProfileTestInfo> newCrossProfileTests(); + + public abstract ImmutableSet<TypeElement> newCustomParcelableWrappers(); + + public abstract ImmutableSet<TypeElement> newCustomFutureWrappers(); + + /** A builder for {@link ValidatorContext}. */ + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder setProcessingEnv(ProcessingEnvironment processingEnv); + + public abstract Builder setElements(Elements elements); + + public abstract Builder setTypes(Types types); + + public abstract Builder setGlobalSupportedTypes(SupportedTypes globalSupportedTypes); + + public abstract Builder setNewProfileConnectorInterfaces( + Collection<ProfileConnectorInfo> newProfileConnectorInterfaces); + + public abstract Builder setNewUserConnectorInterfaces( + Collection<UserConnectorInfo> newUserConnectorInterfaces); + + public abstract Builder setNewGeneratedProfileConnectors( + Collection<TypeElement> newGeneratedConnectors); + + public abstract Builder setNewGeneratedUserConnectors( + Collection<TypeElement> newGeneratedUserConnectors); + + public abstract Builder setNewConfigurations( + Collection<ValidatorCrossProfileConfigurationInfo> newConfigurations); + + public abstract Builder setNewCrossProfileTypes( + Collection<ValidatorCrossProfileTypeInfo> newCrossProfileTypes); + + public abstract Builder setNewCrossProfileMethods( + Collection<ExecutableElement> newCrossProfileMethods); + + public abstract Builder setNewProviderClasses( + Collection<ValidatorProviderClassInfo> newProviderClasses); + + public abstract Builder setNewProviderMethods(Collection<ExecutableElement> newProviderMethods); + + public abstract Builder setNewCrossProfileCallbackInterfaces( + Collection<TypeElement> newCrossProfileCallbackInterfaces); + + public abstract Builder setNewCrossProfileTests( + Collection<ValidatorCrossProfileTestInfo> newCrossProfileTests); + + public abstract Builder setNewCustomParcelableWrappers( + Collection<TypeElement> newCustomParcelableWrappers); + + public abstract Builder setNewCustomFutureWrappers( + Collection<TypeElement> newCustomFutureWrappers); + + public abstract ValidatorContext build(); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java new file mode 100644 index 0000000..973714c --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileConfigurationInfo.java @@ -0,0 +1,125 @@ +/* + * 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.containers; + +import static com.google.android.enterprise.connectedapps.processor.CommonClassNames.SERVICE_CLASSNAME; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfileConfiguration; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import java.util.Optional; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +/** A wrapper around basic information from a {@link CrossProfileConfiguration} annotation. */ +@AutoValue +public abstract class ValidatorCrossProfileConfigurationInfo { + + public abstract TypeElement configurationElement(); + + public abstract ImmutableCollection<TypeElement> providerClassElements(); + + public abstract ClassName serviceSuperclass(); + + public abstract Optional<TypeElement> serviceClass(); + + public abstract Optional<TypeElement> connector(); + + public static ImmutableSet<ValidatorCrossProfileConfigurationInfo> createMultipleFromElement( + ProcessingEnvironment processingEnvironment, TypeElement annotatedElement) { + ImmutableSet<CrossProfileConfigurationAnnotationInfo> infos = + AnnotationFinder.extractCrossProfileConfigurationsAnnotationInfo( + annotatedElement, + processingEnvironment.getTypeUtils(), + processingEnvironment.getElementUtils()) + .configurations(); + ImmutableSet.Builder<ValidatorCrossProfileConfigurationInfo> configurations = + ImmutableSet.builder(); + + if (infos.isEmpty()) { + configurations.add(createFromElement(processingEnvironment, annotatedElement)); + } else { + for (CrossProfileConfigurationAnnotationInfo info : infos) { + configurations.add(createFromAnnotationInfo(info, annotatedElement)); + } + } + + return configurations.build(); + } + + public static ValidatorCrossProfileConfigurationInfo createFromElement( + ProcessingEnvironment processingEnv, TypeElement annotatedElement) { + CrossProfileConfigurationAnnotationInfo annotationInfo = + extractFromCrossProfileConfigurationAnnotation(annotatedElement, processingEnv); + + return createFromAnnotationInfo(annotationInfo, annotatedElement); + } + + private static ValidatorCrossProfileConfigurationInfo createFromAnnotationInfo( + CrossProfileConfigurationAnnotationInfo annotationInfo, TypeElement annotatedElement) { + ClassName serviceSuperclass = + serviceSuperclassIsDefault(annotationInfo.serviceSuperclass()) + ? SERVICE_CLASSNAME + : ClassName.get(annotationInfo.serviceSuperclass()); + + TypeElement serviceClass = + serviceClassIsDefault(annotationInfo.serviceClass()) ? null : annotationInfo.serviceClass(); + + Optional<TypeElement> connector = + connectorIsDefault(annotationInfo.connector()) + ? Optional.empty() + : Optional.of(annotationInfo.connector()); + + return new AutoValue_ValidatorCrossProfileConfigurationInfo( + annotatedElement, + ImmutableSet.copyOf(annotationInfo.providerClasses()), + serviceSuperclass, + Optional.ofNullable(serviceClass), + connector); + } + + private static boolean serviceSuperclassIsDefault(TypeElement serviceSuperclass) { + // CrossProfileConfiguration.class is the default specified serviceSuperclass + return serviceSuperclass + .asType() + .toString() + .equals(CrossProfileConfiguration.class.getCanonicalName()); + } + + private static boolean serviceClassIsDefault(TypeElement serviceClass) { + // CrossProfileConfiguration.class is the default specified serviceClass + return serviceClass + .asType() + .toString() + .equals(CrossProfileConfiguration.class.getCanonicalName()); + } + + private static boolean connectorIsDefault(TypeElement connector) { + // CrossProfileConfiguration.class is the default specified connector + return connector.asType().toString().equals(CrossProfileConfiguration.class.getCanonicalName()); + } + + private static CrossProfileConfigurationAnnotationInfo + extractFromCrossProfileConfigurationAnnotation( + Element annotatedElement, ProcessingEnvironment processingEnv) { + return AnnotationFinder.extractCrossProfileConfigurationAnnotationInfo( + annotatedElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java new file mode 100644 index 0000000..172c8ed --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTestInfo.java @@ -0,0 +1,40 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.testing.annotations.CrossProfileTest; +import com.google.auto.value.AutoValue; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; + +/** Wrapper of a {@link CrossProfileTest} annotated class. */ +@AutoValue +public abstract class ValidatorCrossProfileTestInfo { + + public abstract TypeElement crossProfileTestElement(); + + public abstract TypeElement configurationElement(); + + public static ValidatorCrossProfileTestInfo create( + ProcessingEnvironment processingEnv, TypeElement crossProfileTestElement) { + CrossProfileTestAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileTestAnnotationInfo( + crossProfileTestElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils()); + return new AutoValue_ValidatorCrossProfileTestInfo( + crossProfileTestElement, annotationInfo.configuration()); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java new file mode 100644 index 0000000..c757ba4 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorCrossProfileTypeInfo.java @@ -0,0 +1,115 @@ +/* + * 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.containers; + +import static com.google.android.enterprise.connectedapps.processor.GeneratorUtilities.findCrossProfileMethodsInClass; +import static java.util.stream.Collectors.toList; + +import com.google.android.enterprise.connectedapps.annotations.CrossProfile; +import com.google.android.enterprise.connectedapps.processor.SupportedTypes; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.interfaces.CrossProfileAnnotation; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; + +/** A wrapper around basic information from a {@link CrossProfile} type annotation. */ +@AutoValue +public abstract class ValidatorCrossProfileTypeInfo { + + public abstract TypeElement crossProfileTypeElement(); + + public abstract ImmutableList<ExecutableElement> crossProfileMethods(); + + public abstract Optional<ProfileConnectorInfo> profileConnector(); + + public abstract SupportedTypes supportedTypes(); + + public abstract ImmutableCollection<TypeElement> parcelableWrapperClasses(); + + public abstract ImmutableCollection<TypeElement> futureWrapperClasses(); + + public abstract String profileClassName(); + + public abstract boolean isStatic(); + + /** + * The specified timeout for async calls, or {@link CrossProfileAnnotation#DEFAULT_TIMEOUT_MILLIS} + * if unspecified. + */ + public abstract long timeoutMillis(); + + public static ValidatorCrossProfileTypeInfo create( + ProcessingEnvironment processingEnv, + TypeElement crossProfileTypeElement, + SupportedTypes globalSupportedTypes) { + CrossProfileAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileAnnotationInfo( + crossProfileTypeElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils()); + + Optional<ProfileConnectorInfo> profileConnectorElement = + annotationInfo.connectorIsDefault() + ? Optional.empty() + : Optional.of( + ProfileConnectorInfo.create( + processingEnv, annotationInfo.connectorClass(), globalSupportedTypes)); + + List<ExecutableElement> crossProfileMethodElements = + findCrossProfileMethodsInClass(crossProfileTypeElement).stream() + .sorted(Comparator.comparing(i -> i.getSimpleName().toString())) + .collect(toList()); + + SupportedTypes incomingSupportedTypes = + profileConnectorElement.isPresent() + ? profileConnectorElement.get().supportedTypes() + : globalSupportedTypes; + + SupportedTypes supportedTypes = + incomingSupportedTypes + .asBuilder() + .addParcelableWrappers( + ParcelableWrapper.createCustomParcelableWrappers( + processingEnv.getTypeUtils(), + processingEnv.getElementUtils(), + annotationInfo.parcelableWrapperClasses())) + .addFutureWrappers( + FutureWrapper.createCustomFutureWrappers( + processingEnv.getTypeUtils(), + processingEnv.getElementUtils(), + annotationInfo.futureWrapperClasses())) + .build(); + + return new AutoValue_ValidatorCrossProfileTypeInfo( + crossProfileTypeElement, + ImmutableList.copyOf(crossProfileMethodElements), + profileConnectorElement, + supportedTypes, + annotationInfo.parcelableWrapperClasses(), + annotationInfo.futureWrapperClasses(), + annotationInfo.profileClassName(), + annotationInfo.isStatic(), + annotationInfo + .timeoutMillis() + .filter(value -> value != CrossProfileAnnotation.TIMEOUT_MILLIS_NOT_SET) + .orElse(CrossProfileAnnotation.DEFAULT_TIMEOUT_MILLIS)); + } +} diff --git a/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java new file mode 100644 index 0000000..c3ffc28 --- /dev/null +++ b/processor/src/main/java/com/google/android/enterprise/connectedapps/processor/containers/ValidatorProviderClassInfo.java @@ -0,0 +1,51 @@ +/* + * 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.containers; + +import com.google.android.enterprise.connectedapps.processor.annotationdiscovery.AnnotationFinder; +import com.google.auto.value.AutoValue; +import com.google.common.collect.ImmutableCollection; +import com.google.common.collect.ImmutableSet; +import com.squareup.javapoet.ClassName; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; + +/** Wrapper of basic information for a cross-profile provider class. */ +@AutoValue +public abstract class ValidatorProviderClassInfo { + + public abstract TypeElement providerClassElement(); + + public abstract ImmutableCollection<TypeElement> staticTypes(); + + public String simpleName() { + return providerClassElement().getSimpleName().toString(); + } + + public ClassName className() { + return ClassName.get(providerClassElement()); + } + + public static ValidatorProviderClassInfo create( + ProcessingEnvironment processingEnv, TypeElement providerClassElement) { + CrossProfileProviderAnnotationInfo annotationInfo = + AnnotationFinder.extractCrossProfileProviderAnnotationInfo( + providerClassElement, processingEnv.getTypeUtils(), processingEnv.getElementUtils()); + + return new AutoValue_ValidatorProviderClassInfo( + providerClassElement, ImmutableSet.copyOf(annotationInfo.staticTypes())); + } +} diff --git a/processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java b/processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java new file mode 100644 index 0000000..f9fe728 --- /dev/null +++ b/processor/src/main/resources/futurewrappers/ListenableFutureWrapper.java @@ -0,0 +1,122 @@ +/* + * 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.futurewrappers; + +import static com.google.common.util.concurrent.MoreExecutors.directExecutor; + +import com.google.android.enterprise.connectedapps.FutureWrapper; +import com.google.android.enterprise.connectedapps.Profile; +import com.google.android.enterprise.connectedapps.exceptions.UnavailableProfileException; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.android.enterprise.connectedapps.internal.CrossProfileCallbackMultiMerger; +import com.google.android.enterprise.connectedapps.internal.FutureResultWriter; +import com.google.common.util.concurrent.FluentFuture; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.SettableFuture; +import java.util.Map; + +/** Wrapper for adding support for {@link ListenableFuture} to the Connected Apps SDK. */ +public final class ListenableFutureWrapper<E> extends FutureWrapper<E> { + + private final SettableFuture<E> future = SettableFuture.create(); + + public static <E> ListenableFutureWrapper<E> create(Bundler bundler, BundlerType bundlerType) { + return new ListenableFutureWrapper<>(bundler, bundlerType); + } + + private ListenableFutureWrapper(Bundler bundler, BundlerType bundlerType) { + super(bundler, bundlerType); + } + + public ListenableFuture<E> getFuture() { + return future; + } + + @Override + public void onResult(E result) { + future.set(result); + } + + @Override + public void onException(Throwable throwable) { + future.setException(throwable); + } + + public static <E> void writeFutureResult( + ListenableFuture<E> future, FutureResultWriter<E> resultWriter) { + FluentFuture.from(future) + .addCallback( + new FutureCallback<E>() { + @Override + public void onSuccess(E result) { + resultWriter.onSuccess(result); + } + + @Override + public void onFailure(Throwable t) { + resultWriter.onFailure(t); + } + }, + directExecutor()); + } + + private static class MergerFutureCallback<E> implements FutureCallback<E> { + + private final Profile profileId; + private final CrossProfileCallbackMultiMerger<E> merger; + + MergerFutureCallback(Profile profileId, CrossProfileCallbackMultiMerger<E> merger) { + if (profileId == null || merger == null) { + throw new NullPointerException(); + } + this.profileId = profileId; + this.merger = merger; + } + + @Override + public void onSuccess(E result) { + merger.onResult(profileId, result); + } + + @Override + public void onFailure(Throwable t) { + // TODO: What should we do with the Throwable? + merger.missingResult(profileId); + } + } + + public static <E> ListenableFuture<Map<Profile, E>> groupResults( + Map<Profile, ListenableFuture<E>> results) { + SettableFuture<Map<Profile, E>> m = SettableFuture.create(); + CrossProfileCallbackMultiMerger<E> merger = + new CrossProfileCallbackMultiMerger<>(results.size(), m::set); + for (Map.Entry<Profile, ListenableFuture<E>> result : results.entrySet()) { + FluentFuture.from(result.getValue()) + .catching( + UnavailableProfileException.class, + (throwable) -> { + merger.missingResult(result.getKey()); + return null; // This will be passed into the callback but will be rejected by merger + // as duplicate + }, + directExecutor()) + .addCallback(new MergerFutureCallback<>(result.getKey(), merger), directExecutor()); + } + return m; + } +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableArray.java b/processor/src/main/resources/parcelablewrappers/ParcelableArray.java new file mode 100644 index 0000000..c1cf335 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableArray.java @@ -0,0 +1,118 @@ +/* + * 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.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; + +/** Wrapper for reading & writing arrays from and to {@link Parcel} instances. */ +public class ParcelableArray<E> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final E[] array; + + /** + * Create a wrapper for a given array. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableArray<F> of(Bundler bundler, BundlerType type, F[] array) { + return new ParcelableArray<>(bundler, type, array); + } + + public E[] get() { + return array; + } + + private ParcelableArray(Bundler bundler, BundlerType type, E[] array) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.array = array; + } + + private ParcelableArray(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + array = null; + return; + } + + type = in.readParcelable(Bundler.class.getClassLoader()); + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E[] a = (E[]) bundler.createArray(valueType, size); + array = a; + + if (size > 0) { + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + array[i] = value; + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (array == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(array.length); + dest.writeParcelable(type, flags); + if (array.length > 0) { + BundlerType valueType = type.typeArguments().get(0); + + for (E value : array) { + bundler.writeToParcel(dest, value, valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableArray> CREATOR = + new Creator<ParcelableArray>() { + @Override + public ParcelableArray createFromParcel(Parcel in) { + return new ParcelableArray(in); + } + + @Override + public ParcelableArray[] newArray(int size) { + return new ParcelableArray[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java b/processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java new file mode 100644 index 0000000..ba27af8 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableBitmap.java @@ -0,0 +1,101 @@ +/* + * 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.parcelablewrappers; + +import android.graphics.Bitmap; +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; + +/** Wrapper for reading & writing {@link Bitmap} instances from and to {@link Parcel} instances. */ +// Though Bitmap is itself Parcelable, in some circumstances the Parcelling process can fail (see +// b/159895007). +public class ParcelableBitmap implements Parcelable { + private final Bitmap bitmap; + + /** Create a wrapper for a given bitmap. */ + public static ParcelableBitmap of(Bundler bundler, BundlerType type, Bitmap bitmap) { + return new ParcelableBitmap(bitmap); + } + + private ParcelableBitmap(Bitmap bitmap) { + this.bitmap = bitmap; + } + + private ParcelableBitmap(Parcel in) { + String configKey = in.readString(); + + if (configKey == null) { + bitmap = null; + return; + } + + Bitmap.Config config = Bitmap.Config.valueOf(configKey); + int width = in.readInt(); + int height = in.readInt(); + int[] colors = new int[width * height]; + in.readIntArray(colors); + + bitmap = Bitmap.createBitmap(colors, width, height, config); + } + + @Override + public void writeToParcel(Parcel out, int flags) { + if (bitmap == null) { + out.writeString(null); + return; + } + + Bitmap.Config config = bitmap.getConfig(); + int width = bitmap.getWidth(); + int height = bitmap.getHeight(); + int[] colors = bitmapToPixelArray(bitmap); + + out.writeString(config.toString()); + out.writeInt(width); + out.writeInt(height); + out.writeIntArray(colors); + } + + @Override + public int describeContents() { + return 0; + } + + public Bitmap get() { + return bitmap; + } + + public static final Creator<ParcelableBitmap> CREATOR = + new Creator<ParcelableBitmap>() { + @Override + public ParcelableBitmap createFromParcel(Parcel in) { + return new ParcelableBitmap(in); + } + + @Override + public ParcelableBitmap[] newArray(int size) { + return new ParcelableBitmap[size]; + } + }; + + private static int[] bitmapToPixelArray(Bitmap bitmap) { + int[] pixels = new int[bitmap.getHeight() * bitmap.getWidth()]; + bitmap.getPixels(pixels, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); + return pixels; + } +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableCollection.java b/processor/src/main/resources/parcelablewrappers/ParcelableCollection.java new file mode 100644 index 0000000..66db136 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableCollection.java @@ -0,0 +1,117 @@ +/* + * 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.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.ArrayList; +import java.util.Collection; + +/** + * Wrapper for reading & writing {@link Collection} instances from and to {@link Parcel} instances. + */ +public class ParcelableCollection<E> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final Collection<E> collection; + + /** + * Create a wrapper for a given collection. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableCollection<F> of( + Bundler bundler, BundlerType type, Collection<F> collection) { + return new ParcelableCollection<>(bundler, type, collection); + } + + public Collection<E> get() { + return collection; + } + + private ParcelableCollection(Bundler bundler, BundlerType type, Collection<E> collection) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.collection = collection; + } + + private ParcelableCollection(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + if (size == NULL_SIZE) { + type = null; + collection = null; + return; + } + + collection = new ArrayList<>(); + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType valueType = type.typeArguments().get(0); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + collection.add(value); + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (collection == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(collection.size()); + dest.writeParcelable(type, flags); + if (!collection.isEmpty()) { + BundlerType valueType = type.typeArguments().get(0); + for (E value : collection) { + bundler.writeToParcel(dest, value, valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableCollection> CREATOR = + new Creator<ParcelableCollection>() { + @Override + public ParcelableCollection createFromParcel(Parcel in) { + return new ParcelableCollection(in); + } + + @Override + public ParcelableCollection[] newArray(int size) { + return new ParcelableCollection[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java b/processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java new file mode 100644 index 0000000..e2a14b9 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableGuavaOptional.java @@ -0,0 +1,120 @@ +/* + * 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.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.common.base.Optional; + +/** + * Wrapper for reading & writing {@link Optional} instances from and to {@link Parcel} instances. + */ +public class ParcelableGuavaOptional<E> implements Parcelable { + + private static final int NULL = -1; + private static final int ABSENT = 0; + private static final int PRESENT = 1; + + private final Bundler bundler; + private final BundlerType type; + private final Optional<E> optional; + + /** + * Create a wrapper for a given optional. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableGuavaOptional<F> of( + Bundler bundler, BundlerType type, Optional<F> optional) { + return new ParcelableGuavaOptional<>(bundler, type, optional); + } + + public Optional<E> get() { + return optional; + } + + private ParcelableGuavaOptional(Bundler bundler, BundlerType type, Optional<E> optional) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.optional = optional; + } + + private ParcelableGuavaOptional(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + + int presentValue = in.readInt(); + + if (presentValue == NULL) { + type = null; + optional = null; + return; + } + + boolean isPresent = presentValue == PRESENT; + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (isPresent) { + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + + optional = Optional.of(value); + } else { + optional = Optional.absent(); + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (optional == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(optional.isPresent() ? PRESENT : ABSENT); + dest.writeParcelable(type, flags); + if (optional.isPresent()) { + BundlerType valueType = type.typeArguments().get(0); + bundler.writeToParcel(dest, optional.get(), valueType, flags); + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableGuavaOptional> CREATOR = + new Creator<ParcelableGuavaOptional>() { + @Override + public ParcelableGuavaOptional createFromParcel(Parcel in) { + return new ParcelableGuavaOptional(in); + } + + @Override + public ParcelableGuavaOptional[] newArray(int size) { + return new ParcelableGuavaOptional[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java b/processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java new file mode 100644 index 0000000..78b7790 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableImmutableMap.java @@ -0,0 +1,129 @@ +/* + * 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.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import com.google.common.collect.ImmutableMap; + +/** + * Wrapper for reading & writing {@link ImmutableMap} instances from and to {@link Parcel} + * instances. + */ +public class ParcelableImmutableMap<E, F> implements Parcelable { + + private static final int NULL_SIZE = -1; + private static final int KEY_TYPE_INDEX = 0; + private static final int VALUE_TYPE_INDEX = 1; + + private final Bundler bundler; + private final BundlerType type; + private final ImmutableMap<E, F> map; + + /** + * Create a wrapper for a given immutable map. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code E} and {@code F}. + */ + public static <E, F> ParcelableImmutableMap<E, F> of( + Bundler bundler, BundlerType type, ImmutableMap<E, F> map) { + return new ParcelableImmutableMap<>(bundler, type, map); + } + + public ImmutableMap<E, F> get() { + return map; + } + + private ParcelableImmutableMap(Bundler bundler, BundlerType type, ImmutableMap<E, F> map) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.map = map; + } + + private ParcelableImmutableMap(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + map = null; + return; + } + + ImmutableMap.Builder<E, F> mapBuilder = ImmutableMap.builder(); + + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType keyType = type.typeArguments().get(KEY_TYPE_INDEX); + BundlerType valueType = type.typeArguments().get(VALUE_TYPE_INDEX); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E key = (E) bundler.readFromParcel(in, keyType); + @SuppressWarnings("unchecked") + F value = (F) bundler.readFromParcel(in, valueType); + mapBuilder.put(key, value); + } + } + + map = mapBuilder.build(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (map == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(map.size()); + dest.writeParcelable(type, flags); + if (!map.isEmpty()) { + BundlerType keyType = type.typeArguments().get(0); + BundlerType valueType = type.typeArguments().get(1); + + for (ImmutableMap.Entry<E, F> entry : map.entrySet()) { + bundler.writeToParcel(dest, entry.getKey(), keyType, flags); + bundler.writeToParcel(dest, entry.getValue(), valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableImmutableMap> CREATOR = + new Creator<ParcelableImmutableMap>() { + @Override + public ParcelableImmutableMap createFromParcel(Parcel in) { + return new ParcelableImmutableMap(in); + } + + @Override + public ParcelableImmutableMap[] newArray(int size) { + return new ParcelableImmutableMap[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableList.java b/processor/src/main/resources/parcelablewrappers/ParcelableList.java new file mode 100644 index 0000000..b1ff12e --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableList.java @@ -0,0 +1,117 @@ +/* + * 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.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.ArrayList; +import java.util.List; + +/** Wrapper for reading & writing {@link List} instances from and to {@link Parcel} instances. */ + +public class ParcelableList<E> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final List<E> list; + + /** + * Create a wrapper for a given list. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableList<F> of(Bundler bundler, BundlerType type, List<F> list) { + return new ParcelableList<>(bundler, type, list); + } + + public List<E> get() { + return list; + } + + private ParcelableList(Bundler bundler, BundlerType type, List<E> list) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.list = list; + } + + private ParcelableList(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + list = null; + return; + } + + list = new ArrayList<>(); + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType valueType = type.typeArguments().get(0); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + list.add(value); + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (list == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(list.size()); + dest.writeParcelable(type, flags); + if (!list.isEmpty()) { + BundlerType valueType = type.typeArguments().get(0); + + for (E value : list) { + bundler.writeToParcel(dest, value, valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableList> CREATOR = + new Creator<ParcelableList>() { + @Override + public ParcelableList createFromParcel(Parcel in) { + return new ParcelableList(in); + } + + @Override + public ParcelableList[] newArray(int size) { + return new ParcelableList[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableMap.java b/processor/src/main/resources/parcelablewrappers/ParcelableMap.java new file mode 100644 index 0000000..e90c22b --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableMap.java @@ -0,0 +1,121 @@ +/* + * 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.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.HashMap; +import java.util.Map; + +/** Wrapper for reading & writing {@link Map} instances from and to {@link Parcel} instances. */ +public class ParcelableMap<E, F> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final Map<E, F> map; + + /** + * Create a wrapper for a given map. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code E} and {@code F}. + */ + public static <E, F> ParcelableMap<E, F> of(Bundler bundler, BundlerType type, Map<E, F> map) { + return new ParcelableMap<>(bundler, type, map); + } + + public Map<E, F> get() { + return map; + } + + private ParcelableMap(Bundler bundler, BundlerType type, Map<E, F> map) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.map = map; + } + + private ParcelableMap(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + map = null; + return; + } + + map = new HashMap<>(); + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType keyType = type.typeArguments().get(0); + BundlerType valueType = type.typeArguments().get(1); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E key = (E) bundler.readFromParcel(in, keyType); + @SuppressWarnings("unchecked") + F value = (F) bundler.readFromParcel(in, valueType); + map.put(key, value); + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (map == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(map.size()); + dest.writeParcelable(type, flags); + if (!map.isEmpty()) { + BundlerType keyType = type.typeArguments().get(0); + BundlerType valueType = type.typeArguments().get(1); + + for (Map.Entry<E, F> entry : map.entrySet()) { + bundler.writeToParcel(dest, entry.getKey(), keyType, flags); + bundler.writeToParcel(dest, entry.getValue(), valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableMap> CREATOR = + new Creator<ParcelableMap>() { + @Override + public ParcelableMap createFromParcel(Parcel in) { + return new ParcelableMap(in); + } + + @Override + public ParcelableMap[] newArray(int size) { + return new ParcelableMap[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableOptional.java b/processor/src/main/resources/parcelablewrappers/ParcelableOptional.java new file mode 100644 index 0000000..aa81dc9 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableOptional.java @@ -0,0 +1,120 @@ +/* + * 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.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.Optional; + +/** + * Wrapper for reading & writing {@link Optional} instances from and to {@link Parcel} instances. + */ +public class ParcelableOptional<E> implements Parcelable { + + private static final int NULL = -1; + private static final int ABSENT = 0; + private static final int PRESENT = 1; + + private final Bundler bundler; + private final BundlerType type; + private final Optional<E> optional; + + /** + * Create a wrapper for a given optional. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableOptional<F> of( + Bundler bundler, BundlerType type, Optional<F> optional) { + return new ParcelableOptional<>(bundler, type, optional); + } + + public Optional<E> get() { + return optional; + } + + private ParcelableOptional(Bundler bundler, BundlerType type, Optional<E> optional) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.optional = optional; + } + + private ParcelableOptional(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + + int presentValue = in.readInt(); + + if (presentValue == NULL) { + type = null; + optional = null; + return; + } + + boolean isPresent = presentValue == PRESENT; + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (isPresent) { + BundlerType valueType = type.typeArguments().get(0); + + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + + optional = Optional.of(value); + } else { + optional = Optional.empty(); + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (optional == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(optional.isPresent() ? PRESENT : ABSENT); + dest.writeParcelable(type, flags); + if (optional.isPresent()) { + BundlerType valueType = type.typeArguments().get(0); + bundler.writeToParcel(dest, optional.get(), valueType, flags); + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableOptional> CREATOR = + new Creator<ParcelableOptional>() { + @Override + public ParcelableOptional createFromParcel(Parcel in) { + return new ParcelableOptional(in); + } + + @Override + public ParcelableOptional[] newArray(int size) { + return new ParcelableOptional[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelablePair.java b/processor/src/main/resources/parcelablewrappers/ParcelablePair.java new file mode 100644 index 0000000..41dea47 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelablePair.java @@ -0,0 +1,115 @@ +/* + * 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.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import android.util.Pair; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; + +/** Wrapper for reading & writing {@link Pair} instances from and to {@link Parcel} instances. */ +public class ParcelablePair<F, S> implements Parcelable { + + private static final int NULL = -1; + private static final int NOT_NULL = 1; + + private final Bundler bundler; + private final BundlerType type; + private final Pair<F, S> pair; + + /** + * Create a wrapper for a given pair. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code E} and {@code F}. + */ + public static <F, S> ParcelablePair<F, S> of(Bundler bundler, BundlerType type, Pair<F, S> pair) { + return new ParcelablePair<>(bundler, type, pair); + } + + public Pair<F, S> get() { + return pair; + } + + private ParcelablePair(Bundler bundler, BundlerType type, Pair<F, S> pair) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.pair = pair; + } + + private ParcelablePair(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int present = in.readInt(); + + if (present == NULL) { + type = null; + pair = null; + return; + } + + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + BundlerType fType = type.typeArguments().get(0); + BundlerType sType = type.typeArguments().get(1); + + @SuppressWarnings("unchecked") + F first = (F) bundler.readFromParcel(in, fType); + @SuppressWarnings("unchecked") + S second = (S) bundler.readFromParcel(in, sType); + + pair = new Pair<>(first, second); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (pair == null) { + dest.writeInt(NULL); + return; + } + + dest.writeInt(NOT_NULL); + dest.writeParcelable(type, flags); + + BundlerType fType = type.typeArguments().get(0); + BundlerType sType = type.typeArguments().get(1); + + bundler.writeToParcel(dest, pair.first, fType, flags); + bundler.writeToParcel(dest, pair.second, sType, flags); + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelablePair> CREATOR = + new Creator<ParcelablePair>() { + @Override + public ParcelablePair createFromParcel(Parcel in) { + return new ParcelablePair(in); + } + + @Override + public ParcelablePair[] newArray(int size) { + return new ParcelablePair[size]; + } + }; +} diff --git a/processor/src/main/resources/parcelablewrappers/ParcelableSet.java b/processor/src/main/resources/parcelablewrappers/ParcelableSet.java new file mode 100644 index 0000000..b032f21 --- /dev/null +++ b/processor/src/main/resources/parcelablewrappers/ParcelableSet.java @@ -0,0 +1,116 @@ +/* + * 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.parcelablewrappers; + +import android.os.Parcel; +import android.os.Parcelable; +import com.google.android.enterprise.connectedapps.internal.Bundler; +import com.google.android.enterprise.connectedapps.internal.BundlerType; +import java.util.HashSet; +import java.util.Set; + +/** Wrapper for reading & writing {@link Set} instances from and to {@link Parcel} instances. */ +public class ParcelableSet<E> implements Parcelable { + + private static final int NULL_SIZE = -1; + + private final Bundler bundler; + private final BundlerType type; + private final Set<E> set; + + /** + * Create a wrapper for a given set. + * + * <p>The passed in {@link Bundler} must be capable of bundling {@code F}. + */ + public static <F> ParcelableSet<F> of(Bundler bundler, BundlerType type, Set<F> set) { + return new ParcelableSet<>(bundler, type, set); + } + + public Set<E> get() { + return set; + } + + private ParcelableSet(Bundler bundler, BundlerType type, Set<E> set) { + if (bundler == null || type == null) { + throw new NullPointerException(); + } + this.bundler = bundler; + this.type = type; + this.set = set; + } + + private ParcelableSet(Parcel in) { + bundler = in.readParcelable(Bundler.class.getClassLoader()); + int size = in.readInt(); + + if (size == NULL_SIZE) { + type = null; + set = null; + return; + } + + set = new HashSet<>(); + type = (BundlerType) in.readParcelable(Bundler.class.getClassLoader()); + if (size > 0) { + BundlerType valueType = type.typeArguments().get(0); + for (int i = 0; i < size; i++) { + @SuppressWarnings("unchecked") + E value = (E) bundler.readFromParcel(in, valueType); + set.add(value); + } + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(bundler, flags); + + if (set == null) { + dest.writeInt(NULL_SIZE); + return; + } + + dest.writeInt(set.size()); + dest.writeParcelable(type, flags); + if (!set.isEmpty()) { + BundlerType valueType = type.typeArguments().get(0); + + for (E value : set) { + bundler.writeToParcel(dest, value, valueType, flags); + } + } + } + + @Override + public int describeContents() { + return 0; + } + + @SuppressWarnings("rawtypes") + public static final Creator<ParcelableSet> CREATOR = + new Creator<ParcelableSet>() { + @Override + public ParcelableSet createFromParcel(Parcel in) { + return new ParcelableSet(in); + } + + @Override + public ParcelableSet[] newArray(int size) { + return new ParcelableSet[size]; + } + }; +} |