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

com.bol.reflection.ReflectionCache Maven / Gradle / Ivy

package com.bol.reflection;

import com.bol.secure.FieldEncryptedPredicate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.mapping.Field;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class ReflectionCache {

    private static final Logger LOG = LoggerFactory.getLogger(ReflectionCache.class);

    private final ConcurrentHashMap, List> reflectionCache = new ConcurrentHashMap<>();
    private final FieldEncryptedPredicate fieldEncryptedPredicate;

    public ReflectionCache(FieldEncryptedPredicate fieldEncryptedPredicate) {
        this.fieldEncryptedPredicate = fieldEncryptedPredicate;
    }

    // used by CachedEncryptionEventListener to gather metadata of a class and all it fields, recursively.
    public List reflectRecursive(Class objectClass) {
        List nodes = reflectionCache.get(objectClass);
        if (nodes != null) return nodes;

        synchronized (this) {
            return buildRecursive(objectClass, new HashMap<>());
        }
    }

    // building is necessary to avoid putting half-processed data in `reflectionCache` (where it would be returned to other threads)
    private List buildRecursive(Class objectClass, HashMap, List> building) {
        if (isPrimitive(objectClass)) return Collections.emptyList();

        List processed = reflectionCache.get(objectClass);
        if (processed != null) return processed;

        List processing = building.get(objectClass);
        if (processing != null) return processing;

        List nodes = new ArrayList<>();
        building.put(objectClass, nodes);

        ReflectionUtils.doWithFields(objectClass, field -> {
            String fieldName = field.getName();
            try {
                if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) return;

                String documentName = parseFieldAnnotation(field, fieldName);

                if (fieldEncryptedPredicate.test(field)) {
                    // direct @Encrypted annotation - crypt the corresponding field of BasicDbObject
                    nodes.add(new Node(fieldName, documentName, Collections.emptyList(), Node.Type.DIRECT, field));

                } else {
                    Class fieldType = field.getType();
                    Type fieldGenericType = field.getGenericType();

                    if (Collection.class.isAssignableFrom(fieldType)) {
                        List children = processParameterizedTypes(fieldGenericType, building);
                        if (!children.isEmpty()) nodes.add(new Node(fieldName, documentName, unwrap(children), Node.Type.LIST, field));

                    } else if (Map.class.isAssignableFrom(fieldType)) {
                        List children = processParameterizedTypes(fieldGenericType, building);
                        if (!children.isEmpty()) nodes.add(new Node(fieldName, documentName, unwrap(children), Node.Type.MAP, field));

                    } else {
                        // descending into sub-documents
                        List children = buildRecursive(fieldType, building);
                        if (!children.isEmpty()) nodes.add(new Node(fieldName, documentName, children, Node.Type.DOCUMENT, field));
                    }
                }

            } catch (Exception e) {
                throw new IllegalArgumentException(objectClass.getName() + "." + fieldName, e);
            }
        });

        reflectionCache.put(objectClass, nodes);

        return nodes;
    }

    // used by ReflectionEncryptionEventListener to map a single Document
    public List reflectSingle(Class objectClass) {
        return reflectionCache.computeIfAbsent(objectClass, this::buildSingle);
    }

    // FIXME: this is a slimmed down copy-paste of buildRecursive(); find a way to bring Cached and Reflective listener closer together!
    private List buildSingle(Class objectClass) {
        if (isPrimitive(objectClass)) return Collections.emptyList();

        List nodes = new ArrayList<>();

        ReflectionUtils.doWithFields(objectClass, field -> {
            String fieldName = field.getName();
            try {
                if (Modifier.isStatic(field.getModifiers()) || Modifier.isTransient(field.getModifiers())) return;

                String documentName = parseFieldAnnotation(field, fieldName);

                if (fieldEncryptedPredicate.test(field)) {
                    // direct @Encrypted annotation - crypt the corresponding field of BasicDbObject
                    nodes.add(new Node(fieldName, documentName, Collections.emptyList(), Node.Type.DIRECT, field));

                } else {
                    Class fieldType = field.getType();

                    if (Collection.class.isAssignableFrom(fieldType)) {
                        nodes.add(new Node(fieldName, documentName, Collections.emptyList(), Node.Type.LIST, field));

                    } else if (Map.class.isAssignableFrom(fieldType)) {
                        nodes.add(new Node(fieldName, documentName, Collections.emptyList(), Node.Type.MAP, field));

                    } else {
                        nodes.add(new Node(fieldName, documentName, Collections.emptyList(), Node.Type.DOCUMENT, field));
                    }
                }

            } catch (Exception e) {
                throw new IllegalArgumentException(objectClass.getName() + "." + fieldName, e);
            }
        });

        return nodes;
    }

    List processParameterizedTypes(Type type, HashMap, List> building) {
        if (type instanceof Class) {
            List children = buildRecursive((Class) type, building);
            if (!children.isEmpty()) return Collections.singletonList(new Node(null, children, Node.Type.DOCUMENT));

        } else if (type instanceof ParameterizedType) {
            ParameterizedType subType = (ParameterizedType) type;
            Class rawType = (Class) subType.getRawType();

            if (Collection.class.isAssignableFrom(rawType)) {
                List children = processParameterizedTypes(subType.getActualTypeArguments()[0], building);
                if (!children.isEmpty()) return Collections.singletonList(new Node(null, children, Node.Type.LIST));

            } else if (Map.class.isAssignableFrom(rawType)) {
                List children = processParameterizedTypes(subType.getActualTypeArguments()[1], building);
                if (!children.isEmpty()) return Collections.singletonList(new Node(null, children, Node.Type.MAP));

            } else {
                throw new IllegalArgumentException("Unknown reflective raw type class " + rawType);
            }

        } else {
            throw new IllegalArgumentException("Unknown reflective type class " + type.getClass());
        }

        return Collections.emptyList();
    }

    static List unwrap(List result) {
        if (result.size() != 1) return result;
        Node node = result.get(0);
        if (node.fieldName != null) return result;
        return node.children;
    }

    /**
     * process custom name in @Field annotation
     */
    static String parseFieldAnnotation(java.lang.reflect.Field field, String fieldName) {
        Field fieldAnnotation = field.getAnnotation(Field.class);
        if (fieldAnnotation != null) {
            String name = fieldAnnotation.name();

            if (!name.isEmpty()) return name;
            else {
                String value = fieldAnnotation.value();
                if (!value.isEmpty()) return value;
            }
        }
        return fieldName;
    }

    // same as ClassUtils.isPrimitiveOrWrapper(), but also includes String
    public static boolean isPrimitive(Class clazz) {
        return clazz.isPrimitive() || primitiveClasses.contains(clazz);
    }

    private static final Set> primitiveClasses = new HashSet<>(Arrays.asList(
            Boolean.class,
            Byte.class,
            Character.class,
            Double.class,
            Float.class,
            Integer.class,
            Long.class,
            Short.class,
            Void.class,
            String.class
    ));
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy