/*
* Copyright (C) 2009-2013 The Project Lombok Authors.
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package lombok.core;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import lombok.core.AST.Kind;
/**
* Represents a single annotation in a source file and can be used to query the parameters present on it.
*
* @param A The annotation that this class represents, such as {@code lombok.Getter}
*/
public class AnnotationValues<A extends Annotation> {
private final Class<A> type;
private final Map<String, AnnotationValue> values;
private final LombokNode<?, ?, ?> ast;
/**
* Represents a single method on the annotation class. For example, the value() method on the Getter annotation.
*/
public static class AnnotationValue {
/** A list of the raw expressions. List is size 1 unless an array is provided. */
public final List<String> raws;
/** Guesses for each raw expression. If the raw expression is a literal expression, the guess will
* likely be right. If not, it'll be wrong. */
public final List<Object> valueGuesses;
/** A list of the actual expressions. List is size 1 unless an array is provided. */
public final List<Object> expressions;
private final LombokNode<?, ?, ?> node;
private final boolean isExplicit;
/**
* Like the other constructor, but used for when the annotation method is initialized with an array value.
*/
public AnnotationValue(LombokNode<?, ?, ?> node, List<String> raws, List<Object> expressions, List<Object> valueGuesses, boolean isExplicit) {
this.node = node;
this.raws = raws;
this.expressions = expressions;
this.valueGuesses = valueGuesses;
this.isExplicit = isExplicit;
}
/**
* Override this if you want more specific behaviour (to get the source position just right).
*
* @param message English message with the problem.
* @param valueIdx The index into the values for this annotation key that caused the problem.
* -1 for a problem that applies to all values, otherwise the 0-based index into an array of values.
* If there is no array for this value (e.g. value=1 instead of value={1,2}), then always -1 or 0.
*/
public void setError(String message, int valueIdx) {
node.addError(message);
}
/**
* Override this if you want more specific behaviour (to get the source position just right).
*
* @param message English message with the problem.
* @param valueIdx The index into the values for this annotation key that caused the problem.
* -1 for a problem that applies to all values, otherwise the 0-based index into an array of values.
* If there is no array for this value (e.g. value=1 instead of value={1,2}), then always -1 or 0.
*/
public void setWarning(String message, int valueIdx) {
node.addError(message);
}
/** {@inheritDoc} */
@Override public String toString() {
return "raws: " + raws + " valueGuesses: " + valueGuesses;
}
public boolean isExplicit() {
return isExplicit;
}
}
/**
* Creates a new AnnotationValues.
*
* @param type The annotation type. For example, "Getter.class"
* @param values a Map of method names to AnnotationValue instances, for example 'value -> annotationValue instance'.
* @param ast The Annotation node.
*/
public AnnotationValues(Class<A> type, Map<String, AnnotationValue> values, LombokNode<?, ?, ?> ast) {
this.type = type;
this.values = values;
this.ast = ast;
}
public static <A extends Annotation> AnnotationValues<A> of(Class<A> type) {
return new AnnotationValues<A>(type, Collections.<String, AnnotationValue>emptyMap(), null);
}
/**
* Creates a new annotation wrapper with all default values, and using the provided ast as lookup anchor for
* class literals.
*/
public static <A extends Annotation> AnnotationValues<A> of(Class<A> type, LombokNode<?, ?, ?> ast) {
return new AnnotationValues<A>(type, Collections.<String, AnnotationValue>emptyMap(), ast);
}
/**
* Thrown on the fly if an actual annotation instance procured via the {@link #getInstance()} method is queried
* for a method for which this AnnotationValues instance either doesn't have a guess or can't manage to fit
* the guess into the required data type.
*/
public static class AnnotationValueDecodeFail extends RuntimeException {
private static final long serialVersionUID = 1L;
/** The index into an array initializer (e.g. if the second value in an array initializer is
* an integer constant expression like '5+SomeOtherClass.CONSTANT', this exception will be thrown,
* and you'll get a '1' for idx. */
public final int idx;
/** The AnnotationValue object that goes with the annotation method for which the failure occurred. */
public final AnnotationValue owner;
public AnnotationValueDecodeFail(AnnotationValue owner, String msg, int idx) {
super(msg);
this.idx = idx;
this.owner = owner;
}
}
private static AnnotationValueDecodeFail makeNoDefaultFail(AnnotationValue owner, Method method) {
return new AnnotationValueDecodeFail(owner,
"No value supplied but " + method.getName() + " has no default either.", -1);
}
private A cachedInstance = null;
/**
* Creates an actual annotation instance. You can use this to query any annotation methods, except for
* those annotation methods with class literals, as those can most likely not be turned into Class objects.
*
* If some of the methods cannot be implemented, this method still works; it's only when you call a method
* that has a problematic value that an AnnotationValueDecodeFail exception occurs.
*/
@SuppressWarnings("unchecked")
public A getInstance() {
if (cachedInstance != null) return cachedInstance;
InvocationHandler invocations = new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
AnnotationValue v = values.get(method.getName());
if (v == null) {
Object defaultValue = method.getDefaultValue();
if (defaultValue != null) return defaultValue;
throw makeNoDefaultFail(v, method);
}
boolean isArray = false;
Class<?> expected = method.getReturnType();
Object array = null;
if (expected.isArray()) {
isArray = true;
expected = expected.getComponentType();
array = Array.newInstance(expected, v.valueGuesses.size());
}
if (!isArray && v.valueGuesses.size() > 1) {
throw new AnnotationValueDecodeFail(v,
"Expected a single value, but " + method.getName() + " has an array of values", -1);
}
if (v.valueGuesses.size() == 0 && !isArray) {
Object defaultValue = method.getDefaultValue();
if (defaultValue == null) throw makeNoDefaultFail(v, method);
return defaultValue;
}
int idx = 0;
for (Object guess : v.valueGuesses) {
Object result = guess == null ? null : guessToType(guess, expected, v, idx);
if (!isArray) {
if (result == null) {
Object defaultValue = method.getDefaultValue();
if (defaultValue == null) throw makeNoDefaultFail(v, method);
return defaultValue;
}
return result;
}
if (result == null) {
if (v.valueGuesses.size() == 1) {
Object defaultValue = method.getDefaultValue();
if (defaultValue == null) throw makeNoDefaultFail(v, method);
return defaultValue;
}
throw new AnnotationValueDecodeFail(v,
"I can't make sense of this annotation value. Try using a fully qualified literal.", idx);
}
Array.set(array, idx++, result);
}
return array;
}
};
return cachedInstance = (A) Proxy.newProxyInstance(type.getClassLoader(), new Class[] { type }, invocations);
}
private Object guessToType(Object guess, Class<?> expected, AnnotationValue v, int pos) {
if (expected == int.class) {
if (guess instanceof Integer || guess instanceof Short || guess instanceof Byte) {
return ((Number)guess).intValue();
}
}
if (expected == long.class) {
if (guess instanceof Long || guess instanceof Integer || guess instanceof Short || guess instanceof Byte) {
return ((Number)guess).longValue();
}
}
if (expected == short.class) {
if (guess instanceof Integer || guess instanceof Short || guess instanceof Byte) {
int intVal = ((Number)guess).intValue();
int shortVal = ((Number)guess).shortValue();
if (shortVal == intVal) return shortVal;
}
}
if (expected == byte.class) {
if (guess instanceof Integer || guess instanceof Short || guess instanceof Byte) {
int intVal = ((Number)guess).intValue();
int byteVal = ((Number)guess).byteValue();
if (byteVal == intVal) return byteVal;
}
}
if (expected == double.class) {
if (guess instanceof Number) return ((Number)guess).doubleValue();
}
if (expected == float.class) {
if (guess instanceof Number) return ((Number)guess).floatValue();
}
if (expected == boolean.class) {
if (guess instanceof Boolean) return ((Boolean)guess).booleanValue();
}
if (expected == char.class) {
if (guess instanceof Character) return ((Character)guess).charValue();
}
if (expected == String.class) {
if (guess instanceof String) return guess;
}
if (Enum.class.isAssignableFrom(expected) ) {
if (guess instanceof String) {
for (Object enumConstant : expected.getEnumConstants()) {
String target = ((Enum<?>)enumConstant).name();
if (target.equals(guess)) return enumConstant;
}
throw new AnnotationValueDecodeFail(v,
"Can't translate " + guess + " to an enum of type " + expected, pos);
}
}
if (Class.class == expected) {
if (guess instanceof String) try {
return Class.forName(toFQ((String)guess));
} catch (ClassNotFoundException e) {
throw new AnnotationValueDecodeFail(v,
"Can't translate " + guess + " to a class object.", pos);
}
}
throw new AnnotationValueDecodeFail(v,
"Can't translate a " + guess.getClass() + " to the expected " + expected, pos);
}
/**
* Returns the raw expressions used for the provided {@code annotationMethodName}.
*
* You should use this method for annotation methods that return {@code Class} objects. Remember that
* class literals end in ".class" which you probably want to strip off.
*/
public List<String> getRawExpressions(String annotationMethodName) {
AnnotationValue v = values.get(annotationMethodName);
return v == null ? Collections.<String>emptyList() : v.raws;
}
/**
* Returns the actual expressions used for the provided {@code annotationMethodName}.
*/
public List<Object> getActualExpressions(String annotationMethodName) {
AnnotationValue v = values.get(annotationMethodName);
return v == null ? Collections.<Object>emptyList() : v.expressions;
}
public boolean isExplicit(String annotationMethodName) {
AnnotationValue annotationValue = values.get(annotationMethodName);
return annotationValue != null && annotationValue.isExplicit();
}
/**
* Convenience method to return the first result in a {@link #getRawExpressions(String)} call.
*
* You should use this method if the annotation method is not an array type.
*/
public String getRawExpression(String annotationMethodName) {
List<String> l = getRawExpressions(annotationMethodName);
return l.isEmpty() ? null : l.get(0);
}
/**
* Convenience method to return the first result in a {@link #getActualExpressions(String)} call.
*
* You should use this method if the annotation method is not an array type.
*/
public Object getActualExpression(String annotationMethodName) {
List<Object> l = getActualExpressions(annotationMethodName);
return l.isEmpty() ? null : l.get(0);
}
/** Generates an error message on the stated annotation value (you should only call this method if you know it's there!) */
public void setError(String annotationMethodName, String message) {
setError(annotationMethodName, message, -1);
}
/** Generates a warning message on the stated annotation value (you should only call this method if you know it's there!) */
public void setWarning(String annotationMethodName, String message) {
setWarning(annotationMethodName, message, -1);
}
/** Generates an error message on the stated annotation value, which must have an array initializer.
* The index-th item in the initializer will carry the error (you should only call this method if you know it's there!) */
public void setError(String annotationMethodName, String message, int index) {
AnnotationValue v = values.get(annotationMethodName);
if (v == null) return;
v.setError(message, index);
}
/** Generates a warning message on the stated annotation value, which must have an array initializer.
* The index-th item in the initializer will carry the error (you should only call this method if you know it's there!) */
public void setWarning(String annotationMethodName, String message, int index) {
AnnotationValue v = values.get(annotationMethodName);
if (v == null) return;
v.setWarning(message, index);
}
/**
* Attempts to translate class literals to their fully qualified names, such as 'Throwable.class' to 'java.lang.Throwable'.
*
* This process is at best a guess, but it will take into account import statements.
*/
public List<String> getProbableFQTypes(String annotationMethodName) {
List<String> result = new ArrayList<String>();
AnnotationValue v = values.get(annotationMethodName);
if (v == null) return Collections.emptyList();
for (Object o : v.valueGuesses) result.add(o == null ? null : toFQ(o.toString()));
return result;
}
/**
* Convenience method to return the first result in a {@link #getProbableFQType(String)} call.
*
* You should use this method if the annotation method is not an array type.
*/
public String getProbableFQType(String annotationMethodName) {
List<String> l = getProbableFQTypes(annotationMethodName);
return l.isEmpty() ? null : l.get(0);
}
/*
* Credit goes to Petr Jiricka of Sun for highlighting the problems with the earlier version of this method.
*/
private String toFQ(String typeName) {
String prefix = typeName.indexOf('.') > -1 ? typeName.substring(0, typeName.indexOf('.')) : typeName;
/* 1. Walk through type names in this source file at this level. */ {
LombokNode<?, ?, ?> n = ast;
walkThroughCU:
while (n != null) {
if (n.getKind() == Kind.TYPE) {
String simpleName = n.getName();
if (prefix.equals(simpleName)) {
//We found a matching type name in the local hierarchy!
List<String> outerNames = new ArrayList<String>();
while (true) {
n = n.up();
if (n == null || n.getKind() == Kind.COMPILATION_UNIT) break;
if (n.getKind() == Kind.TYPE) outerNames.add(n.getName());
//If our type has a parent that isn't either the CompilationUnit or another type, then we are
//a method-local class or an anonymous inner class literal. These technically do have FQNs
//and we may, with a lot of effort, figure out their name, but, that's some fairly horrible code
//style and these methods have 'probable' in their name for a reason.
break walkThroughCU;
}
StringBuilder result = new StringBuilder();
if (ast.getPackageDeclaration() != null) result.append(ast.getPackageDeclaration());
if (result.length() > 0) result.append('.');
Collections.reverse(outerNames);
for (String outerName : outerNames) result.append(outerName).append('.');
result.append(typeName);
return result.toString();
}
}
n = n.up();
}
}
/* 2. Walk through non-star imports and search for a match. */ {
if (prefix.equals(typeName)) {
String fqn = ast.getImportList().getFullyQualifiedNameForSimpleName(typeName);
if (fqn != null) return fqn;
}
}
/* 3. Walk through star imports and, if they start with "java.", use Class.forName based resolution. */ {
for (String potential : ast.getImportList().applyNameToStarImports("java", typeName)) {
try {
Class<?> c = Class.forName(potential);
if (c != null) return c.getName();
} catch (Throwable t) {
//Class.forName failed for whatever reason - it most likely does not exist, continue.
}
}
}
/* 4. If the type name is a simple name, then our last guess is that it's another class in this package. */ {
if (typeName.indexOf('.') == -1) return inLocalPackage(ast, typeName);
}
/* 5. It's either an FQN or a nested class in another class in our package. Use code conventions to guess. */ {
char firstChar = typeName.charAt(0);
if (Character.isTitleCase(firstChar) || Character.isUpperCase(firstChar)) {
//Class names start with uppercase letters, so presume it's a nested class in another class in our package.
return inLocalPackage(ast, typeName);
}
//Presume it's fully qualified.
return typeName;
}
}
private static String inLocalPackage(LombokNode<?, ?, ?> node, String typeName) {
StringBuilder result = new StringBuilder();
if (node != null && node.getPackageDeclaration() != null) result.append(node.getPackageDeclaration());
if (result.length() > 0) result.append('.');
result.append(typeName);
return result.toString();
}
}