| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * 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 |
| * |
| * http://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 android.processor.compat.changeid; |
| |
| import static javax.lang.model.element.ElementKind.CLASS; |
| import static javax.lang.model.element.ElementKind.PARAMETER; |
| import static javax.tools.Diagnostic.Kind.ERROR; |
| import static javax.tools.StandardLocation.CLASS_OUTPUT; |
| |
| import android.processor.compat.SingleAnnotationProcessor; |
| import android.processor.compat.SourcePosition; |
| |
| import com.google.common.collect.ImmutableSet; |
| import com.google.common.collect.Table; |
| |
| import java.io.IOException; |
| import java.io.OutputStream; |
| import java.util.List; |
| import java.util.Objects; |
| import java.util.regex.Pattern; |
| |
| import javax.annotation.processing.SupportedAnnotationTypes; |
| import javax.annotation.processing.SupportedSourceVersion; |
| import javax.lang.model.SourceVersion; |
| import javax.lang.model.element.AnnotationMirror; |
| import javax.lang.model.element.AnnotationValue; |
| import javax.lang.model.element.Element; |
| import javax.lang.model.element.ElementKind; |
| 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.tools.FileObject; |
| |
| /** |
| * Annotation processor for ChangeId annotations. |
| * |
| * This processor outputs an XML file containing all the changeIds defined by this |
| * annotation. The file is bundled into the pratform image and used by the system server. |
| * Design doc: go/gating-and-logging. |
| */ |
| @SupportedAnnotationTypes({"android.compat.annotation.ChangeId"}) |
| @SupportedSourceVersion(SourceVersion.RELEASE_11) |
| public class ChangeIdProcessor extends SingleAnnotationProcessor { |
| |
| private static final String CONFIG_XML = "compat_config.xml"; |
| |
| private static final String IGNORED_CLASS = "android.compat.Compatibility"; |
| private static final ImmutableSet<String> IGNORED_METHOD_NAMES = |
| ImmutableSet.of("reportUnconditionalChange", "isChangeEnabled"); |
| |
| private static final String CHANGE_ID_QUALIFIED_CLASS_NAME = |
| "android.compat.annotation.ChangeId"; |
| |
| private static final String DISABLED_CLASS_NAME = "android.compat.annotation.Disabled"; |
| private static final String ENABLED_AFTER_CLASS_NAME = "android.compat.annotation.EnabledAfter"; |
| private static final String ENABLED_SINCE_CLASS_NAME = "android.compat.annotation.EnabledSince"; |
| private static final String LOGGING_CLASS_NAME = "android.compat.annotation.LoggingOnly"; |
| private static final String TARGET_SDK_VERSION = "targetSdkVersion"; |
| private static final String OVERRIDABLE_CLASS_NAME = "android.compat.annotation.Overridable"; |
| |
| private static final Pattern JAVADOC_SANITIZER = Pattern.compile("^\\s", Pattern.MULTILINE); |
| private static final Pattern HIDE_TAG_MATCHER = Pattern.compile("(\\s|^)@hide(\\s|$)"); |
| |
| @Override |
| protected void process(TypeElement annotation, |
| Table<PackageElement, String, List<Element>> annotatedElements) { |
| for (PackageElement packageElement : annotatedElements.rowKeySet()) { |
| for (String enclosingElementName : annotatedElements.row(packageElement).keySet()) { |
| XmlWriter writer = new XmlWriter(); |
| for (Element element : annotatedElements.get(packageElement, |
| enclosingElementName)) { |
| Change change = |
| createChange(packageElement.toString(), enclosingElementName, element); |
| writer.addChange(change); |
| } |
| |
| try { |
| FileObject resource = processingEnv.getFiler().createResource( |
| CLASS_OUTPUT, packageElement.toString(), |
| enclosingElementName + "_" + CONFIG_XML); |
| try (OutputStream outputStream = resource.openOutputStream()) { |
| writer.write(outputStream); |
| } |
| } catch (IOException e) { |
| messager.printMessage(ERROR, "Failed to write output: " + e); |
| } |
| } |
| } |
| } |
| |
| @Override |
| protected boolean ignoreAnnotatedElement(Element element, AnnotationMirror mirror) { |
| // Ignore the annotations on method parameters in known methods in package android.compat |
| // (libcore/luni/src/main/java/android/compat/Compatibility.java) |
| // without generating an error. |
| if (element.getKind() == PARAMETER) { |
| Element enclosingMethod = element.getEnclosingElement(); |
| Element enclosingElement = enclosingMethod.getEnclosingElement(); |
| if (enclosingElement.getKind() == CLASS) { |
| if (enclosingElement.toString().equals(IGNORED_CLASS) && |
| IGNORED_METHOD_NAMES.contains(enclosingMethod.getSimpleName().toString())) { |
| return true; |
| } |
| } |
| } |
| return !isValidChangeId(element); |
| } |
| |
| /** |
| * Checks if the provided java element is a valid change id (i.e. a long parameter with a |
| * constant value). |
| * |
| * @param element java element to check. |
| * @return true if the provided element is a legal change id that should be added to the |
| * produced XML file. If true is returned it's guaranteed that the following operations are |
| * safe. |
| */ |
| private boolean isValidChangeId(Element element) { |
| if (element.getKind() != ElementKind.FIELD) { |
| messager.printMessage( |
| ERROR, |
| "Non FIELD element annotated with @ChangeId.", |
| element); |
| return false; |
| } |
| if (!(element instanceof VariableElement)) { |
| messager.printMessage( |
| ERROR, |
| "Non variable annotated with @ChangeId.", |
| element); |
| return false; |
| } |
| if (((VariableElement) element).getConstantValue() == null) { |
| messager.printMessage( |
| ERROR, |
| "Non constant/final variable annotated with @ChangeId.", |
| element); |
| return false; |
| } |
| if (element.asType().getKind() != TypeKind.LONG) { |
| messager.printMessage( |
| ERROR, |
| "Variables annotated with @ChangeId must be of type long.", |
| element); |
| return false; |
| } |
| if (!element.getModifiers().contains(Modifier.STATIC)) { |
| messager.printMessage( |
| ERROR, |
| "Non static variable annotated with @ChangeId.", |
| element); |
| return false; |
| } |
| return true; |
| } |
| |
| private Change createChange(String packageName, String enclosingElementName, Element element) { |
| Change.Builder builder = new Change.Builder() |
| .id((Long) ((VariableElement) element).getConstantValue()) |
| .name(element.getSimpleName().toString()); |
| |
| AnnotationMirror changeId = null; |
| for (AnnotationMirror mirror : element.getAnnotationMirrors()) { |
| final String type = |
| ((TypeElement) mirror.getAnnotationType().asElement()).getQualifiedName() |
| .toString(); |
| final AnnotationValue sdkValue = |
| getAnnotationValue(element, mirror, TARGET_SDK_VERSION); |
| switch (type) { |
| case DISABLED_CLASS_NAME: |
| builder.disabled(); |
| break; |
| case LOGGING_CLASS_NAME: |
| builder.loggingOnly(); |
| break; |
| case ENABLED_AFTER_CLASS_NAME: |
| builder.enabledAfter((Integer)(Objects.requireNonNull(sdkValue).getValue())); |
| break; |
| case ENABLED_SINCE_CLASS_NAME: |
| builder.enabledSince((Integer)(Objects.requireNonNull(sdkValue).getValue())); |
| break; |
| case OVERRIDABLE_CLASS_NAME: |
| builder.overridable(); |
| break; |
| case CHANGE_ID_QUALIFIED_CLASS_NAME: |
| changeId = mirror; |
| break; |
| } |
| } |
| |
| String comment = |
| elements.getDocComment(element); |
| if (comment != null) { |
| comment = HIDE_TAG_MATCHER.matcher(comment).replaceAll(""); |
| comment = JAVADOC_SANITIZER.matcher(comment).replaceAll(""); |
| comment = comment.replaceAll("\\n", " "); |
| builder.description(comment.trim()); |
| } |
| |
| return verifyChange(element, |
| builder.javaClass(enclosingElementName) |
| .javaPackage(packageName) |
| .qualifiedClass(packageName + "." + enclosingElementName) |
| .sourcePosition(getLineNumber(element, changeId)) |
| .build()); |
| } |
| |
| private String getLineNumber(Element element, AnnotationMirror mirror) { |
| SourcePosition position = Objects.requireNonNull(getSourcePosition(element, mirror)); |
| return String.format("%s:%d", position.getFilename(), position.getStartLineNumber()); |
| } |
| |
| private Change verifyChange(Element element, Change change) { |
| if (change.disabled && (change.enabledAfter != null || change.enabledSince != null)) { |
| messager.printMessage( |
| ERROR, |
| "ChangeId cannot be annotated with both @Disabled and " |
| + "(@EnabledAfter | @EnabledSince).", |
| element); |
| } |
| if (change.loggingOnly && (change.disabled || change.enabledAfter != null |
| || change.enabledSince != null)) { |
| messager.printMessage( |
| ERROR, |
| "ChangeId cannot be annotated with both @LoggingOnly and " |
| + "(@EnabledAfter | @EnabledSince | @Disabled).", |
| element); |
| } |
| if (change.enabledAfter != null && change.enabledSince != null) { |
| messager.printMessage( |
| ERROR, |
| "ChangeId cannot be annotated with both @EnabledAfter and " |
| + "@EnabledSince. Prefer using the latter.", |
| element); |
| } |
| return change; |
| } |
| } |