All Downloads are FREE. Search and download functionalities are using the official Maven repository.

javax0.jamal.snippet.tools.ReflectionTools Maven / Gradle / Ivy

The newest version!
package javax0.jamal.snippet.tools;

import javax0.refi.selector.Selector;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
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.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static java.lang.reflect.Modifier.isPrivate;
import static java.lang.reflect.Modifier.isProtected;
import static java.lang.reflect.Modifier.isPublic;

public class ReflectionTools {

    public static final int PACKAGE = 0x00010000;
    private static final Selector inheritedField = Selector.compile("!static & !private");
    private static final Selector inheritedFieldDifferentPackage = Selector.compile("!static & !private & !package");
    private static final Map> PRIMITIVES = Map.of(
            "byte", byte.class,
            "char", char.class,
            "short", short.class,
            "int", int.class,
            "long", long.class,
            "float", float.class,
            "double", double.class,
            "boolean", boolean.class);

    /**
     * Get the modifiers as string.
     *
     * @param member for which the modifiers are needed
     * @return the string containing the modifiers, space separated
     */
    public static String modifiersString(Member member) {
        return new ModifiersBuilder(member.getModifiers()).toString();
    }

    /**
     * Get the modifiers as string except abstract.
     *
     * @param member for which the modifiers are needed
     * @return the string containing the modifiers, space separated
     */
    public static String modifiersStringConcrete(Member member) {
        return new ModifiersBuilder(member.getModifiers() & ~Modifier.ABSTRACT).toString();
    }

    /**
     * Get the modifiers as string except access modifier.
     *
     * @param member for which the modifiers are needed
     * @return the string containing the modifiers space separated, except the access modifier
     */
    public static String modifiersStringNoAccess(Member member) {
        return new ModifiersBuilder(member.getModifiers()
                & ~Modifier.PROTECTED & ~Modifier.PRIVATE & ~Modifier.PUBLIC).toString();
    }

    /**
     * Get the modifiers as string except access modifier and without the possible abstract modifier.
     *
     * @param member for which the modifiers are needed
     * @return the string containing the modifiers space separated, except the access modifier
     */
    public static String modifiersStringNoAccessConcrete(Member member) {
        return new ModifiersBuilder(member.getModifiers()
                & ~Modifier.PROTECTED & ~Modifier.PRIVATE & ~Modifier.PUBLIC & ~Modifier.ABSTRACT).toString();
    }

    /**
     * Get a field's or a method's type as string.
     *
     * @param member of which the type is needed
     * @return string containing the type as string with all the generics.
     */
    public static String typeAsString(Member member) {
        return getGenericTypeName(member instanceof Field ?
                ((Field) member).getGenericType()
                :
                ((Method) member).getGenericReturnType());
    }

    /**
     * Normalize a generic type name removing all {@code java.lang.} from the type names.
     * 

* Even the generated code should be human-readable, especially when you debug the working of the code. In that case * the generic names with all the {@code java.lang.String}, {@code java.lang.Integer} and so on are disturbing. This * method removes those prefixes. *

* Note that the prefixes {@code java.util.} and similar others that are usually imported by the class are NOT * removed, because we cannot know that the class imports those or not. *

* Since there is no API in JDK to get the canonical name of a {@link Type} the inner classes before normalization * will contain {@code $} in the names and not dot. As a last resort here we replace all {@code $} character in the * name to {@code .} (dot). This has the consequence that the application that uses fluent API generator must not * use {@code $} in the name of the classes. * * @param s generic type name to be normalized * @return the normalized type name */ public static String normalizeTypeName(String s) { s = s.replaceAll("\\s*<\\s*", "<") .replaceAll("\\s*>\\s*", ">") .replaceAll("\\s*\\.\\s*", ".") .replaceAll("\\s*,\\s*", ",") .replaceAll("\\s+", " "); if (s.startsWith("java.lang.")) { s = s.substring("java.lang.".length()); } s = s.replaceAll("([^\\w\\d.^])java.lang.", "$1"); s = s.replaceAll("\\$", "."); return s; } /** * Normalize a type assuming that the type will be used inside the class {@code klass}. First this method does all * normalizations that are performed by {@link #normalizeTypeName(String)} and then it checks if the type is in the * same package as the given class * * @param s generic type name to be normalized * @param klass the class where the type name will be used * @return the normalized type name */ public static String normalizeTypeName(String s, Class klass) { s = normalizeTypeName(s); if (s.startsWith(klass.getPackageName() + ".")) { s = s.substring(klass.getPackageName().length() + 1); } return s; } private static String removeJavaLang(String s) { if (s.startsWith("java.lang.") && !s.substring("java.lang.".length()).contains(".")) { return s.substring("java.lang.".length()); } else { return s; } } /** * Get the generic type name of the type passed as argument. The JDK {@code Type#getTypeName()} returns a string * that contains the classes with their names and not with the canonical names (inner classes have {@code $} in the * names instead of dot). This method goes through the type structure and converts the names (generic types also) * to * * @param t the type * @return the type as string */ public static String getGenericTypeName(Type t) { final String normalizedName; if (t instanceof ParameterizedType) { normalizedName = getGenericParametrizedTypeName((ParameterizedType) t); } else if (t instanceof Class) { normalizedName = removeJavaLang(((Class) t).getCanonicalName()); } else if (t instanceof WildcardType) { normalizedName = getGenericWildcardTypeName((WildcardType) t); } else if (t instanceof GenericArrayType) { var at = (GenericArrayType) t; normalizedName = getGenericTypeName(at.getGenericComponentType()) + "[]"; } else if (t instanceof TypeVariable) { normalizedName = t.getTypeName(); } else { throw new RuntimeException(String.format( "Type is something not handled. It is '%s' for the type '%s'", t.getClass(), t.getTypeName())); } return normalizedName; } /** * Get the simple class name with the generic parameters as they are defined in the declaration of the class. Since * this can only be used inside the class there is no need for the canonical name when a code generator needs this * string (see {@code ChainedAccessor}). *

* Only the simple name is returned even if the class is an inner class. For example: * *

{@code
     *         java.util.Map.Entry.class -> Entry
     * }
*

* If you need the surrounding classes in the name then call {@link #getLocalGenericClassName(Class)}. * * @param t the class we need the name for * @return the name of the class with the generic declarations in case there are generic parameters of the class. */ public static String getSimpleGenericClassName(Class t) { return t.getSimpleName() + getGenericParametersString(t); } /** * Get the local class name with the generic parameters as they are defined in the declaration of the class. This * method is almost the same as {@link #getSimpleGenericClassName(Class)}. The only difference is when {@code t} is * an inner class. In this case this method will return the class name that contains the names of the encapsulating * classes. For example this method will * *

{@code
     *         java.util.Map.Entry.class -> Map.Entry
     * }
*

* return the class name {@code Map} as part of the name. * * @param t the class we need the name for * @return the name of the class with the generic declarations in case there are generic parameters of the class. */ public static String getLocalGenericClassName(Class t) { return normalizeTypeName(t.getCanonicalName() .substring(t.getPackageName().length() + 1)) + getGenericParametersString(t); } private static String getGenericParametersString(Class t) { final var generics = Arrays.stream(t.getTypeParameters()) .map(ReflectionTools::getGenericTypeName) .collect(Collectors.joining(",")); if (generics.length() == 0) { return ""; } else { return "<" + generics + ">"; } } private static String getGenericWildcardTypeName(WildcardType t) { String normalizedName; var ub = joinTypes(t.getUpperBounds()); var lb = joinTypes(t.getLowerBounds()); normalizedName = "?" + (lb.length() > 0 && !lb.equals("Object") ? " super " + lb : "") + (ub.length() > 0 && !ub.equals("Object") ? " extends " + ub : ""); return normalizedName; } private static String getGenericParametrizedTypeName(ParameterizedType t) { String normalizedName; var types = t.getActualTypeArguments(); if (!(t.getRawType() instanceof Class)) { throw new RuntimeException(String.format("'getRawType()' returned something that is not a class : %s", t.getClass().getTypeName())); } final var klass = (Class) t.getRawType(); final String klassName = removeJavaLang(klass.getCanonicalName()); if (types.length > 0) { normalizedName = klassName + "<" + joinTypes(types) + ">"; } else { normalizedName = klassName; } return normalizedName; } /** * Join the type names also removing the {@code java.lang. } prefixes if any. * * @param types the types to join. These are the generic types, or the super or extends types in a wildcard. * @return the string of the list comma separated. */ private static String joinTypes(Type[] types) { return Arrays.stream(types) .map(ReflectionTools::getGenericTypeName) .collect(Collectors.joining(",")); } /** * Get the declared fields of the class sorted alphabetically. The actual order is usually not interesting for the * code generators, but a deterministic order is. When a code generator generates code for all or for some of the * declared fields it is important that the order is always the same. If the order changes from time to time then it * may happen that the code generation creates the code every time differently and breaking the build. It happens in * practice, for example, when you have a different version of Java on the development machine and on the build * server. In development you run the build, generate the code, run the build again, commit the code. You expect * that on the build server the code generation will not fail because all the generated code is there in the * repository in the files. However, you may use a different version of Java (even if only different build) on the * build server and because of that the order of the fields is different and the generated code is different, * although functionally it is the same. *

* This method and also the {@link #getDeclaredMethodsSorted(Class)} can and should be used by code generators to * have a deterministic output. * * @param klass of which the fields are collected * @return the sorted array of fields */ public static Field[] getDeclaredFieldsSorted(Class klass) { final var fields = klass.getDeclaredFields(); Arrays.sort(fields, Comparator.comparing(Field::getName)); return fields; } /** * Get all the fields, declared and inherited fields sorted. About the sorting see the JavaDoc of {@link * #getDeclaredFieldsSorted(Class)}. * * @param klass of which the fields are collected * @return the sorted array of fields */ public static Field[] getAllFieldsSorted(Class klass) { Set allFields = new HashSet<>(Arrays.asList(klass.getDeclaredFields())); var samePackage = true; for (var currentClass = klass.getSuperclass(); currentClass != null; currentClass = currentClass.getSuperclass()) { samePackage = samePackage && klass.getPackage() == currentClass.getPackage(); collectFields(samePackage, currentClass, allFields); } final var fieldsArray = allFields.toArray(new Field[0]); Arrays.sort(fieldsArray, Comparator.comparing(Field::getName)); return fieldsArray; } /** * Collect all the fields from the actual class that are inherited by the base class assuming that the base class * extends directly or through other classes transitively the actual class. * * @param isSamePackage a boolean flag for deciding whether add package-private fields. * @param actualClass the class in which we look for the fields * @param fields the collection of the fields where to put the fields */ private static void collectFields(boolean isSamePackage, Class actualClass, Set fields) { final var declaredFields = actualClass.getDeclaredFields(); final var selector = isSamePackage ? inheritedField : inheritedFieldDifferentPackage; Arrays.stream(declaredFields) .filter(selector::match) .forEach(fields::add); } /** * Get the declared methods of the class sorted. *

* See the notes at the javadoc of the method {@link #getDeclaredFieldsSorted(Class)} *

* The methods are sorted according to the string representation of the signature. How the method signature is * created is document in the javadoc of the method {@link MethodTool#methodSignature(Method)} * * @param klass class of which the methods are returned * @return the sorted array of the methods */ public static Method[] getDeclaredMethodsSorted(Class klass) { final var methods = klass.getDeclaredMethods(); Arrays.sort(methods, Comparator.comparing(MethodTool::methodSignature)); return methods; } /** * The same as {@link #getDeclaredMethodsSorted(Class)} except it returns the methods and not the declared methods. * It means that only the methods that are available from outside but including the inherited methods are returned. * * @param klass the class of which we need the methods * @return the array of the methods of the class */ public static Method[] getMethodsSorted(Class klass) { final var methods = klass.getMethods(); Arrays.sort(methods, Comparator.comparing(MethodTool::methodSignature)); return methods; } /** * Get all the methods of the class sorted. This includes all the methods that are declared in the class and also * all the inherited methods, even the protected or package private methods. Note that package private methods are * only inherited if the parent class is in the same package as the inheriting class and it is not possible to * inherit via an intermediate package that is in a different package. * * @param klass the class of which we need the methods * @return the array of the methods of the class */ public static Method[] getAllMethodsSorted(final Class klass) { final var allMethods = new ArrayList<>(Arrays.asList(klass.getDeclaredMethods())); var samePackage = true; for (var currentClass = klass.getSuperclass(); currentClass != null; currentClass = currentClass.getSuperclass()) { samePackage = samePackage && klass.getPackage() == currentClass.getPackage(); collectMethods(samePackage, currentClass, allMethods); } final Method[] methodArray = allMethods.toArray(new Method[0]); Arrays.sort(methodArray, Comparator.comparing(MethodTool::methodSignature)); return methodArray; } private static void collectMethods(boolean samePackage, Class currentClass, ArrayList allMethods) { Arrays.stream(currentClass.getDeclaredMethods()) .filter(method -> isVisible(method, samePackage)) .filter(method -> isNotOverridden(method, allMethods)) .forEach(allMethods::add); } private static boolean isNotOverridden(Method currentMethod, ArrayList allMethods) { return allMethods.stream() .filter(method -> method.getName().equals(currentMethod.getName())) .noneMatch(method -> Arrays.deepEquals(method.getParameterTypes(), currentMethod.getParameterTypes())); } private static boolean isVisible(Method method, boolean samePackage) { final var modifier = method.getModifiers(); return isProtected(modifier) || isPublic(modifier) || (samePackage && !isPublic(modifier) && !isProtected(modifier) && !isPrivate(modifier)); } /** * Get all the member classes sorted either declared in the class or inherited. * * @param klass that we want the inner and nested classes * @return the array of {@code Class} objects representing the public members of this class in a sorted order. The * soring order is not guaranteed. Sorting only guarantees that the returned array contains the classes in the same * order even if the code runs on different JVMs. */ public static Class[] getAllClassesSorted(Class klass) { final var classes = Arrays.stream(klass.getClasses()).collect(Collectors.toSet()); final var declaredClasses = Arrays.stream(klass.getDeclaredClasses()).collect(Collectors.toSet()); final var allClasses = new HashSet>(); allClasses.addAll(classes); allClasses.addAll(declaredClasses); final Class[] classArray = allClasses.toArray(new Class[0]); Arrays.sort(classArray, Comparator.comparing(Class::getName)); return classArray; } /** * Get the declared classes of the class sorted. * *

See the notes at the javadoc of the method {@link * #getDeclaredFieldsSorted(Class)} * * @param klass class of which the member classes are returned * @return the sorted array of the classes */ public static Class[] getDeclaredClassesSorted(Class klass) { final var classes = klass.getDeclaredClasses(); Arrays.sort(classes, Comparator.comparing(Class::getName)); return classes; } /** * The same as {@link #getDeclaredClassesSorted(Class)}} except it returns the classes and not the declared classes. * It means that only the classes that are available from outside but including the inherited classes are returned. * * @param klass the class of which we need the classes * @return the array of the classes of the class */ public static Class[] getClassesSorted(Class klass) { final var classes = klass.getClasses(); Arrays.sort(classes, Comparator.comparing(Class::getName)); return classes; } public static Method getMethod(Class klass, String methodName, Class... classes) throws NoSuchMethodException { return Stream.of(getAllMethodsSorted(klass)) .filter(method -> method.getName().equals(methodName) && Arrays.deepEquals(method.getParameterTypes(), classes)).findAny() .orElseThrow(() -> new NoSuchMethodException("No method " + methodName + " was found in " + klass.getName())); } public static Field getField(Class klass, String fieldName) throws NoSuchFieldException { return Stream.of(getAllFieldsSorted(klass)) .filter(field -> field.getName().equals(fieldName)).findAny() .orElseThrow(() -> new NoSuchFieldException("No field " + fieldName + " was found in " + klass.getName())); } private static Class classForNoArray(String className) throws ClassNotFoundException { if (PRIMITIVES.containsKey(className)) { return PRIMITIVES.get(className); } try { return Class.forName(className); } catch (ClassNotFoundException ignored) { return Class.forName("java.lang." + className); } } /** * Get the class that is represented by the name {@code className}. This functionality extends the basic * functionality provided by the static method {@link Class#forName(String)} so that it also works for for input * strings {@code int}, {@code byte} and so on for all the eight primitive types and also it works for types that * end with {@code []}, so when they are essentially arrays. This also works for primitives. *

* If the class cannot be found in the first round then this method tries it again prepending the {@code java.lang.} * in front of the name given as argument, so Java language types can be referenced as, for example {@code Integer} * and they do not need the fully qualified name. *

* Note that there are many everyday used types, like {@code Map}, which are NOT in the {@code java.lang} package. * They have to be specified with the fully qualified name. * * @param className the name of the class or a primitive type optionally one or more {@code []} pairs at the end. * The JVM limitation is that there can be at most 255 {@code []} pairs. * @return the class * @throws ClassNotFoundException if the class cannot be found */ public static Class classForName(String className) throws ClassNotFoundException { var arrayCounter = 0; while (className.endsWith("[]")) { className = className.substring(0, className.length() - 2); arrayCounter++; } var klass = classForNoArray(className); while (arrayCounter-- > 0) { klass = Array.newInstance(klass, 0).getClass(); } return klass; } /** * Convert a string that contains lower case letter Java modifiers comma separated into an access mask. * * @param masks is the comma separated list of modifiers. The list can also contain the word {@code package} that * will be translated to {@link ReflectionTools#PACKAGE} since there is no modifier {@code * package} in Java. * @param dfault the mask to return in case the {@code includes} string is empty. * @return the mask converted from String */ public static int mask(String masks, int dfault) { int modMask = 0; if (masks == null) { return dfault; } else { for (var maskString : masks.split(",", -1)) { var maskTrimmed = maskString.trim(); switch (maskTrimmed) { case "private": modMask |= Modifier.PRIVATE; break; case "public": modMask |= Modifier.PUBLIC; break; case "protected": modMask |= Modifier.PROTECTED; break; case "static": modMask |= Modifier.STATIC; break; case "package": modMask |= ReflectionTools.PACKAGE;//reuse the bit break; case "abstract": modMask |= Modifier.ABSTRACT; break; case "final": modMask |= Modifier.FINAL; break; case "interface": modMask |= Modifier.INTERFACE; break; case "synchronized": modMask |= Modifier.SYNCHRONIZED; break; case "native": modMask |= Modifier.NATIVE; break; case "transient": modMask |= Modifier.TRANSIENT; break; case "volatile": modMask |= Modifier.VOLATILE; break; default: throw new IllegalArgumentException(maskTrimmed + " can not be used as a modifier string"); } } return modMask; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy