/*
* JBoss, Home of Professional Open Source
* Copyright 2008, Red Hat, Inc., and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* 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 org.jboss.weld.util.reflection;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
import javax.enterprise.inject.spi.AnnotatedConstructor;
import javax.enterprise.inject.spi.AnnotatedField;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedParameter;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.InjectionPoint;
import org.jboss.classfilewriter.util.DescriptorUtils;
import org.jboss.weld.ejb.spi.BusinessInterfaceDescriptor;
import org.jboss.weld.resources.ClassLoaderResourceLoader;
import org.jboss.weld.resources.DefaultResourceLoader;
import org.jboss.weld.resources.WeldClassLoaderResourceLoader;
import org.jboss.weld.resources.spi.ResourceLoader;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
/**
* Utility class to produce friendly names e.g. for debugging
*
* @author Pete Muir
* @author Nicklas Karlsson
* @author Jozef Hartinger
*/
public class Formats {
private static final String SNAPSHOT = "SNAPSHOT";
private static final String NULL = "null";
private static final String SQUARE_BRACKETS = "[]";
private static final String BCEL_CLASS_PARSER_FQCN = "com.sun.org.apache.bcel.internal.classfile.ClassParser";
private static final String BCEL_JAVA_CLASS_FQCN = "com.sun.org.apache.bcel.internal.classfile.JavaClass";
private static final String BCEL_METHOD_FQCN = "com.sun.org.apache.bcel.internal.classfile.Method";
private static final String BCEL_LINE_NUMBER_TABLE_FQCN = "com.sun.org.apache.bcel.internal.classfile.LineNumberTable";
private static final String BCEL_M_PARSE = "parse";
private static final String BCEL_M_GET_METHODS = "getMethods";
private static final String BCEL_M_GET_LINE_NUMBER_TABLE = "getLineNumberTable";
private static final String BCEL_M_GET_SOURCE_LINE = "getSourceLine";
private static final String BCEL_M_GET_NAME = "getName";
private static final String BCEL_M_GET_MODIFIERS = "getModifiers";
private static final String BCEL_M_GET_SIGNATURE = "getSignature";
private static final String INIT_METHOD_NAME = "<init>";
private static final String BUILD_PROPERTIES_FILE = "weld-build.properties";
private static final String BUILD_PROPERTIES_VERSION = "version";
private static final String BUILD_PROPERTIES_TIMESTAMP = "timestamp";
private static final String UPPER_BOUND = " extends ";
private static final String WILDCARD = "?";
private static final String WILDCARD_UPPER_BOUND = WILDCARD + UPPER_BOUND;
private static final String WILDCARD_LOWER_BOUND = WILDCARD + " super ";
private static final String GT = ">";
private static final String LT = "<";
private Formats() {
}
/**
* See also WELD-1454.
*
* @param ij
* @return the formatted string
*/
public static String formatAsStackTraceElement(InjectionPoint ij) {
Member member;
if (ij.getAnnotated() instanceof AnnotatedField) {
AnnotatedField<?> annotatedField = (AnnotatedField<?>) ij.getAnnotated();
member = annotatedField.getJavaMember();
} else if (ij.getAnnotated() instanceof AnnotatedParameter<?>) {
AnnotatedParameter<?> annotatedParameter = (AnnotatedParameter<?>) ij.getAnnotated();
member = annotatedParameter.getDeclaringCallable().getJavaMember();
} else {
// Not throwing an exception, because this method is invoked when an exception is already being thrown.
// Throwing an exception here would hide the original exception.
return "-";
}
return formatAsStackTraceElement(member);
}
public static String formatAsStackTraceElement(Member member) {
return member.getDeclaringClass().getName()
+ "." + (member instanceof Constructor<?> ? INIT_METHOD_NAME : member.getName())
+ "(" + getFileName(member.getDeclaringClass()) + ":" + getLineNumber(member) + ")";
}
/**
* Try to get the line number associated with the given member.
*
* The reflection API does not expose such an info and so we need to analyse the bytecode. Unfortunately, it seems there is no way to get this kind of
* information for fields. Moreover, the <code>LineNumberTable</code> attribute is just optional, i.e. the compiler is not required to store this
* information at all. See also <a href="http://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.1">Java Virtual Machine Specification</a>
*
* Implementation note: it wouldn't be appropriate to add a bytecode scanning dependency just for this functionality, therefore Apache BCEL included in
* Oracle JDK 1.5+ and OpenJDK 1.6+ is used. Other JVMs should not crash as we only use it if it's on the classpath and by means of reflection calls.
*
* @param member
* @param resourceLoader
* @return the line number or 0 if it's not possible to find it
*/
public static int getLineNumber(Member member) {
if (!(member instanceof Method || member instanceof Constructor)) {
// We are not able to get this info for fields
return 0;
}
ResourceLoader bcelResourceLoader = WeldClassLoaderResourceLoader.INSTANCE;
if (!Reflections.isClassLoadable(BCEL_JAVA_CLASS_FQCN, bcelResourceLoader)) {
// Try TCCL as a fallback
bcelResourceLoader = DefaultResourceLoader.INSTANCE;
if (!Reflections.isClassLoadable(BCEL_JAVA_CLASS_FQCN, bcelResourceLoader)) {
// Apache BCEL classes not found on the classpath
return 0;
}
}
String classFile = member.getDeclaringClass().getName().replace('.', '/');
ClassLoaderResourceLoader classFileResourceLoader = new ClassLoaderResourceLoader(member.getDeclaringClass().getClassLoader());
InputStream in = null;
try {
URL classFileUrl = classFileResourceLoader.getResource(classFile + ".class");
if (classFileUrl == null) {
// The class file is not available
return 0;
}
in = classFileUrl.openStream();
Class<?> classParserClass = Reflections.loadClass(BCEL_CLASS_PARSER_FQCN, bcelResourceLoader);
Class<?> javaClassClass = Reflections.loadClass(BCEL_JAVA_CLASS_FQCN, bcelResourceLoader);
Class<?> methodClass = Reflections.loadClass(BCEL_METHOD_FQCN, bcelResourceLoader);
Class<?> lntClass = Reflections.loadClass(BCEL_LINE_NUMBER_TABLE_FQCN, bcelResourceLoader);
Object parser = classParserClass.getConstructor(InputStream.class, String.class).newInstance(in, classFile);
Object javaClass = classParserClass.getMethod(BCEL_M_PARSE).invoke(parser);
// First get all declared methods and constructors
// Note that in bytecode constructor is translated into a method
Object[] methods = (Object[]) javaClassClass.getMethod(BCEL_M_GET_METHODS).invoke(javaClass);
Object match = null;
String signature;
String name;
if (member instanceof Method) {
signature = DescriptorUtils.methodDescriptor((Method) member);
name = member.getName();
} else if (member instanceof Constructor) {
signature = DescriptorUtils.makeDescriptor((Constructor<?>) member);
name = INIT_METHOD_NAME;
} else {
return 0;
}
for (Object method : methods) {
// Matching method must have the same name, modifiers and signature
if (methodClass.getMethod(BCEL_M_GET_NAME).invoke(method).equals(name)
&& methodClass.getMethod(BCEL_M_GET_MODIFIERS).invoke(method).equals(member.getModifiers())
&& methodClass.getMethod(BCEL_M_GET_SIGNATURE).invoke(method).equals(signature)) {
match = method;
}
}
if (match != null) {
// If a method is found, try to obtain the optional LineNumberTable attribute
Object lineNumberTable = methodClass.getMethod(BCEL_M_GET_LINE_NUMBER_TABLE).invoke(match);
if (lineNumberTable != null) {
int line = (int) lntClass.getMethod(BCEL_M_GET_SOURCE_LINE, int.class).invoke(lineNumberTable, 0);
return line == -1 ? 0 : line;
}
}
// No suitable method found
return 0;
} catch (Throwable t) {
return 0;
} finally {
if (in != null) {
try {
in.close();
} catch (Exception e) {
return 0;
}
}
}
}
private static String getFileName(Class<?> clazz) {
return clazz.getSimpleName() + ".java";
}
/**
* A transformation from one object to a String.
*
* @param <F> the type of the function input
*/
private interface Function<F> {
/**
* Applies the function to an object of type {@code F}, resulting in an
* object of type String.
*
* @param from the source object
* @param position the position in the list the object is at
* @return the resulting object
*/
String apply(F from, int position);
}
private static final Function<?> SPACE_DELIMITER_FUNCTION = new Function<Object>() {
@Override
public String apply(Object from, int position) {
if (position > 0) {
return " " + (from == null ? NULL : from.toString());
} else {
return from == null ? NULL : from.toString();
}
}
};
private static final Function<?> COMMA_DELIMITER_FUNCTION = new Function<Object>() {
@Override
public String apply(Object from, int position) {
if (position > 0) {
return ", " + (from == null ? NULL : from.toString());
} else {
return from == null ? NULL : from.toString();
}
}
};
private static final Function<Annotation> ANNOTATION_LIST_FUNCTION = new Function<Annotation>() {
@Override
public String apply(Annotation from, int position) {
return spaceDelimiterFunction().apply("@" + from.annotationType().getSimpleName(), position);
}
};
@SuppressWarnings("unchecked")
private static <T> Function<T> spaceDelimiterFunction() {
return (Function<T>) SPACE_DELIMITER_FUNCTION;
}
@SuppressWarnings("unchecked")
private static <T> Function<T> commaDelimiterFunction() {
return (Function<T>) COMMA_DELIMITER_FUNCTION;
}
public static String formatInjectionPointType(Type type) {
if (type instanceof Class<?>) {
return ((Class<?>) type).getSimpleName();
} else {
return Formats.formatType(type);
}
}
public static String formatType(Type baseType) {
return formatType(baseType, true);
}
public static String formatType(Type baseType, boolean simpleNames) {
if (baseType == null) {
return NULL;
}
if (baseType instanceof Class<?>) {
Class<?> clazz = (Class<?>) baseType;
if (clazz.isArray()) {
return formatType(clazz.getComponentType(), simpleNames) + SQUARE_BRACKETS;
}
return getClassName(clazz, simpleNames);
}
if (baseType instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) baseType;
return getClassName((Class<?>) parameterizedType.getRawType(), simpleNames) + formatActualTypeArguments(parameterizedType.getActualTypeArguments());
} else if (baseType instanceof WildcardType) {
WildcardType wildcardType = (WildcardType) baseType;
Type[] upperBound = wildcardType.getUpperBounds();
Type[] lowerBound = wildcardType.getLowerBounds();
if(lowerBound.length == 0 && Reflections.isEmptyBoundArray(upperBound)) {
return WILDCARD;
} else if(lowerBound.length == 0) {
return WILDCARD_UPPER_BOUND + formatType(upperBound[0], simpleNames);
}
return WILDCARD_LOWER_BOUND + formatType(lowerBound[0], simpleNames);
} else if (baseType instanceof GenericArrayType) {
GenericArrayType gat = (GenericArrayType) baseType;
return formatType(gat.getGenericComponentType(), simpleNames) + SQUARE_BRACKETS;
} else if (baseType instanceof TypeVariable) {
return formatTypeVariable((TypeVariable<?>) baseType, simpleNames);
}
return baseType.toString();
}
private static String getClassName(Class<?> clazz, boolean simpleNames) {
if (simpleNames) {
return clazz.getSimpleName();
} else {
return clazz.getName();
}
}
public static String formatTypes(Iterable<? extends Type> baseTypes, boolean simpleNames) {
return formatIterable(baseTypes, new Function<Type>() {
@Override
public String apply(Type from, int position) {
return commaDelimiterFunction().apply(formatType(from, simpleNames), position);
}
});
}
public static String formatTypes(Iterable<? extends Type> baseTypes) {
return formatTypes(baseTypes, true);
}
public static String formatBusinessInterfaceDescriptors(Iterable<? extends BusinessInterfaceDescriptor<?>> businessInterfaceDescriptors) {
return formatIterable(businessInterfaceDescriptors, new Function<BusinessInterfaceDescriptor<?>>() {
@Override
public String apply(BusinessInterfaceDescriptor<?> from, int position) {
return commaDelimiterFunction().apply(formatType(from.getInterface()), position);
}
});
}
public static String addSpaceIfNeeded(String string) {
if (string.length() > 0) {
return string + " ";
} else {
return string;
}
}
public static String formatAsFormalParameterList(Iterable<? extends AnnotatedParameter<?>> parameters) {
return "(" + formatIterable(parameters, new Function<AnnotatedParameter<?>>() {
@Override
public String apply(AnnotatedParameter<?> from, int position) {
return commaDelimiterFunction().apply(formatParameter(from), position);
}
}) + ")";
}
public static String formatParameter(AnnotatedParameter<?> parameter) {
return addSpaceIfNeeded(formatAnnotations(parameter.getAnnotations())) + formatType(parameter.getBaseType());
}
public static String formatModifiers(int modifiers) {
return formatIterable(parseModifiers(modifiers), spaceDelimiterFunction());
}
private static <F> String formatIterable(Iterable<? extends F> items, Function<F> function) {
if (items == null) {
return "";
}
StringBuilder stringBuilder = new StringBuilder();
int i = 0;
for (F item : items) {
stringBuilder.append(function.apply(item, i));
i++;
}
return stringBuilder.toString();
}
private static <F> String formatIterable(F[] items, Function<F> function) {
StringBuilder stringBuilder = new StringBuilder();
int i = 0;
for (F item : items) {
stringBuilder.append(function.apply(item, i));
i++;
}
return stringBuilder.toString();
}
/**
* Parses a reflection modifier to a list of string
*
* @param modifiers The modifier to parse
* @return The resulting string list
*/
private static List<String> parseModifiers(int modifiers) {
List<String> result = new ArrayList<String>();
if (Modifier.isPrivate(modifiers)) {
result.add("private");
}
if (Modifier.isProtected(modifiers)) {
result.add("protected");
}
if (Modifier.isPublic(modifiers)) {
result.add("public");
}
if (Modifier.isAbstract(modifiers)) {
result.add("abstract");
}
if (Modifier.isFinal(modifiers)) {
result.add("final");
}
if (Modifier.isNative(modifiers)) {
result.add("native");
}
if (Modifier.isStatic(modifiers)) {
result.add("static");
}
if (Modifier.isStrict(modifiers)) {
result.add("strict");
}
if (Modifier.isSynchronized(modifiers)) {
result.add("synchronized");
}
if (Modifier.isTransient(modifiers)) {
result.add("transient");
}
if (Modifier.isVolatile(modifiers)) {
result.add("volatile");
}
if (Modifier.isInterface(modifiers)) {
result.add("interface");
}
return result;
}
public static String formatActualTypeArguments(Type type) {
if (type instanceof ParameterizedType) {
return formatActualTypeArguments(ParameterizedType.class.cast(type).getActualTypeArguments());
}
return "";
}
public static String formatActualTypeArguments(Type[] actualTypeArguments) {
return formatActualTypeArguments(actualTypeArguments, true);
}
public static String formatActualTypeArguments(Type[] actualTypeArguments, boolean simpleNames) {
return wrapIfNecessary(formatIterable(actualTypeArguments, new Function<Type>() {
@Override
public String apply(Type from, int position) {
return commaDelimiterFunction().apply(formatType(from, simpleNames), position);
}
}), LT, GT);
}
public static String wrapIfNecessary(String string, String prepend, String append) {
if (string != null && string.length() > 0) {
return prepend + string + append;
} else {
return string;
}
}
public static String formatAnnotations(Iterable<Annotation> annotations) {
return formatIterable(annotations, ANNOTATION_LIST_FUNCTION);
}
/**
* Gets a string representation from an array of annotations
*
* @param annotations The annotations
* @return The string representation
*/
public static String formatAnnotations(Annotation[] annotations) {
return formatIterable(annotations, ANNOTATION_LIST_FUNCTION);
}
/**
*
* @param pkg This param is completely ignored
* @return the formatted version
*/
public static String version(@Deprecated Package pkg) {
String version = null;
String timestamp = null;
// First try the weld-build.properties file
Properties buildProperties = getBuildProperties();
if (buildProperties != null) {
version = buildProperties.getProperty(BUILD_PROPERTIES_VERSION);
timestamp = buildProperties.getProperty(BUILD_PROPERTIES_TIMESTAMP);
}
if (version == null) {
// If needed use the manifest info
version = getManifestImplementationVersion();
}
return version(version, timestamp);
}
/**
*
* @return a simple version string, i.e. no formatting is applied
*/
public static String getSimpleVersion() {
Properties buildProperties = getBuildProperties();
if(buildProperties != null) {
buildProperties.getProperty(BUILD_PROPERTIES_VERSION);
}
return getManifestImplementationVersion();
}
@SuppressFBWarnings(value = "NP_NULL_ON_SOME_PATH_MIGHT_BE_INFEASIBLE", justification = "False positive.")
public static String version(String version, String timestamp) {
if (version == null && timestamp != null) {
return timestamp;
} else if (version == null && timestamp == null) {
return SNAPSHOT;
}
String major = null;
String minor = null;
String micro = null;
String qualifier = null;
List<String> split = new ArrayList<String>(Arrays.asList(version.split("\\.")));
String[] split2 = split.get(split.size() - 1).split("\\-");
if (split2.length > 1) {
// We split it, so swap out the last digit
split.remove(split.size() - 1);
split.add(split.size(), split2[0]);
qualifier = split2[1];
} else if (split2.length > 0) {
// We didn't split it
split.remove(split.size() - 1);
qualifier = split2[0];
}
if (split.size() > 0) {
major = split.get(0);
}
if (split.size() > 1) {
minor = split.get(1);
}
if (split.size() > 2) {
micro = split.get(2);
}
if (major == null && timestamp != null) {
// Handle the case we only have a timestamp
return timestamp;
}
if (major == null && timestamp == null) {
// Handle the case we have nothing
return SNAPSHOT;
}
StringBuilder builder = new StringBuilder();
builder.append(major);
if (minor != null) {
builder.append(".").append(minor);
}
if (minor != null && micro != null) {
builder.append(".").append(micro);
}
if (qualifier != null) {
builder.append(" (");
if (qualifier.equals(SNAPSHOT) && timestamp != null) {
builder.append(timestamp);
} else {
builder.append(qualifier);
}
builder.append(")");
}
return builder.toString();
}
public static String formatSimpleClassName(Object object) {
return formatSimpleClassName(object.getClass());
}
public static String formatSimpleClassName(Class<?> javaClass) {
String simpleName = javaClass.getSimpleName();
StringBuilder builder = new StringBuilder(simpleName.length() + 2);
builder.append("[");
builder.append(simpleName);
builder.append("]");
return builder.toString();
}
public static String formatAnnotatedType(AnnotatedType<?> type) {
return Formats.formatSimpleClassName(type) + " " + Formats.addSpaceIfNeeded(Formats.formatModifiers(type.getJavaClass().getModifiers()))
+ Formats.formatAnnotations(type.getAnnotations()) + " class " + type.getJavaClass().getName() + Formats.formatActualTypeArguments(type.getBaseType());
}
public static String formatAnnotatedConstructor(AnnotatedConstructor<?> constructor) {
return Formats.formatSimpleClassName(constructor) + " " + Formats.addSpaceIfNeeded(Formats.formatAnnotations(constructor.getAnnotations()))
+ Formats.addSpaceIfNeeded(Formats.formatModifiers(constructor.getJavaMember().getModifiers())) + constructor.getDeclaringType().getJavaClass().getName()
+ Formats.formatAsFormalParameterList(constructor.getParameters());
}
public static String formatAnnotatedField(AnnotatedField<?> field) {
return Formats.formatSimpleClassName(field) + " " + Formats.addSpaceIfNeeded(Formats.formatAnnotations(field.getAnnotations()))
+ Formats.addSpaceIfNeeded(Formats.formatModifiers(field.getJavaMember().getModifiers())) + field.getDeclaringType().getJavaClass().getName() + "."
+ field.getJavaMember().getName();
}
public static String formatAnnotatedMethod(AnnotatedMethod<?> method) {
return Formats.formatSimpleClassName(method) + " " + Formats.addSpaceIfNeeded(Formats.formatAnnotations(method.getAnnotations()))
+ Formats.addSpaceIfNeeded(Formats.formatModifiers(method.getJavaMember().getModifiers())) + method.getDeclaringType().getJavaClass().getName() + "."
+ method.getJavaMember().getName() + Formats.formatAsFormalParameterList(method.getParameters());
}
public static String formatAnnotatedParameter(AnnotatedParameter<?> parameter) {
return Formats.formatSimpleClassName(parameter) + " Parameter " + (parameter.getPosition() + 1) + " of " + parameter.getDeclaringCallable().toString();
}
/**
* Attempts to extract a name of a missing class loader dependency from an exception such as {@link NoClassDefFoundError} or {@link ClassNotFoundException}.
*/
public static String getNameOfMissingClassLoaderDependency(Throwable e) {
if (e instanceof NoClassDefFoundError) {
// NoClassDefFoundError sometimes includes CNFE as the cause. Since CNFE has a better formatted class name
// and may also include classloader info, we prefer CNFE's over NCDFE's message.
if (e.getCause() instanceof ClassNotFoundException) {
return getNameOfMissingClassLoaderDependency(e.getCause());
}
if (e.getMessage() != null) {
return e.getMessage().replace('/', '.');
}
}
if (e instanceof ClassNotFoundException) {
if (e.getMessage() != null) {
return e.getMessage();
}
}
if (e.getCause() != null) {
return getNameOfMissingClassLoaderDependency(e.getCause());
} else {
return "[unknown]";
}
}
public static <D extends GenericDeclaration> String formatTypeParameters(TypeVariable<D>[] typeParams) {
return wrapIfNecessary(formatIterable(typeParams, new Function<TypeVariable<D>>() {
@Override
public String apply(TypeVariable<D> from, int position) {
return spaceDelimiterFunction().apply(formatTypeVariable(from, true), position);
}
}), LT, GT);
}
private static <D extends GenericDeclaration> String formatTypeVariable(TypeVariable<D> typeVariable, boolean simpleNames) {
Type[] bounds = typeVariable.getBounds();
if (Reflections.isEmptyBoundArray(bounds)) {
return typeVariable.getName();
}
return typeVariable.getName() + UPPER_BOUND + formatType(bounds[0], simpleNames);
}
private static Properties getBuildProperties() {
Properties buildProperties = null;
try (InputStream in = getBuildPropertiesResource()) {
if (in != null) {
buildProperties = new Properties();
buildProperties.load(in);
}
} catch (IOException ignored) {
}
return buildProperties;
}
private static String getManifestImplementationVersion() {
Package pack = WeldClassLoaderResourceLoader.class.getPackage();
if (pack == null) {
throw new IllegalArgumentException("Package can not be null");
}
return pack.getImplementationVersion();
}
private static InputStream getBuildPropertiesResource() {
URL url = WeldClassLoaderResourceLoader.INSTANCE.getResource(BUILD_PROPERTIES_FILE);
try {
return url != null ? url.openStream() : null;
} catch (IOException e) {
return null;
}
}
}