/* * #%L * Nazgul Project: nazgul-core-algorithms-api * %% * Copyright (C) 2010 - 2017 jGuru Europe AB * %% * Licensed under the jGuru Europe AB license (the "License"), based * on Apache License, Version 2.0; you may not use this file except * in compliance with the License. * * You may obtain a copy of the License at * * http://www.jguru.se/licenses/jguruCorporateSourceLicense-2.0.txt * * 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. * #L% * */ package se.jguru.nazgul.core.algorithms.api; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import se.jguru.nazgul.core.algorithms.api.types.TypeInformation; import javax.validation.constraints.NotNull; import javax.xml.bind.annotation.XmlTransient; import java.beans.BeanInfo; import java.beans.Introspector; import java.lang.annotation.Annotation; import java.lang.reflect.Constructor; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.SortedMap; import java.util.SortedSet; import java.util.StringTokenizer; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collector; import java.util.stream.Collectors; /** * Class- and Interface related algorithms. * * @author <a href="mailto:lj@jguru.se">Lennart Jörelid</a>, jGuru Europe AB */ @XmlTransient public final class TypeAlgorithms { /** * Our logger. */ public static final Logger log = LoggerFactory.getLogger(TypeAlgorithms.class); /** * Default property delimiter, used to construct property path expressions. * Typically an expression on the form {@code anObject.foo.bar.baz} implies that a set of JavaBean * getters should be invoked on the {@code anObject} object. * * @see #FIND_JAVABEAN_GETTERS * @see #getProperty(Object, String) */ public static final String PROPERTY_DELIMITER = "."; /** * Compares two classes by comparing their fully qualified ClassNames. * Any null argument is replaced with an empty string before comparing. */ public static final Comparator<Class<?>> CLASSNAME_COMPARATOR = Comparator.comparing(aClass -> (aClass == null ? "" : aClass.getName())); /** * Compares two Annotations by comparing the fully qualified ClassNames of their annotation types. * Any null argument is replaced with an empty string before comparing. * * @see Annotation#annotationType() */ public static final Comparator<Annotation> ANNOTATION_COMPARATOR = Comparator.comparing(anAnnotation -> (anAnnotation == null ? "" : anAnnotation.annotationType().getName())); /** * Compares two Members by comparing their respective Declaring Class + toString value. * Any null argument is replaced with an empty string before comparing. */ public static final Comparator<Member> MEMBER_COMPARATOR = (l, r) -> { final String leftSortKey = l == null ? "" : l.getDeclaringClass().getName() + l.toString(); final String rightSortKey = r == null ? "" : r.getDeclaringClass().getName() + r.toString(); // All Done. return leftSortKey.compareTo(rightSortKey); }; /** * <p>Standard Supplied to create a SortedSet of Class'es using the {@link TypeAlgorithms#CLASSNAME_COMPARATOR} * to determine order within the SortedSet yielded. The typical application of this Supplier is</p> * <pre> * <code> * [someStream].collect(Collectors.toCollection(TypeAlgorithms.SORTED_CLASS_SUPPLIER)); * </code> * </pre> */ public static final Supplier<SortedSet<Class<?>>> SORTED_CLASS_SUPPLIER = () -> new TreeSet<>(TypeAlgorithms.CLASSNAME_COMPARATOR); /** * <p>Standard Collector to create a SortedSet of Class'es using the {@link TypeAlgorithms#SORTED_CLASS_SUPPLIER} * supplied. Typically used as follows:</p> * <pre> * <code> * [someStream].collect(TypeAlgorithms.SORTED_CLASSNAME_COLLECTOR)); * </code> * </pre> */ public static final Collector<Class<?>, ?, SortedSet<Class<?>>> SORTED_CLASSNAME_COLLECTOR = Collectors.toCollection(TypeAlgorithms.SORTED_CLASS_SUPPLIER); /** * <p>Standard Collector to create a SortedSet of Constructors using the * {@link TypeAlgorithms#SORTED_CONSTRUCTOR_SUPPLIER}. Typically used as follows:</p> * <pre> * <code> * [someStream].collect(TypeAlgorithms.SORTED_CONSTRUCTOR_COLLECTOR)); * </code> * </pre> */ public static final Collector<Constructor<?>, ?, SortedSet<Constructor<?>>> SORTED_CONSTRUCTOR_COLLECTOR = Collectors.toCollection(TypeAlgorithms.SORTED_CONSTRUCTOR_SUPPLIER); /** * <p>Standard Supplied to create a SortedSet of Members using the {@link TypeAlgorithms#MEMBER_COMPARATOR} * to determine order within the SortedSet yielded. The typical application of this Supplier is</p> * <pre> * <code> * [someStream].collect(Collectors.toCollection(() -> new TreeSet<>(TypeAlgorithms.MEMBER_COMPARATOR)); * </code> * </pre> */ public static final Supplier<SortedSet<Member>> SORTED_MEMBER_SUPPLIER = () -> new TreeSet<>(TypeAlgorithms.MEMBER_COMPARATOR); /** * <p>Standard Supplied to create a SortedSet of Method using the {@link TypeAlgorithms#MEMBER_COMPARATOR} * to determine order within the SortedSet yielded. The typical application of this Supplier is</p> * <pre> * <code> * [someStream].collect(Collectors.toCollection(TypeAlgorithms.SORTED_METHOD_SUPPLIER)); * </code> * </pre> */ public static final Supplier<SortedSet<Method>> SORTED_METHOD_SUPPLIER = () -> new TreeSet<>(TypeAlgorithms.MEMBER_COMPARATOR); /** * <p>Standard Supplied to create a SortedSet of Constructor using the {@link TypeAlgorithms#MEMBER_COMPARATOR} * to determine order within the SortedSet yielded. The typical application of this Supplier is</p> * <pre> * <code> * [someStream].collect(Collectors.toCollection(TypeAlgorithms.SORTED_CONSTRUCTOR_SUPPLIER)); * </code> * </pre> */ public static final Supplier<SortedSet<Constructor<?>>> SORTED_CONSTRUCTOR_SUPPLIER = () -> new TreeSet<>(TypeAlgorithms.MEMBER_COMPARATOR); /** * Function returning all public methods (including those inherited from superclasses) within a given class. * The sort order of the returned SortedSet is given by {@link #MEMBER_COMPARATOR}. */ public static final Function<Class<?>, SortedSet<Method>> FIND_PUBLIC_METHODS = c -> { final SortedSet<Method> toReturn = new TreeSet<>(MEMBER_COMPARATOR); toReturn.addAll(Arrays.asList(c.getMethods())); // All Done. return toReturn; }; /** * Function retrieving a SortedSet relating readable JavaBean property (names) to * their corresponding getter Methods. The JavaBean getter methods within the Object class * (i.e. "getClass()" will be ignored). */ public static final Function<Class<?>, SortedMap<String, Method>> FIND_JAVABEAN_GETTERS = c -> { final SortedMap<String, Method> name2GetterMap = new TreeMap<>(); try { // Find the BeanInfo of the current class. final BeanInfo beanInfo = Introspector.getBeanInfo(c, Object.class); // Find the method corresponding to each JavaBean property Arrays.stream(beanInfo.getPropertyDescriptors()) .forEach(propertyDescriptor -> { final String name = propertyDescriptor.getName(); final Method getter = propertyDescriptor.getReadMethod(); if (getter != null && name != null) { // Should we overwrite the current name2GetterMap value? boolean writeProperty; final Method existingGetterMethod = name2GetterMap.get(name); if (existingGetterMethod != null) { final Class<?> currentDeclaringClass = existingGetterMethod.getDeclaringClass(); final Class<?> candidateDeclaringClass = getter.getDeclaringClass(); writeProperty = currentDeclaringClass.isAssignableFrom(candidateDeclaringClass); } else { writeProperty = true; } // (Over)write the property? if (writeProperty) { name2GetterMap.put(name, getter); } } }); } catch (Exception e) { // Complain throw new IllegalArgumentException("Could not extract BeanInfo/JavaBeanGetters for class " + c.getName(), e); } // All Done. return name2GetterMap; }; /** * Function retrieving a SortedSet relating writable JavaBean property (names) to * their corresponding setter Methods. */ public static final Function<Class<?>, SortedMap<String, Method>> FIND_JAVABEAN_SETTERS = c -> { final SortedMap<String, Method> name2SetterMap = new TreeMap<>(); try { // Find the BeanInfo of the current class. final BeanInfo beanInfo = Introspector.getBeanInfo(c); // Find the method corresponding to each JavaBean property Arrays.stream(beanInfo.getPropertyDescriptors()) .forEach(propertyDescriptor -> { final String name = propertyDescriptor.getName(); final Method setter = propertyDescriptor.getWriteMethod(); if (setter != null && name != null && !name2SetterMap.containsKey(name)) { name2SetterMap.put(name, setter); } }); } catch (Exception e) { // Complain throw new IllegalArgumentException("Could not extract BeanInfo/JavaBeanSetters for class " + c.getName(), e); } // All Done. return name2SetterMap; }; /* * Hide the constructor for utility classes. */ private TypeAlgorithms() { // Do nothing } /** * Collects a Set containing all types held by the supplied aClass (including its supertypes). * * @param aClass The class to inspect, and retrieve all types for. * @return A TypeInformation object with data extracted for the supplied Class. */ @NotNull public static TypeInformation getAllTypesFor(@NotNull final Class<?> aClass) { // Check sanity Validate.notNull(aClass, "aClass"); // All Done return new TypeInformation(aClass); } /** * <p>Retrieves a property from the supplied pointOfOrigin Object using the supplied propertyExpression as the * definition of a property path. The property path should be on the form {@code foo.bar.baz} providing the * JavaBean property names ("foo", "bar" and "baz"). Such JavaBean property names do - in turn - imply methods * with the signature {@code getFoo(), getBar()} and {@code getBaz()} respectively.</p> * <p>The code above should yield an implementation chain similar to the following:</p> * <pre> * <code> * // The call: * final SomeType result = TypeAlgorithms.getProperty(pointOfOrigin, "foo.bar.baz"); * * // ... is equal to calling the JavaBean getter methods in the * // order given within the propertyExpression: * final SomeType result = pointOfOrigin.getFoo().getBar().getBaz(); * </code> * </pre> * * @param pointOfOrigin The object in which we should start the JavaBean invocation chain. * @param propertyExpression The property expression defining a chained * @return The result of the * @throws IllegalArgumentException if a JavaBean getter method is not present within the current/intermediary * type in which it was requested to be invoked. */ public static Object getProperty(@NotNull final Object pointOfOrigin, @NotNull final String propertyExpression) throws IllegalArgumentException { // #1) Check sanity Validate.notNull(pointOfOrigin, "pointOfOrigin"); Validate.notEmpty(propertyExpression, "propertyExpression"); // #2) Splice the propertyExpression into tokens, and define the intermediary state final List<String> tokens = splicePropertyExpression(propertyExpression); if (log.isDebugEnabled()) { log.debug("Split propertyExpression [" + propertyExpression + "] into " + tokens.size() + " tokens: " + tokens); } // #3) Execute the JavaBean invocation chain. Object currentResult = null; final AtomicInteger index = new AtomicInteger(); for (Object current = pointOfOrigin; index.get() < tokens.size(); current = currentResult, index.incrementAndGet()) { // #1) Get the current class in which to invoke a JavaBean getter. final Class<?> currentClass = current.getClass(); // #2) Find the current JavaBean property final String currentPropertyName = tokens.get(index.get()); // #3) Find the JavaBean getters for this Class, and - more specifically - the Method // corresponding to the currentPropertyName. final TypeInformation currentTypeInformation = new TypeInformation(currentClass); final SortedMap<String, Method> currentGetters = currentTypeInformation.getJavaBeanGetterMethods(); final Method getter = currentGetters.get(currentPropertyName); if (getter == null) { throw new IllegalArgumentException("Nonexistent expected JavaBean getter for property [" + currentPropertyName + "] within class [" + currentClass.getName() + "]. Found JavaBean properties: " + currentGetters.keySet().stream().reduce((l, r) -> l + ", " + r).orElse("<none>")); } // #4) Invoke the getter, update the result. try { currentResult = getter.invoke(current); } catch (Exception e) { throw new IllegalArgumentException("JavaBean property [" + currentPropertyName + "] getter method call failed in class [" + currentClass.getName() + "]", e); } } // All done. return currentResult; } // // Private helpers // private static List<String> splicePropertyExpression(@NotNull final String propertyExpression) { final List<String> toReturn = new ArrayList<>(); final StringTokenizer tok = new StringTokenizer(propertyExpression, PROPERTY_DELIMITER, false); while (tok.hasMoreTokens()) { final String candidate = tok.nextToken().trim(); // Check sanity, at least trivially. if (candidate.contains(" ")) { throw new IllegalArgumentException("PropertyExpressions cannot contain internal whitespace. " + "(Found token '" + candidate + "' within propertyExpression [" + propertyExpression + "]"); } // Add the candidate toReturn.add(candidate); } // All Done. return toReturn; } }