com.yahoo.elide.core.EntityBinding.java Source code

Java tutorial

Introduction

Here is the source code for com.yahoo.elide.core.EntityBinding.java

Source

/*
 * Copyright 2015, Yahoo Inc.
 * Licensed under the Apache License, Version 2.0
 * See LICENSE file in project root for terms.
 */
package com.yahoo.elide.core;

import com.yahoo.elide.annotation.ComputedAttribute;
import com.yahoo.elide.annotation.Exclude;
import com.yahoo.elide.annotation.OnCommit;
import com.yahoo.elide.annotation.OnCreate;
import com.yahoo.elide.annotation.OnDelete;
import com.yahoo.elide.annotation.OnUpdate;
import com.yahoo.elide.core.exceptions.DuplicateMappingException;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.map.MultiValueMap;
import org.apache.commons.lang3.text.WordUtils;
import org.apache.commons.lang3.tuple.Pair;

import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import javax.persistence.Column;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.ManyToOne;
import javax.persistence.OneToMany;
import javax.persistence.OneToOne;
import javax.persistence.Transient;

/**
 * Entity Dictionary maps JSON API Entity beans to/from Entity type names.
 *
 * @see com.yahoo.elide.annotation.Include#type
 */
class EntityBinding {
    private static final List<Method> OBJ_METHODS = Arrays.asList(Object.class.getMethods());

    public final String jsonApi;
    public final ConcurrentLinkedDeque<String> attrsDeque;
    public final List<String> attrs;
    public final ConcurrentLinkedDeque<String> relationshipsDeque;
    public final List<String> relationships;
    public final ConcurrentHashMap<String, RelationshipType> relationshipTypes;
    public final ConcurrentHashMap<String, String> relationshipToInverse;
    public final ConcurrentHashMap<String, AccessibleObject> fieldsToValues;
    public final ConcurrentHashMap<String, Class<?>> fieldsToTypes;
    public final ConcurrentHashMap<String, String> aliasesToFields;
    public final ConcurrentHashMap<String, AccessibleObject> accessibleObject;
    public final MultiValueMap<Pair<Class, String>, Method> fieldsToTriggers;
    @Getter
    private AccessibleObject idField;
    @Getter
    private String idFieldName;
    @Getter
    private Class<?> idType;
    @Getter
    @Setter
    private Initializer initializer;

    public static final EntityBinding EMPTY_BINDING = new EntityBinding();

    /* empty binding constructor */
    private EntityBinding() {
        jsonApi = null;
        idField = null;
        idType = null;
        attrsDeque = null;
        attrs = null;
        relationshipsDeque = null;
        relationships = null;
        relationshipTypes = null;
        relationshipToInverse = null;
        fieldsToValues = null;
        fieldsToTypes = null;
        fieldsToTriggers = new MultiValueMap();
        aliasesToFields = null;
        accessibleObject = null;
    }

    public EntityBinding(Class<?> cls, String type) {
        // Map id's, attributes, and relationships
        Collection<AccessibleObject> fieldOrMethodList = CollectionUtils.union(Arrays.asList(cls.getFields()),
                Arrays.asList(cls.getMethods()));

        jsonApi = type;
        // Initialize our maps for this entity. Duplicates are checked above.
        attrsDeque = new ConcurrentLinkedDeque<>();
        relationshipsDeque = new ConcurrentLinkedDeque<>();
        relationshipTypes = new ConcurrentHashMap<>();
        relationshipToInverse = new ConcurrentHashMap<>();
        fieldsToValues = new ConcurrentHashMap<>();
        fieldsToTypes = new ConcurrentHashMap<>();
        fieldsToTriggers = new MultiValueMap<>();
        aliasesToFields = new ConcurrentHashMap<>();
        accessibleObject = new ConcurrentHashMap<>();
        bindEntityFields(cls, type, fieldOrMethodList);
        bindAccessibleObjects(cls, fieldOrMethodList);

        attrs = dequeToList(attrsDeque);
        relationships = dequeToList(relationshipsDeque);
    }

    /**
     * Bind fields of an entity including the Id field, attributes, and relationships.
     *
     * @param cls Class type to bind fields
     * @param type JSON API type identifier
     * @param fieldOrMethodList List of fields and methods on entity
     */
    private void bindEntityFields(Class<?> cls, String type, Collection<AccessibleObject> fieldOrMethodList) {
        for (AccessibleObject fieldOrMethod : fieldOrMethodList) {
            bindTrigger(OnCreate.class, fieldOrMethod);
            bindTrigger(OnDelete.class, fieldOrMethod);
            bindTrigger(OnUpdate.class, fieldOrMethod);
            bindTrigger(OnCommit.class, fieldOrMethod);

            if (fieldOrMethod.isAnnotationPresent(Id.class)) {
                bindEntityId(cls, type, fieldOrMethod);
            } else if (fieldOrMethod.isAnnotationPresent(Transient.class)
                    && !fieldOrMethod.isAnnotationPresent(ComputedAttribute.class)) {
                continue; // Transient. Don't serialize
            } else if (!fieldOrMethod.isAnnotationPresent(Exclude.class)) {
                if (fieldOrMethod instanceof Field
                        && Modifier.isTransient(((Field) fieldOrMethod).getModifiers())) {
                    continue; // Transient. Don't serialize
                }
                if (fieldOrMethod instanceof Method
                        && Modifier.isTransient(((Method) fieldOrMethod).getModifiers())) {
                    continue; // Transient. Don't serialize
                }
                if (fieldOrMethod instanceof Field && !fieldOrMethod.isAnnotationPresent(Column.class)
                        && Modifier.isStatic(((Field) fieldOrMethod).getModifiers())) {
                    continue; // Field must have Column annotation?
                }
                bindAttrOrRelation(cls, fieldOrMethod);
            }
        }
    }

    private void bindAccessibleObjects(Class<?> targetClass, Collection<AccessibleObject> fieldOrMethodList) {
        for (AccessibleObject fieldOrMethod : fieldOrMethodList) {
            String fieldName = getFieldName(fieldOrMethod);
            if (fieldName != null) {
                this.accessibleObject.put(fieldName, fieldOrMethod);
            }
        }
    }

    /**
     * Bind an id field to an entity.
     *
     * @param cls Class type to bind fields
     * @param type JSON API type identifier
     * @param fieldOrMethod Field or method to bind
     */
    private void bindEntityId(Class<?> cls, String type, AccessibleObject fieldOrMethod) {
        String fieldName = getFieldName(fieldOrMethod);
        Class<?> fieldType = getFieldType(fieldOrMethod);

        //Add id field to type map for the entity
        fieldsToTypes.put(fieldName, fieldType);

        //Set id field, type, and name
        idField = fieldOrMethod;
        idType = fieldType;
        idFieldName = fieldName;

        if (idField != null && !fieldOrMethod.equals(idField)) {
            throw new DuplicateMappingException(type + " " + cls.getName() + ":" + fieldName);
        }
    }

    /**
     * Convert a deque to a list.
     *
     * @param deque Deque to convert
     * @return Deque as a list
     */
    private static List<String> dequeToList(final Deque<String> deque) {
        ArrayList<String> result = new ArrayList<>();
        deque.stream().forEachOrdered(result::add);
        result.sort(String.CASE_INSENSITIVE_ORDER);
        return Collections.unmodifiableList(result);
    }

    /**
     * Bind an attribute or relationship.
     *
     * @param cls Class type to bind fields
     * @param fieldOrMethod Field or method to bind
     */
    private void bindAttrOrRelation(Class<?> cls, AccessibleObject fieldOrMethod) {
        boolean manyToMany = fieldOrMethod.isAnnotationPresent(ManyToMany.class);
        boolean manyToOne = fieldOrMethod.isAnnotationPresent(ManyToOne.class);
        boolean oneToMany = fieldOrMethod.isAnnotationPresent(OneToMany.class);
        boolean oneToOne = fieldOrMethod.isAnnotationPresent(OneToOne.class);
        boolean isRelation = manyToMany || manyToOne || oneToMany || oneToOne;

        String fieldName = getFieldName(fieldOrMethod);

        if (fieldName == null || fieldName.equals("id") || fieldName.equals("class")
                || OBJ_METHODS.contains(fieldOrMethod)
                || parameterizedFieldContainsAnnotation(fieldOrMethod, Arrays.asList(Exclude.class))) {
            return; // Reserved. Not attributes. Otherwise, potentially excluded.
        }

        Class<?> fieldType = getFieldType(fieldOrMethod);

        ConcurrentLinkedDeque<String> fieldList;
        if (isRelation) {
            fieldList = relationshipsDeque;
            RelationshipType type;
            String mappedBy;
            if (oneToMany) {
                type = RelationshipType.ONE_TO_MANY;
                mappedBy = fieldOrMethod.getAnnotation(OneToMany.class).mappedBy();
            } else if (oneToOne) {
                type = RelationshipType.ONE_TO_ONE;
                mappedBy = fieldOrMethod.getAnnotation(OneToOne.class).mappedBy();
            } else if (manyToMany) {
                type = RelationshipType.MANY_TO_MANY;
                mappedBy = fieldOrMethod.getAnnotation(ManyToMany.class).mappedBy();
            } else if (manyToOne) {
                type = RelationshipType.MANY_TO_ONE;
                mappedBy = "";
            } else {
                type = RelationshipType.NONE;
                mappedBy = "";
            }
            relationshipTypes.put(fieldName, type);
            relationshipToInverse.put(fieldName, mappedBy);
        } else {
            fieldList = attrsDeque;
        }

        fieldList.push(fieldName);
        fieldsToValues.put(fieldName, fieldOrMethod);
        fieldsToTypes.put(fieldName, fieldType);
    }

    /**
     * Returns name of field whether public member or method.
     *
     * @param fieldOrMethod field or method
     * @return field or method name
     */
    private static String getFieldName(AccessibleObject fieldOrMethod) {
        if (fieldOrMethod instanceof Field) {
            return ((Field) fieldOrMethod).getName();
        } else {
            Method method = (Method) fieldOrMethod;
            String name = method.getName();

            if (name.startsWith("get") && method.getParameterCount() == 0) {
                name = WordUtils.uncapitalize(name.substring("get".length()));
            } else if (name.startsWith("is") && method.getParameterCount() == 0) {
                name = WordUtils.uncapitalize(name.substring("is".length()));
            } else {
                return null;
            }
            return name;
        }
    }

    /**
     * Returns type of field whether public member or method.
     *
     * @param fieldOrMethod field or method
     * @return field type
     */
    private static Class<?> getFieldType(AccessibleObject fieldOrMethod) {
        if (fieldOrMethod instanceof Field) {
            return ((Field) fieldOrMethod).getType();
        } else {
            return ((Method) fieldOrMethod).getReturnType();
        }
    }

    private static boolean parameterizedFieldContainsAnnotation(AccessibleObject fieldOrMethod,
            List<Class<? extends Annotation>> annotations) {
        Type type;
        if (fieldOrMethod instanceof Method) {
            type = ((Method) fieldOrMethod).getGenericReturnType();
        } else {
            type = ((Field) fieldOrMethod).getGenericType();
        }

        if (type instanceof ParameterizedType) {
            Type[] types = ((ParameterizedType) type).getActualTypeArguments();
            if (types != null) {
                // NOTE: We look through all types to ensure nothing is exluded as part of complex representations
                // Consider, for instance, a Map<Relation1, Map<Relation2, ExcludedRelation3>>
                for (Type paramType : types) {
                    if (EntityDictionary.getFirstAnnotation((Class<?>) paramType, annotations) != null) {
                        return true;
                    }
                }
            }
        } else {
            return EntityDictionary.getFirstAnnotation(getFieldType(fieldOrMethod), annotations) != null;
        }

        return false;
    }

    private <A extends Annotation> void bindTrigger(Class<A> annotationClass, AccessibleObject fieldOrMethod) {
        if (fieldOrMethod instanceof Method && fieldOrMethod.isAnnotationPresent(annotationClass)) {
            A onTrigger = fieldOrMethod.getAnnotation(annotationClass);
            String value;
            try {
                value = (String) annotationClass.getMethod("value").invoke(onTrigger);
            } catch (ReflectiveOperationException | IllegalArgumentException | SecurityException e) {
                value = "";
            }
            fieldsToTriggers.put(Pair.of(annotationClass, value), fieldOrMethod);
        }
    }

    public <A extends Annotation> Collection<Method> getTriggers(Class<A> annotationClass, String fieldName) {
        Collection<Method> methods = fieldsToTriggers.getCollection(Pair.of(annotationClass, fieldName));
        return methods == null ? Collections.emptyList() : methods;
    }
}