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

com.blackbuild.klum.ast.util.layer3.ClusterModel Maven / Gradle / Ivy

There is a newer version: 2.0.0-rc.39
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2015-2024 Stephan Pauxberger
 *
 * 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 com.blackbuild.klum.ast.util.layer3;

import groovy.lang.*;
import groovy.transform.stc.ClosureParams;
import groovy.transform.stc.SimpleType;
import org.codehaus.groovy.reflection.CachedField;
import org.codehaus.groovy.reflection.CachedMethod;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.jetbrains.annotations.NotNull;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.*;
import java.util.function.Predicate;
import java.util.stream.Stream;

import static java.lang.String.format;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;
import static org.codehaus.groovy.runtime.DefaultGroovyMethods.getMetaClass;
import static org.codehaus.groovy.runtime.DefaultGroovyMethods.getMetaPropertyValues;

/**
 * Helper class that can be used for (not only) cluster models. A cluster model provides access to
 */
public class ClusterModel {

    private ClusterModel() {}

    /**
     * Returns a map of all fields of the given container with the given type, using the given filter.
     * This includes null values.
     * @param container The object whose fields should be returned
     * @param fieldType The type of properties to be returned
     * @param filter Filter closure on the AnnotatedElement (Method or Field)
     * @return A Map of property names to property values containing all properties of the given type.
     */
    public static  Map getPropertiesOfType(Object container, Class fieldType, @ClosureParams(value = SimpleType.class, options = "java.lang.reflect.AnnotatedElement") Closure filter) {
        return getPropertiesOfType(container, fieldType, filter::call);
    }

    /**
     * Returns a map of all fields of the given container with the given type
     * This includes null values.
     * @param container The object whose fields should be returned
     * @param fieldType The type of properties to be returned
     * @return A Map of property names to property values containing all properties of the given type.
     */
    @SuppressWarnings("unchecked") // PropertyValue is not generic
    public static  Map getPropertiesOfType(Object container, Class fieldType) {
        return (Map) getPropertiesStream(container, fieldType)
                .collect(toMap(PropertyValue::getName, PropertyValue::getValue));
    }

    /**
     * Returns a map of all fields of the given container with the given type, and the given annotation.
     * This includes null values.
     * @param container The object whose fields should be returned
     * @param fieldType The type of properties to be returned
     * @param filter Annotation type that must be present on the field
     * @return A Map of property names to property values containing all properties of the given type.
     */
    @SuppressWarnings("unchecked") // PropertyValue is not generic
    public static  Map getPropertiesOfType(Object container, Class fieldType, Class filter) {
        return getPropertiesStream(container, fieldType, it -> it.isAnnotationPresent(filter))
                .collect(HashMap::new,
                        (m, e) -> m.put(e.getName(), (T) e.getValue()),
                        Map::putAll);
    }

    /**
     * Returns a map of all fields of the given container with the given type, using the given filter.
     * This includes null values.
     * @param container The object whose fields should be returned
     * @param fieldType The type of properties to be returned
     * @param filter Filter closure on the AnnotatedElement (Method or Field)
     * @return A Map of property names to property values containing all properties of the given type.
     */
    @SuppressWarnings("unchecked") // PropertyValue is not generic
    public static  Map getPropertiesOfType(Object container, Class fieldType, Predicate filter) {
        return (Map) getPropertiesStream(container, fieldType, filter)
                .collect(toMap(PropertyValue::getName, PropertyValue::getValue));
    }

    /**
     * Returns a map of all fields of the given container with the given type, using the given filter,
     * only including fields that are not null.
     * @param container The object whose fields should be returned
     * @param fieldType The type of properties to be returned
     * @param filter Filter closure on the AnnotatedElement (Method or Field)
     * @return A Map of property names to property values containing all properties of the given type.
     */
    public static  Map getNonEmptyPropertiesOfType(Object container, Class fieldType, @ClosureParams(value = SimpleType.class, options = "java.lang.reflect.AnnotatedElement") Closure filter) {
        return getNonEmptyPropertiesOfType(container,fieldType, filter::call);
    }

    /**
     * Returns a map of all fields of the given container with the given type, and the given annotation,
     * only including fields that are not null.
     * @param container The object whose fields should be returned
     * @param fieldType The type of properties to be returned
     * @param filter Annotation type that must be present on the field
     * @return A Map of property names to property values containing all properties of the given type.
     */
    public static  Map getNonEmptyPropertiesOfType(Object container, Class fieldType, Class filter) {
        return getNonEmptyPropertiesOfType(container,fieldType, it -> it.isAnnotationPresent(filter));
    }

    /**
     * Returns a map of all fields of the given container with the given type,
     * only including fields that are not null.
     * @param container The object whose fields should be returned
     * @param fieldType The type of properties to be returned
     * @return A Map of property names to property values containing all properties of the given type.
     */
    @SuppressWarnings("unchecked") // PropertyValue is not generic
    public static  Map getNonEmptyPropertiesOfType(Object container, Class fieldType) {
        return (Map) getPropertiesStream(container, fieldType)
                .filter(it -> it.getValue() != null)
                .collect(toMap(PropertyValue::getName, PropertyValue::getValue));   }

    /**
     * Returns a map of all fields of the given container with the given type, using the given filter,
     * only including fields that are not null.
     * @param container The object whose fields should be returned
     * @param fieldType The type of properties to be returned
     * @param filter Filter closure on the AnnotatedElement (Method or Field)
     * @return A Map of property names to property values containing all properties of the given type.
     */
    @SuppressWarnings("unchecked") // PropertyValue is not generic
    public static  Map getNonEmptyPropertiesOfType(Object container, Class fieldType, Predicate filter) {
        return (Map) getPropertiesStream(container, fieldType, filter)
                .filter(it -> it.getValue() != null)
                .collect(toMap(PropertyValue::getName, PropertyValue::getValue));
    }

    /**
     * Returns a Map of all collection fields with the given element type, using the given filter
     * @param container The object to search
     * @param fieldType The element type to look for
     * @param filter Filter closure on the AnnotatedElement (Method or Field)
     * @return A Map of property names to collections of values.
     */
    public static  Map> getCollectionsOfType(Object container, Class fieldType, @ClosureParams(value = SimpleType.class, options = "java.lang.reflect.AnnotatedElement") Closure filter) {
        return getCollectionsOfType(container, fieldType, filter::call);
    }

    /**
     * Returns a Map of all collection fields with the given element type, with the given annotation.
     * @param container The object to search
     * @param fieldType The element type to look for
     * @param filter Annotation type that must be present on the field
     * @return A Map of property names to collections of values.
     */
    public static  Map> getCollectionsOfType(Object container, Class fieldType, Class filter) {
        return getCollectionsOfType(container, fieldType, it -> it.isAnnotationPresent(filter));
    }

    /**
     * Returns a Map of all collection fields with the given element type.
     * @param container The object to search
     * @param fieldType The element type to look for
     * @return A Map of property names to collections of values.
     */
    @SuppressWarnings("unchecked") // PropertyValue is not generic
    public static  Map> getCollectionsOfType(Object container, Class fieldType) {
        return getPropertiesStream(container, Collection.class)
                .filter(it -> isCollectionOf(container, it, fieldType))
                .collect(toMap(PropertyValue::getName, it -> (Collection) it.getValue()));
    }

    static  boolean isCollectionOf(Object container, PropertyValue value, Class type) {
        if (!Collection.class.isAssignableFrom(value.getType()))
            return false;

        Optional aType = getParameterizedTypeForProperty(container, value);

        return getGenericParameter(aType, 1)
                .filter(type::isAssignableFrom)
                .isPresent();
    }

    static  boolean isMapOf(Object container, PropertyValue value, Class type) {
        if (!Map.class.isAssignableFrom(value.getType()))
            return false;

        Optional aType = getParameterizedTypeForProperty(container, value);

        return getGenericParameter(aType, 2)
                .filter(type::isAssignableFrom)
                .isPresent();
    }

    @NotNull
    private static Optional> getGenericParameter(Optional aType, int index) {
        return aType
                .map(ParameterizedType::getActualTypeArguments)
                .filter(fieldArgTypes -> fieldArgTypes.length >= index)
                .map(fieldArgTypes -> fieldArgTypes[index - 1])
                .map(it -> (Class) it);
    }

    private static Optional getParameterizedTypeForProperty(Object container, PropertyValue value) {
        AnnotatedElement element = getAnnotatedElementForProperty(container, value);

        return Optional.of(element)
                .filter(Field.class::isInstance)
                .map(it -> (Field) it)
                .map(Field::getGenericType)
                .filter(ParameterizedType.class::isInstance)
                .map(it -> (ParameterizedType) it);
    }

    /**
     * Returns a Map of all collection fields with the given element type, using the given filter
     * @param container The object to search
     * @param fieldType The element type to look for
     * @param filter Filter closure on the AnnotatedElement (Method or Field)
     * @return A Map of property names to collections of values.
     */
    @SuppressWarnings("unchecked") // PropertyValue is not generic
    public static  Map> getCollectionsOfType(Object container, Class fieldType, Predicate filter) {
        return getPropertiesStream(container, Collection.class, filter)
                .filter(it -> isCollectionOf(container, it, fieldType))
                .collect(toMap(PropertyValue::getName, it -> (Collection) it.getValue()));
    }

    @NotNull
    static Stream getPropertiesStream(Object container, Class fieldType, Predicate filter) {
        return getPropertiesStream(container, fieldType)
                .filter(it -> filter.test(getAnnotatedElementForProperty(container, it)));
    }

    @NotNull
    static Stream getPropertiesStream(Object container, Class fieldType) {
        return getAllPropertiesStream(container)
                .filter(it -> fieldType.isAssignableFrom(it.getType()) || it.getType().isPrimitive() && fieldType == Object.class)
                .filter(it -> hasField(container.getClass(), it.getName()));  // TODO Do we want to exclude getter only fields?
    }

    @NotNull
    static Stream getFieldPropertiesStream(Object container) {
        return getAllPropertiesStream(container)
                .filter(it -> hasField(container.getClass(), it.getName()));
    }

    @NotNull
    public static Stream getAllPropertiesStream(Object container) {
        return getMetaPropertyValues(container).stream()
                .filter(ClusterModel::isNoInternalProperty);
    }

    public static List getUnsetPropertiesOfType(Object container, Class fieldType) {
        return getPropertiesStream(container, fieldType)
                .filter(it -> it.getValue() == null)
                .collect(toList());
    }

    public static List getSetPropertiesOfType(Object container, Class fieldType) {
        return getPropertiesStream(container, fieldType)
                .filter(it -> it.getValue() != null)
                .collect(toList());
    }

    public static List getAllPropertiesOfType(Object container, Class fieldType) {
        return getPropertiesStream(container, fieldType)
                .collect(toList());
    }

    public static Optional getField(Class containerType, String fieldName) {
        while (containerType != null) {
            Optional field = Arrays.stream(containerType.getDeclaredFields()).filter(it -> it.getName().equals(fieldName)).findFirst();
            if (field.isPresent())
                return field;
            containerType = containerType.getSuperclass();
        }
        return Optional.empty();
    }

    public static boolean hasField(Class containerType, String fieldName) {
        return getField(containerType,fieldName).isPresent();
    }

    static AnnotatedElement getAnnotatedElementForProperty(Object container, PropertyValue propertyValue) {
        MetaProperty metaProperty = getMetaClass(container).getMetaProperty(propertyValue.getName());
        CachedField cachedField;

        if (metaProperty instanceof CachedField)
            cachedField = (CachedField) metaProperty;
        else if (metaProperty instanceof MetaBeanProperty)
            cachedField = ((MetaBeanProperty) metaProperty).getField();
        else
            throw new IllegalArgumentException("Cannot get AnnotatedElement for " + propertyValue.getName());

        if (cachedField != null) {
            if (GroovySystem.getVersion().startsWith("2"))
                return (Field) InvokerHelper.getProperty(cachedField, "field");
            else
                return (Field) InvokerHelper.getProperty(cachedField, "cachedField");
        }
        MetaBeanProperty property = (MetaBeanProperty) metaProperty;
        return ((CachedMethod) property.getGetter()).getCachedMethod();
    }

    static boolean isNoInternalProperty(PropertyValue property) {
        return !property.getName().contains("$");
    }

    public static  T getSingleValueOrFail(Object container, Class type, @ClosureParams(value = SimpleType.class, options = "java.lang.reflect.AnnotatedElement") Closure filter) {
        //noinspection unchecked
        return (T) getSinglePropertyOrFail(container, type, filter).getValue();
    }

    public static  T getSingleValueOrFail(Object container, Class type, Class filter) {
        //noinspection unchecked
        return (T) getSinglePropertyOrFail(container, type, it -> it.isAnnotationPresent(filter)).getValue();
    }

    public static  T getSingleValueOrFail(Object container, Class type, Predicate filter) {
        //noinspection unchecked
        return (T) getSinglePropertyOrFail(container, type, filter).getValue();
    }

    public static  T getSingleValueOrFail(Object container, Class type) {
        //noinspection unchecked
        return (T) getSinglePropertyOrFail(container, type).getValue();
    }

    public static PropertyValue getSinglePropertyOrFail(Object container, Class type, @ClosureParams(value = SimpleType.class, options = "java.lang.reflect.AnnotatedElement") Closure filter) {
        return getSinglePropertyOrFail(container, type, filter::call);
    }

    public static PropertyValue getSinglePropertyOrFail(Object container, Class type, Class filter) {
        return getSinglePropertyOrFail(container, type, it -> it.isAnnotationPresent(filter));
    }

    public static PropertyValue getSinglePropertyOrFail(Object container, Class type) {
        return getSinglePropertyOrFail(container, type, x -> true);
    }

    public static PropertyValue getSinglePropertyOrFail(Object container, Class type, Predicate filter) {
        List result = getPropertiesStream(container, type, filter).collect(toList());

        if (result.isEmpty())
            throw new IllegalArgumentException(format("Class %s has no field of type %s (possibly with filter)", container.getClass().getName(), type.getName()));

        if (result.size() > 1)
            throw new IllegalArgumentException(format("Class %s has more than one field of type %s (%s) (possibly with filter)",
                    container.getClass().getName(), type.getName(), result.stream().map(PropertyValue::getName).collect(toList())));

        return result.get(0);
    }

    public static  T getFieldOfType(Object container, Class type, String name) {
        Map properties = getPropertiesOfType(container, type);

        if (!properties.containsKey(name))
            throw new IllegalArgumentException("Class ${container.getClass().name} does not have a field '$name' with type '$type.name'");

        return properties.get(name);
    }

    public static Map getAnnotatedValues(Object object, Class annotation) {
        Map result = getFieldsAnnotatedWith(object, annotation);
        result.putAll(getMethodBasedValues(object, annotation));
        return result;
    }

    public static Map getFieldsAnnotatedWith(Object object, Class annotation) {
        return getPropertiesOfType(object, Object.class, annotation);
    }

    /**
     * Executes all parameterless, non-void methods with the given annotation and returns the result.
     * @param object The object whose methods should be executed
     * @param annotation The annotation to look for
     * @return A map of method names and their return values
     */
    public static Map getMethodBasedValues(Object object, Class annotation) {
        return getMethodBasedValues(object, annotation, Object.class);
    }

    /**
     * Executes all parameterless, non-void methods with the given annotation and returns the result.
     * @param object The object whose methods should be executed
     * @param annotation The annotation to look for
     * @param returnType only methods with this return type will be executed
     * @return A map of method names and their return values
     */
    public static Map getMethodBasedValues(Object object, Class annotation, Class returnType) {
        return getMethodsAnnotatedWithStream(object, returnType, annotation)
                .collect(toMap(Method::getName, it -> InvokerHelper.invokeMethod(object, it.getName(), null)));
    }

    /**
     * Returns all parameterless methods of the given Return type (possibly filtered).
     * @param object The object whose methods should be returned
     * @return A list of matching methods
     */
    static List getMethodsAnnotatedWith(Object object, Class annotation) {
        return getMethodsAnnotatedWithStream(object, Object.class, annotation)
                .collect(toList());
    }

    @NotNull
    private static Stream getMethodsAnnotatedWithStream(Object object, Class returnType, Class annotation) {
        return Arrays.stream(object.getClass().getMethods())
                .filter(it -> it.getParameterCount() == 0)
                .filter(it -> returnType.isAssignableFrom(it.getReturnType()))
                .filter(it -> !it.getName().startsWith("get"))
                .filter(it -> !it.getName().startsWith("is"))
                .filter(it -> it.isAnnotationPresent(annotation));
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy