/* * Copyright (C) 2015 Sebastian Daschner, sebastian-daschner.com * * 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/LICENSE2.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.sebastian_daschner.jaxrs_analyzer.model; import com.sebastian_daschner.jaxrs_analyzer.LogProvider; import com.sebastian_daschner.jaxrs_analyzer.analysis.classes.ContextClassReader; import org.objectweb.asm.Type; import org.objectweb.asm.signature.SignatureReader; import org.objectweb.asm.util.TraceSignatureVisitor; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.TypeVariable; import java.util.*; import java.util.stream.Collectors; import java.util.stream.Stream; import static com.sebastian_daschner.jaxrs_analyzer.model.Types.*; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; /** * Contains Java and Javassist utility functionality. * * @author Sebastian Daschner */ public final class JavaUtils { public static final String INITIALIZER_NAME = "<init>"; private JavaUtils() { throw new UnsupportedOperationException(); } /** * Checks if the given method name is a Java initializer. * * @param name The method name * @return {@code true} if name is an initializer */ public static boolean isInitializerName(final String name) { return INITIALIZER_NAME.equals(name); } /** * Returns the annotation or {@code null} if the element is not annotated with that type. * <b>Note:</b> This step is necessary due to issues with external class loaders (e.g. Maven). * The classes may not be identical and are therefore compared by FQ class name. */ public static <A extends Annotation> A getAnnotation(final AnnotatedElement annotatedElement, final Class<A> annotationClass) { final Optional<Annotation> annotation = Stream.of(annotatedElement.getAnnotations()) .filter(a -> a.annotationType().getName().equals(annotationClass.getName())) .findAny(); return (A) annotation.orElse(null); } /** * Checks if the annotation is present on the annotated element. * <b>Note:</b> This step is necessary due to issues with external class loaders (e.g. Maven). * The classes may not be identical and are therefore compared by FQ class name. */ public static boolean isAnnotationPresent(final AnnotatedElement annotatedElement, final Class<?> annotationClass) { return Stream.of(annotatedElement.getAnnotations()).map(Annotation::annotationType).map(Class::getName).anyMatch(n -> n.equals(annotationClass.getName())); } /** * Determines the type which is most "specific" (i. e. parametrized types are more "specific" than generic types, * types which are not {@link Object} are less specific). If no exact statement can be made, the first type is chosen. * * @param types The types * @return The most "specific" type */ public static String determineMostSpecificType(final String... types) { switch (types.length) { case 0: throw new IllegalArgumentException("At least one type has to be provided"); case 1: return types[0]; case 2: return determineMostSpecific(types[0], types[1]); default: String currentMostSpecific = determineMostSpecific(types[0], types[1]); for (int i = 2; i < types.length; i++) { currentMostSpecific = determineMostSpecific(currentMostSpecific, types[i]); } return currentMostSpecific; } } private static String determineMostSpecific(final String firstType, final String secondType) { if (OBJECT.equals(secondType) || firstType.equals(secondType)) { return firstType; } if (OBJECT.equals(firstType)) return secondType; final List<String> firstTypeParameters = getTypeParameters(firstType); final List<String> secondTypeParameters = getTypeParameters(secondType); final boolean firstTypeParameterized = !firstTypeParameters.isEmpty(); final boolean secondTypeParameterized = !secondTypeParameters.isEmpty(); if (firstTypeParameterized || secondTypeParameterized) { if (firstTypeParameterized && !secondTypeParameterized) { return firstType; } if (!firstTypeParameterized) { return secondType; } if (firstTypeParameters.size() != secondTypeParameters.size()) // types parameters are not compatible, no statement can be made return firstType; for (int i = 0; i < firstTypeParameters.size(); i++) { final String firstInner = firstTypeParameters.get(i); final String secondInner = secondTypeParameters.get(i); if (firstInner.equals(secondInner)) continue; // desired to test against identity, i.e. which object was taken by comparison if (firstInner == determineMostSpecific(firstInner, secondInner)) return firstType; return secondType; } } final boolean firstTypeArray = firstType.charAt(0) == '['; final boolean secondTypeArray = secondType.charAt(0) == '['; if (firstTypeArray || secondTypeArray) { if (firstTypeArray && !secondTypeArray) { return firstType; } if (!firstTypeArray) { return secondType; } } // check if one type is inherited from other if (isAssignableTo(firstType, secondType)) return firstType; if (isAssignableTo(secondType, firstType)) return secondType; return firstType; } /** * Determines the type which is least "specific" (i. e. parametrized types are more "specific" than generic types, * types which are not {@link Object} are less specific). If no exact statement can be made, the second type is chosen. * * @param types The types * @return The most "specific" type * @see #determineMostSpecificType(String...) */ public static String determineLeastSpecificType(final String... types) { switch (types.length) { case 0: throw new IllegalArgumentException("At least one type has to be provided"); case 1: return types[0]; case 2: return determineLeastSpecific(types[0], types[1]); default: String currentLeastSpecific = determineLeastSpecific(types[0], types[1]); for (int i = 2; i < types.length; i++) { currentLeastSpecific = determineLeastSpecific(currentLeastSpecific, types[i]); } return currentLeastSpecific; } } private static String determineLeastSpecific(final String firstType, final String secondType) { final String mostSpecificType = determineMostSpecificType(firstType, secondType); // has to compare identity to see which String object was taken if (mostSpecificType == firstType) return secondType; return firstType; } /** * Checks if the left type is assignable to the right type, i.e. the right type is of the same or a sub-type. */ public static boolean isAssignableTo(final String leftType, final String rightType) { if (leftType.equals(rightType)) return true; final boolean firstTypeArray = leftType.charAt(0) == '['; if (firstTypeArray ^ rightType.charAt(0) == '[') { return false; } final Class<?> leftClass = loadClassFromType(leftType); final Class<?> rightClass = loadClassFromType(rightType); if (leftClass == null || rightClass == null) return false; final boolean bothTypesParameterized = hasTypeParameters(leftType) && hasTypeParameters(rightType); return rightClass.isAssignableFrom(leftClass) && (firstTypeArray || !bothTypesParameterized || getTypeParameters(leftType).equals(getTypeParameters(rightType))); } private static boolean hasTypeParameters(final String type) { return type.indexOf('<') >= 0; } /** * Converts the given JVM object type signature to a class name. Erasures parametrized types. * <p> * Example: {@code Ljava/util/List<Ljava/lang/String;>; -> java/util/List} * * @throws IllegalArgumentException If the type is not a reference or array type. */ public static String toClassName(final String type) { switch (type.charAt(0)) { case 'V': return CLASS_PRIMITIVE_VOID; case 'Z': return CLASS_PRIMITIVE_BOOLEAN; case 'C': return CLASS_PRIMITIVE_CHAR; case 'B': return CLASS_PRIMITIVE_BYTE; case 'S': return CLASS_PRIMITIVE_SHORT; case 'I': return CLASS_PRIMITIVE_INT; case 'F': return CLASS_PRIMITIVE_FLOAT; case 'J': return CLASS_PRIMITIVE_LONG; case 'D': return CLASS_PRIMITIVE_DOUBLE; case 'L': final int typeParamStart = type.indexOf('<'); final int endIndex = typeParamStart >= 0 ? typeParamStart : type.indexOf(';'); return type.substring(1, endIndex); case '[': case '+': case '-': return toClassName(type.substring(1)); case 'T': // TODO handle type variables return CLASS_OBJECT; default: throw new IllegalArgumentException("Not a type signature: " + type); } } /** * Converts the given JVM class name to a type signature. * <p> * Example: {@code java/util/List -> Ljava/util/List;} */ public static String toType(final String className) { return 'L' + className + ';'; } /** * Converts the given type signature to a human readable type string. * <p> * Example: {@code Ljava/util/Map<Ljava/lang/String;Ljava/lang/String;>; -> java.util.Map<java.lang.String, java.lang.String>} */ public static String toReadableType(final String type) { final SignatureReader reader = new SignatureReader(type); final TraceSignatureVisitor visitor = new TraceSignatureVisitor(0); reader.acceptType(visitor); return visitor.getDeclaration(); } /** * Returns the JVM type signature of the given object. */ public static String getType(final Object value) { return Type.getDescriptor(value.getClass()); } /** * Returns the type parameters of the given type. Will be an empty list if the type is not parametrized. */ public static List<String> getTypeParameters(final String type) { if (type.charAt(0) != 'L') return emptyList(); int lastStart = type.indexOf('<') + 1; final List<String> parameters = new ArrayList<>(); if (lastStart > 0) { int depth = 0; for (int i = lastStart; i < type.length() - 2; i++) { final char c = type.charAt(i); if (c == '<') depth++; else if (c == '>') depth--; else if (c == ';' && depth == 0) { parameters.add(type.substring(lastStart, i + 1)); lastStart = i + 1; } } } return parameters; } /** * Returns the return type of the given method signature. Parametrized types are supported. */ public static String getReturnType(final String methodSignature) { return getReturnType(methodSignature, null); } public static String getReturnType(final String methodSignature, final String containedType) { final String type = methodSignature.substring(methodSignature.lastIndexOf(')') + 1); return resolvePotentialTypeVariables(type, containedType); } private static Map<String, String> getTypeVariables(final String type) { if (type == null) return emptyMap(); final Map<String, String> variables = new HashMap<>(); final List<String> actualTypeParameters = getTypeParameters(type); final Class<?> loadedClass = loadClassFromType(type); if (loadedClass == null) { LogProvider.debug("could not load class for type " + type); return emptyMap(); } final TypeVariable<? extends Class<?>>[] typeParameters = loadedClass.getTypeParameters(); for (int i = 0; i < actualTypeParameters.size(); i++) { variables.put(typeParameters[i].getName(), actualTypeParameters.get(i)); } return variables; } public static Class<?> loadClassFromName(final String className) { switch (className) { case CLASS_PRIMITIVE_VOID: return int.class; case CLASS_PRIMITIVE_BOOLEAN: return boolean.class; case CLASS_PRIMITIVE_CHAR: return char.class; case CLASS_PRIMITIVE_BYTE: return byte.class; case CLASS_PRIMITIVE_SHORT: return short.class; case CLASS_PRIMITIVE_INT: return int.class; case CLASS_PRIMITIVE_FLOAT: return float.class; case CLASS_PRIMITIVE_LONG: return long.class; case CLASS_PRIMITIVE_DOUBLE: return double.class; } // TODO test for variable types ClassLoader classLoader = ContextClassReader.getClassLoader(); try { return classLoader.loadClass(className.replace('/', '.')); } catch (ClassNotFoundException e) { LogProvider.error("Could not load class " + className); LogProvider.debug(e); return null; } } public static Class<?> loadClassFromType(final String type) { return loadClassFromName(toClassName(type)); } public static Method findMethod(final String className, final String methodName, final String signature) { final Class<?> loadedClass = loadClassFromName(className); if (loadedClass == null) return null; return findMethod(loadedClass, methodName, signature); } public static Method findMethod(final Class<?> loadedClass, final String methodName, final String signature) { final List<String> parameters = getParameters(signature); return Stream.of(loadedClass.getDeclaredMethods()).filter(m -> m.getName().equals(methodName) && m.getParameterCount() == parameters.size() // return types are not taken into account (could be overloaded method w/ different return type) && Objects.equals(getParameters(getMethodSignature(m)), parameters) ).findAny().orElse(null); } public static String getMethodSignature(final String returnType, final String... parameterTypes) { final String parameters = Stream.of(parameterTypes).collect(Collectors.joining()); return '(' + parameters + ')' + returnType; } public static String getMethodSignature(final Method method) { try { final Field signatureField = method.getClass().getDeclaredField("signature"); signatureField.setAccessible(true); final String signature = (String) signatureField.get(method); if (signature != null) return signature; return Type.getMethodDescriptor(method); } catch (ReflectiveOperationException e) { LogProvider.error("Could not access method " + method); LogProvider.debug(e); return null; } } public static String getFieldDescriptor(final Field field, final String containedType) { try { final Field signatureField = field.getClass().getDeclaredField("signature"); signatureField.setAccessible(true); String signature = (String) signatureField.get(field); if (signature != null) { return resolvePotentialTypeVariables(signature, containedType); } return Type.getDescriptor(field.getType()); } catch (ReflectiveOperationException e) { LogProvider.error("Could not access field " + field); LogProvider.debug(e); return null; } } private static String resolvePotentialTypeVariables(final String signature, final String containedType) { // resolve type variables immediately if (signature.charAt(0) == 'T' || signature.contains("<T") || signature.contains(";T") || signature.contains(")T")) { // TODO test final Map<String, String> typeVariables = getTypeVariables(containedType); StringBuilder builder = new StringBuilder(signature); boolean startType = true; for (int i = 0; i < builder.length(); i++) { if (startType && builder.charAt(i) == 'T') { final int end = builder.indexOf(";", i); final String identifier = builder.substring(i + 1, end); final String resolvedVariableType = typeVariables.getOrDefault(identifier, OBJECT); builder.replace(i, end + 1, resolvedVariableType); i = end; continue; } startType = builder.charAt(i) == '<' || builder.charAt(i) == ';'; } return builder.toString(); } return signature; } /** * Returns the parameter types of the given method signature. Parametrized types are supported. */ public static List<String> getParameters(final String methodDesc) { // final String[] types = resolveMethodSignature(methodDesc); // return IntStream.range(0, types.length).mapToObj(i -> types[i]).collect(Collectors.toList()); if (methodDesc == null) return emptyList(); final char[] buffer = methodDesc.toCharArray(); final List<String> args = new ArrayList<>(); // TODO resolve type parameters correctly -> information useful? -> maybe use ASM's SignatureReader/Visitor int offset = methodDesc.indexOf('(') + 1; while (buffer[offset] != ')') { final String type = getNextType(buffer, offset); args.add(type); offset += type.length();// + (type.charAt(0) == 'L' ? 2 : 0); } // TODO change, see type parameters // prevent type parameter identifiers final ListIterator<String> iterator = args.listIterator(); while (iterator.hasNext()) { final String arg = iterator.next(); if (arg.charAt(0) == 'T') iterator.set(OBJECT); } return args; } /** * Resolves the given method signatures to an array of (self-contained) Java type descriptions. * * @param methodDesc The method description signature (can contain type parameters and generics) * @return The types as an array with the method parameter types first and the return type as index {@code array.length - 1} */ private static String[] resolveMethodSignature(final String methodDesc) { // if starts with '<' -> resolve type parameters // final Map<String, String> typeParameters = null; // if (methodDesc.charAt(0) == '<') { // typeParameters = resolveTypeParameters(methodDesc); // } return null; } private static Map<String, String> resolveTypeParameters(final String methodDesc) { // boolean identifierMode = true; // int identifierStart = 1; // String currentIdentifier = null; // // for (int i = 1; methodDesc.charAt(i) != '>'; i++) { // switch (methodDesc.charAt(i)) { // case ':': // if (identifierMode) { // identifierMode = false; // currentIdentifier = methodDesc.substring(identifierStart, i); // } else { // // } // } // } return null; } private static String getNextType(final char[] buf, final int off) { switch (buf[off]) { case 'V': case 'Z': case 'C': case 'B': case 'S': case 'I': case 'F': case 'J': case 'D': return String.valueOf(buf[off]); case '[': int len = 1; while (buf[off + len] == '[') { len++; } return getNextType(buf, off, len); case 'L': // TODO resolve type variables case 'T': return getNextType(buf, off, 0); default: throw new IllegalArgumentException("Illegal signature provided: " + new String(buf)); } } private static String getNextType(char[] buf, int off, int len) { int depth = 0; if (buf[off + len] == 'L' || buf[off + len] == 'T') while (buf[off + len] != ';' || depth != 0) { if (buf[off + len] == '<') depth++; else if (buf[off + len] == '>') depth--; len++; } return new String(buf, off, len + 1); } }