blob: 366c9046ecd5f3d306fa05caaae5a52e89166bcc [file] [log] [blame]
/*
* Copyright (C) 2017 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 com.android.dx;
import com.android.dx.dex.file.ClassDefItem;
import com.android.dx.rop.annotation.Annotation;
import com.android.dx.rop.annotation.AnnotationVisibility;
import com.android.dx.rop.annotation.Annotations;
import com.android.dx.rop.annotation.NameValuePair;
import com.android.dx.rop.cst.*;
import java.lang.annotation.ElementType;
import java.util.HashMap;
/**
* Identifies an annotation on a program element, see {@link java.lang.annotation.ElementType}.
*
* Currently it is only targeting Class, Method, Field and Parameter because those are supported by
* {@link com.android.dx.dex.file.AnnotationsDirectoryItem} so far.
*
* <p><strong>NOTE:</strong>
* So far it only supports adding method annotation. The annotation of class, field and parameter
* will be implemented later.
*
* <p><strong>WARNING:</strong>
* The declared element of an annotation type should either have a default value or be set a value via
* {@code AnnotationId.set(Element)}. Otherwise it will incur
* {@link java.lang.annotation.IncompleteAnnotationException} when accessing the annotation element
* through reflection. The example is as follows:
* <pre>
* {@code @Retention(RetentionPolicy.RUNTIME)}
* {@code @Target({ElementType.METHOD})}
* {@code @interface MethodAnnotation {
* boolean elementBoolean();
* // boolean elementBoolean() default false;
* }
*
* TypeId<?> GENERATED = TypeId.get("LGenerated;");
* MethodId<?, Void> methodId = GENERATED.getMethod(VOID, "call");
* Code code = dexMaker.declare(methodId, PUBLIC);
* code.returnVoid();
*
* TypeId<MethodAnnotation> annotationTypeId = TypeId.get(MethodAnnotation.class);
* AnnotationId<?, MethodAnnotation> annotationId = AnnotationId.get(GENERATED,
* annotationTypeId, ElementType.METHOD);
*
* AnnotationId.Element element = new AnnotationId.Element("elementBoolean", true);
* annotationId.set(element);
* annotationId.addToMethod(dexMaker, methodId);
* }
* </pre>
*
* @param <D> the type that declares the program element.
* @param <V> the annotation type. It should be a known type before compile.
*/
public final class AnnotationId<D, V> {
private final TypeId<D> declaringType;
private final TypeId<V> type;
/** The type of program element to be annotated */
private final ElementType annotatedElement;
/** The elements this annotation holds */
private final HashMap<String, NameValuePair> elements;
private AnnotationId(TypeId<D> declaringType, TypeId<V> type, ElementType annotatedElement) {
this.declaringType = declaringType;
this.type = type;
this.annotatedElement = annotatedElement;
this.elements = new HashMap<>();
}
/**
* Construct an instance. It initially contains no elements.
*
* @param declaringType the type declaring the program element.
* @param type the annotation type.
* @param annotatedElement the program element type to be annotated.
* @return an annotation {@code AnnotationId<D,V>} instance.
*/
public static <D, V> AnnotationId<D, V> get(TypeId<D> declaringType, TypeId<V> type,
ElementType annotatedElement) {
if (annotatedElement != ElementType.TYPE &&
annotatedElement != ElementType.METHOD &&
annotatedElement != ElementType.FIELD &&
annotatedElement != ElementType.PARAMETER) {
throw new IllegalArgumentException("element type is not supported to annotate yet.");
}
return new AnnotationId<>(declaringType, type, annotatedElement);
}
/**
* Set an annotation element of this instance.
* If there is a preexisting element with the same name, it will be
* replaced by this method.
*
* @param element {@code non-null;} the annotation element to be set.
*/
public void set(Element element) {
if (element == null) {
throw new NullPointerException("element == null");
}
CstString pairName = new CstString(element.getName());
Constant pairValue = Element.toConstant(element.getValue());
NameValuePair nameValuePair = new NameValuePair(pairName, pairValue);
elements.put(element.getName(), nameValuePair);
}
/**
* Add this annotation to a method.
*
* @param dexMaker DexMaker instance.
* @param method Method to be added to.
*/
public void addToMethod(DexMaker dexMaker, MethodId<?, ?> method) {
if (annotatedElement != ElementType.METHOD) {
throw new IllegalStateException("This annotation is not for method");
}
if (!method.declaringType.equals(declaringType)) {
throw new IllegalArgumentException("Method" + method + "'s declaring type is inconsistent with" + this);
}
ClassDefItem classDefItem = dexMaker.getTypeDeclaration(declaringType).toClassDefItem();
if (classDefItem == null) {
throw new NullPointerException("No class defined item is found");
} else {
CstMethodRef cstMethodRef = method.constant;
if (cstMethodRef == null) {
throw new NullPointerException("Method reference is NULL");
} else {
// Generate CstType
CstType cstType = CstType.intern(type.ropType);
// Generate Annotation
Annotation annotation = new Annotation(cstType, AnnotationVisibility.RUNTIME);
// Add generated annotation
Annotations annotations = new Annotations();
for (NameValuePair nvp : elements.values()) {
annotation.add(nvp);
}
annotations.add(annotation);
classDefItem.addMethodAnnotations(cstMethodRef, annotations, dexMaker.getDexFile());
}
}
}
/**
* A wrapper of <code>NameValuePair</code> represents a (name, value) pair used as the contents
* of an annotation.
*
* An {@code Element} instance is stored in {@code AnnotationId.elements} by calling {@code
* AnnotationId.set(Element)}.
*
* <p><strong>WARNING: </strong></p>
* the name should be exact same as the annotation element declared in the annotation type
* which is referred by field {@code AnnotationId.type},otherwise the annotation will fail
* to add and {@code java.lang.reflect.Method.getAnnotations()} will return nothing.
*
*/
public static final class Element {
/** {@code non-null;} the name */
private final String name;
/** {@code non-null;} the value */
private final Object value;
/**
* Construct an instance.
*
* @param name {@code non-null;} the name
* @param value {@code non-null;} the value
*/
public Element(String name, Object value) {
if (name == null) {
throw new NullPointerException("name == null");
}
if (value == null) {
throw new NullPointerException("value == null");
}
this.name = name;
this.value = value;
}
public String getName() {
return name;
}
public Object getValue() {
return value;
}
/** {@inheritDoc} */
@Override
public String toString() {
return "[" + name + ", " + value + "]";
}
/** {@inheritDoc} */
@Override
public int hashCode() {
return name.hashCode() * 31 + value.hashCode();
}
/** {@inheritDoc} */
@Override
public boolean equals(Object other) {
if (! (other instanceof Element)) {
return false;
}
Element otherElement = (Element) other;
return name.equals(otherElement.name)
&& value.equals(otherElement.value);
}
/**
* Convert a value of an element to a {@code Constant}.
* <p><strong>Warning:</strong> Array or TypeId value is not supported yet.
*
* @param value an annotation element value.
* @return a Constant
*/
static Constant toConstant(Object value) {
Class clazz = value.getClass();
if (clazz.isEnum()) {
CstString descriptor = new CstString(TypeId.get(clazz).getName());
CstString name = new CstString(((Enum)value).name());
CstNat cstNat = new CstNat(name, descriptor);
return new CstEnumRef(cstNat);
} else if (clazz.isArray()) {
throw new UnsupportedOperationException("Array is not supported yet");
} else if (value instanceof TypeId) {
throw new UnsupportedOperationException("TypeId is not supported yet");
} else {
return Constants.getConstant(value);
}
}
}
}