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

Java tutorial

Introduction

Here is the source code for com.yahoo.elide.core.PersistentResource.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.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.base.Preconditions;
import com.google.common.collect.Sets;
import com.yahoo.elide.annotation.Audit;
import com.yahoo.elide.annotation.CreatePermission;
import com.yahoo.elide.annotation.DeletePermission;
import com.yahoo.elide.annotation.OnCreate;
import com.yahoo.elide.annotation.OnDelete;
import com.yahoo.elide.annotation.OnUpdate;
import com.yahoo.elide.annotation.ReadPermission;
import com.yahoo.elide.annotation.SharePermission;
import com.yahoo.elide.annotation.UpdatePermission;
import com.yahoo.elide.audit.InvalidSyntaxException;
import com.yahoo.elide.audit.LogMessage;
import com.yahoo.elide.core.exceptions.ForbiddenAccessException;
import com.yahoo.elide.core.exceptions.InternalServerErrorException;
import com.yahoo.elide.core.exceptions.InvalidAttributeException;
import com.yahoo.elide.core.exceptions.InvalidEntityBodyException;
import com.yahoo.elide.core.exceptions.InvalidObjectIdentifierException;
import com.yahoo.elide.core.filter.Operator;
import com.yahoo.elide.core.filter.Predicate;
import com.yahoo.elide.extensions.PatchRequestScope;
import com.yahoo.elide.jsonapi.models.Data;
import com.yahoo.elide.jsonapi.models.Relationship;
import com.yahoo.elide.jsonapi.models.Resource;
import com.yahoo.elide.jsonapi.models.ResourceIdentifier;
import com.yahoo.elide.jsonapi.models.SingleElementSet;
import com.yahoo.elide.security.Check;
import com.yahoo.elide.security.User;
import com.yahoo.elide.utils.coerce.CoerceUtil;
import lombok.NonNull;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.text.WordUtils;

import javax.persistence.GeneratedValue;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.TreeMap;

import static com.yahoo.elide.security.UserCheck.DENY;

/**
 * Resource wrapper around Entity bean.
 *
 * @param <T> type of resource
 */
@ToString
@Slf4j
public class PersistentResource<T> {
    private final String type;
    protected T obj;
    private final ResourceLineage lineage;
    private final Optional<String> uuid;
    private final User user;
    private final ObjectEntityCache entityCache;
    private final DataStoreTransaction transaction;
    @NonNull
    private final RequestScope requestScope;
    private final Optional<PersistentResource<?>> parent;

    /**
     * The Dictionary.
     */
    protected final EntityDictionary dictionary;

    /* Sort strings first by length then contents */
    private final Comparator<String> comparator = (string1, string2) -> {
        int diff = string1.length() - string2.length();
        return diff == 0 ? string1.compareTo(string2) : diff;
    };

    protected static final boolean ANY = true;
    protected static final boolean ALL = false;

    /**
     * Create a resource in the database.
     * @param parent - The immediate ancestor in the lineage or null if this is a root.
     * @param entityClass the entity class
     * @param requestScope the request scope
     * @param uuid the uuid
     * @param <T> object type
     * @return persistent resource
     */
    @SuppressWarnings("resource")
    public static <T> PersistentResource<T> createObject(PersistentResource<?> parent, Class<T> entityClass,
            RequestScope requestScope, String uuid) {
        DataStoreTransaction tx = requestScope.getTransaction();

        T obj = tx.createObject(entityClass);
        PersistentResource<T> newResource = new PersistentResource<>(obj, parent, uuid, requestScope);
        checkPermission(CreatePermission.class, newResource);
        newResource.audit(Audit.Action.CREATE);
        newResource.runTriggers(OnCreate.class);
        requestScope.queueCommitTrigger(newResource);

        String type = newResource.getType();
        requestScope.getObjectEntityCache().put(type, uuid, newResource.getObject());

        // Initialize null ToMany collections
        requestScope.getDictionary().getRelationships(entityClass).stream()
                .filter(relationName -> newResource.getRelationshipType(relationName).isToMany() && newResource
                        .getValue(newResource.getObject(), relationName, newResource.dictionary) == null)
                .forEach(relationName -> newResource.setValue(relationName, new LinkedHashSet<>()));

        // Keep track of new resources for non shareable resources
        requestScope.getNewResources().add(newResource);

        return newResource;
    }

    /**
     * Create a resource in the database.
     * @param entityClass the entity class
     * @param requestScope the request scope
     * @param uuid the uuid
     * @param <T> type of resource
     * @return persistent resource
     */
    public static <T> PersistentResource<T> createObject(Class<T> entityClass, RequestScope requestScope,
            String uuid) {
        return createObject(null, entityClass, requestScope, uuid);
    }

    /**
     * Constructor.
     *
     * @param parent the parent
     * @param obj the obj
     * @param requestScope the request scope
     */
    public PersistentResource(PersistentResource<?> parent, T obj, RequestScope requestScope) {
        this(obj, parent, requestScope);
    }

    /**
     * Constructor.
     *
     * @param obj the obj
     * @param requestScope the request scope
     */
    public PersistentResource(T obj, RequestScope requestScope) {
        this(obj, null, requestScope);
    }

    /**
     * Construct a new resource from the ID provided.
     *
     * @param obj the obj
     * @param parent the parent
     * @param id the id
     * @param requestScope the request scope
     */
    protected PersistentResource(@NonNull T obj, PersistentResource<?> parent, String id,
            RequestScope requestScope) {
        this.obj = obj;
        this.uuid = Optional.ofNullable(id);
        // TODO Use Bind annotation
        this.parent = Optional.ofNullable(parent);
        this.lineage = this.parent.isPresent() ? new ResourceLineage(parent.lineage, parent)
                : new ResourceLineage();
        this.dictionary = requestScope.getDictionary();
        this.type = dictionary.getBinding(obj.getClass());
        this.user = requestScope.getUser();
        this.entityCache = requestScope.getObjectEntityCache();
        this.transaction = requestScope.getTransaction();
        this.requestScope = requestScope;
        dictionary.initializeEntity(obj);
    }

    /**
     * Constructor for testing.
     *
     * @param obj the obj
     * @param parent the parent
     * @param requestScope the request scope
     */
    protected PersistentResource(T obj, PersistentResource<?> parent, RequestScope requestScope) {
        this(obj, parent, requestScope.getObjectEntityCache().getUUID(obj), requestScope);
    }

    /**
     * Check whether an id matches for this persistent resource.
     *
     * @param checkId the check id
     * @return True if matches false otherwise
     */
    public boolean matchesId(String checkId) {
        if (checkId == null) {
            return false;
        } else if (uuid.isPresent() && checkId.equals(uuid.get())) {
            return true;
        }
        String id = getId();
        return checkId.equals(id);
    }

    /**
     * Load an single entity from the DB.
     *
     * @param loadClass resource type
     * @param id the id
     * @param requestScope the request scope
     * @param <T> type of resource
     * @return resource persistent resource
     * @throws InvalidObjectIdentifierException the invalid object identifier exception
     */
    @SuppressWarnings("resource")
    @NonNull
    public static <T> PersistentResource<T> loadRecord(Class<T> loadClass, String id, RequestScope requestScope)
            throws InvalidObjectIdentifierException {
        Preconditions.checkNotNull(loadClass);
        Preconditions.checkNotNull(id);
        Preconditions.checkNotNull(requestScope);

        DataStoreTransaction tx = requestScope.getTransaction();
        EntityDictionary dictionary = requestScope.getDictionary();
        ObjectEntityCache cache = requestScope.getObjectEntityCache();

        // Check the resource cache if exists
        @SuppressWarnings("unchecked")
        T obj = (T) cache.get(dictionary.getBinding(loadClass), id);
        if (obj == null) {
            // try to load object
            Class<?> idType = dictionary.getIdType(loadClass);
            obj = tx.loadObject(loadClass, (Serializable) CoerceUtil.coerce(id, idType));
            if (obj == null) {
                throw new InvalidObjectIdentifierException(id, loadClass.getSimpleName());
            }
        }

        PersistentResource<T> resource = new PersistentResource<>(obj, requestScope);
        resource.checkFieldAwarePermissions(ReadPermission.class);
        requestScope.queueCommitTrigger(resource);

        return resource;
    }

    /**
     * Load a collection from the datastore.
     *
     * @param <T> the type parameter
     * @param loadClass the load class
     * @param requestScope the request scope
     * @return a filtered collection of resources loaded from the datastore.
     */
    @NonNull
    public static <T> Set<PersistentResource<T>> loadRecords(Class<T> loadClass, RequestScope requestScope) {
        DataStoreTransaction tx = requestScope.getTransaction();

        if (isDenyFilter(requestScope, loadClass)) {
            return Collections.emptySet();
        }

        Iterable<T> list;
        ReadPermission annotation = requestScope.getDictionary().getAnnotation(loadClass, ReadPermission.class);
        FilterScope filterScope = loadChecks(annotation, requestScope);
        list = tx.loadObjects(loadClass, filterScope);
        Set<PersistentResource<T>> resources = new PersistentResourceSet(list, requestScope);
        resources = filter(ReadPermission.class, resources);
        for (PersistentResource<T> resource : resources) {
            requestScope.queueCommitTrigger(resource);
        }
        return resources;
    }

    /**
     * Update attribute in existing resource.
     *
     * @param fieldName the field name
     * @param newVal the new val
     * @return true if object updated, false otherwise
     */
    public boolean updateAttribute(String fieldName, Object newVal) {
        checkFieldAwarePermissions(UpdatePermission.class, fieldName);
        Object val = getAttribute(fieldName);
        if (val != newVal && (val == null || !val.equals(newVal))) {
            this.setValueChecked(fieldName, newVal);
            transaction.save(obj);
            audit(fieldName);
            return true;
        }
        return false;
    }

    /**
     * Perform a full replacement on relationships.
     * Here is an example:
     * The following are ids for a hypothetical relationship.
     * GIVEN:
     * all (all the ids in the DB) = 1,2,3,4,5
     * mine (everything the current user has access to) = 1,2,3
     * requested (what the user wants to change to) = 3,6
     * THEN:
     * deleted (what gets removed from the DB) = 1,2
     * final (what get stored in the relationship) = 3,4,5,6
     * BECAUSE:
     * notMine = all - mine
     * updated = (requested UNION mine) - (requested INTERSECT mine)
     * deleted = (mine - requested)
     * final = (notMine) UNION requested
     * @param fieldName the field name
     * @param resourceIdentifiers the resource identifiers
     * @return True if object updated, false otherwise
     */
    public boolean updateRelation(String fieldName, Set<PersistentResource> resourceIdentifiers) {
        checkFieldAwarePermissions(UpdatePermission.class, fieldName);
        RelationshipType type = getRelationshipType(fieldName);
        if (type.isToMany()) {
            return updateToManyRelation(fieldName, resourceIdentifiers);
        } else { // To One Relationship
            return updateToOneRelation(fieldName, resourceIdentifiers);
        }
    }

    /**
     * Updates a to-many relationship.
     * @param fieldName the field name
     * @param resourceIdentifiers the resource identifiers
     * @return true if updated. false otherwise
     */
    protected boolean updateToManyRelation(String fieldName, Set<PersistentResource> resourceIdentifiers) {
        Set<PersistentResource> mine = getRelation(fieldName);

        Set<PersistentResource> requested;
        Set<PersistentResource> updated;
        Set<PersistentResource> deleted;
        Set<PersistentResource> added;

        if (resourceIdentifiers == null) {
            throw new InvalidEntityBodyException("Bad relation data");
        }
        if (resourceIdentifiers.isEmpty()) {
            requested = new LinkedHashSet<>();
        } else {

            // TODO - this resource does not include a lineage. This could cause issues for audit.
            requested = resourceIdentifiers;
        }

        // deleted = mine - requested
        deleted = Sets.difference(mine, requested);

        // updated = (requested UNION mine) - (requested INTERSECT mine)
        updated = Sets.difference(Sets.union(mine, requested), Sets.intersection(mine, requested));

        added = Sets.difference(updated, deleted);

        checkSharePermission(added);

        Collection collection = (Collection) this.getValue(fieldName);

        if (collection == null) {
            this.setValue(fieldName, mine);
        }

        deleted.stream().forEach(toDelete -> {
            checkFieldAwarePermissions(UpdatePermission.class, fieldName);
            delFromCollection(collection, fieldName, toDelete);
            deleteInverseRelation(fieldName, toDelete.getObject());
            transaction.save(toDelete.getObject());
        });

        added.stream().forEach(toAdd -> {
            addToCollection(collection, fieldName, toAdd);
            addInverseRelation(fieldName, toAdd.getObject());
            transaction.save(toAdd.getObject());
        });

        transaction.save(getObject());
        audit(fieldName);

        return !updated.isEmpty();
    }

    /**
     * Update a 2-one relationship.
     *
     * @param fieldName the field name
     * @param resourceIdentifiers the resource identifiers
     * @return true if updated. false otherwise
     */
    protected boolean updateToOneRelation(String fieldName, Set<PersistentResource> resourceIdentifiers) {
        Set<PersistentResource> mine = getRelation(fieldName);

        Object newValue;
        if (resourceIdentifiers == null || resourceIdentifiers.isEmpty()) {
            newValue = null;
        } else {
            PersistentResource newResource = resourceIdentifiers.iterator().next();
            newValue = newResource.getObject();
        }

        PersistentResource oldResource = !mine.isEmpty() ? mine.iterator().next() : null;

        if (oldResource == null) {
            if (newValue == null) {
                return false;
            }
            checkSharePermission(resourceIdentifiers);
        } else if (oldResource.getObject().equals(newValue)) {
            return false;
        } else {
            checkSharePermission(resourceIdentifiers);
            deleteInverseRelation(fieldName, oldResource.getObject());
            transaction.save(oldResource.getObject());
        }

        if (newValue != null) {
            addInverseRelation(fieldName, newValue);
            transaction.save(newValue);
        }

        this.setValueChecked(fieldName, newValue);

        transaction.save(obj);
        audit(fieldName);
        return true;
    }

    /**
     * Clear all elements from a relation.
     *
     * @param relationName Name of relation to clear
     * @return True if object updated, false otherwise
     */
    public boolean clearRelation(String relationName) {
        checkFieldAwarePermissions(UpdatePermission.class, relationName);
        Set<PersistentResource> mine = getRelation(relationName);

        if (mine.isEmpty()) {
            return false;
        }

        RelationshipType type = getRelationshipType(relationName);

        mine.stream().forEach(toDelete -> deleteInverseRelation(relationName, toDelete.getObject()));

        if (type.isToOne()) {
            PersistentResource oldValue = mine.iterator().next();
            this.nullValue(relationName, oldValue);
            transaction.save(oldValue.getObject());
        } else {
            Collection collection = (Collection) this.getValue(relationName);
            mine.stream().forEach(toDelete -> {
                String inverseRelation = getInverseRelationField(relationName);
                checkFieldAwarePermissions(UpdatePermission.class, relationName);
                toDelete.checkFieldAwarePermissions(UpdatePermission.class, inverseRelation);
                delFromCollection(collection, relationName, toDelete);
                transaction.save(toDelete.getObject());
            });
        }

        transaction.save(obj);

        audit(relationName);
        return true;
    }

    /**
     * Remove a relationship.
     *
     * @param fieldName the field name
     * @param removeResource the remove resource
     */
    public void removeRelation(String fieldName, PersistentResource removeResource) {
        checkFieldAwarePermissions(UpdatePermission.class, fieldName);
        String inverseField = getInverseRelationField(fieldName);
        if (inverseField.length() > 0) {
            removeResource.checkFieldAwarePermissions(UpdatePermission.class, inverseField);
        }

        Object relation = this.getValue(fieldName);
        if (relation instanceof Collection) {
            if (!((Collection) relation).contains(removeResource.getObject())) {

                //Nothing to do
                return;
            }
            delFromCollection((Collection) relation, fieldName, removeResource);
        } else {
            Object oldValue = getValue(fieldName);
            if (oldValue == null || !oldValue.equals(removeResource.getObject())) {

                //Nothing to do
                return;
            }
            this.nullValue(fieldName, removeResource);
        }

        deleteInverseRelation(fieldName, removeResource.getObject());

        transaction.save(removeResource.getObject());
        transaction.save(obj);
        audit(fieldName);
    }

    /**
     * Add relation link from a given parent resource to a child resource.
     *
     * @param fieldName which relation link
     * @param newRelation the new relation
     */
    public void addRelation(String fieldName, PersistentResource newRelation) {
        checkFieldAwarePermissions(UpdatePermission.class, fieldName);
        checkSharePermission(Collections.singleton(newRelation));
        Object relation = this.getValue(fieldName);

        if (relation instanceof Collection) {
            addToCollection((Collection) relation, fieldName, newRelation);
            addInverseRelation(fieldName, newRelation.getObject());
        } else {
            // Not a collection, but may be trying to create a ToOne relationship.
            updateRelation(fieldName, Collections.singleton(newRelation));
            return;
        }

        transaction.save(newRelation.getObject());
        transaction.save(obj);
        audit(fieldName);
    }

    /**
     * Check if adding or updating a relation is allowed.
     *
     * @param resourceIdentifiers The persistent resources that are being added
     */
    protected void checkSharePermission(Set<PersistentResource> resourceIdentifiers) {
        if (resourceIdentifiers == null) {
            return;
        }

        final Set<PersistentResource> newResources = getRequestScope().getNewResources();

        for (PersistentResource persistentResource : resourceIdentifiers) {
            if (!newResources.contains(persistentResource)) {
                if (persistentResource.isShareable()) {
                    checkPermission(SharePermission.class, persistentResource);
                } else if (!lineage.getRecord(persistentResource.getType()).contains(persistentResource)) {
                    String message = String.format("ForbiddenAccess not shared %s#%s", persistentResource.getType(),
                            persistentResource.getId());
                    log.debug(message);
                    throw new ForbiddenAccessException(message);
                }
            }
        }
    }

    /**
     * Checks if this persistent resource's underlying entity is shareable.
     *
     * @return true if this persistent resource's entity is shareable.
     */
    private boolean isShareable() {
        return getRequestScope().getDictionary().isShareable(obj.getClass());
    }

    /**
     * Delete an existing entity.
     *
     * @throws ForbiddenAccessException the forbidden access exception
     */
    public void deleteResource() throws ForbiddenAccessException {
        checkPermission(DeletePermission.class, this);

        /*
         * Search for bidirectional relationships.  For each bidirectional relationship,
         * we need to remove ourselves from that relationship
         */
        Map<String, Relationship> relations = getRelationships();
        for (Map.Entry<String, Relationship> entry : relations.entrySet()) {
            String relationName = entry.getKey();
            String inverseRelationName = dictionary.getRelationInverse(getResourceClass(), relationName);
            if (!inverseRelationName.equals("")) {
                for (PersistentResource inverseResource : getRelation(relationName)) {
                    deleteInverseRelation(relationName, inverseResource.getObject());
                    transaction.save(inverseResource.getObject());
                }
            }
        }

        transaction.delete(getObject());
        audit(Audit.Action.DELETE);
        runTriggers(OnDelete.class);
    }

    /**
     * Get resource ID.
     *
     * @return ID id
     */
    public String getId() {
        return dictionary.getId(getObject());
    }

    /**
     * Set resource ID.
     *
     * @param id resource id
     */
    public void setId(String id) {
        this.setValue(dictionary.getIdFieldName(getResourceClass()), id);
    }

    /**
     * Indicates if the ID is generated or not.
     *
     * @return Boolean
     */
    public Boolean isIdGenerated() {
        return getIdAnnotations().stream().anyMatch(a -> a.annotationType().equals(GeneratedValue.class));
    }

    /**
     * Returns annotations applied to the ID field.
     *
     * @return Collection of Annotations
     */
    private Collection<Annotation> getIdAnnotations() {
        return dictionary.getIdAnnotations(getObject());
    }

    /**
     * Gets UUID.
     * @return the UUID
     */
    public Optional<String> getUUID() {
        return uuid;
    }

    /**
     * Load a single entity relation from the PersistentResource.
     *
     * @param relation the relation
     * @param id the id
     * @return PersistentResource relation
     */
    public PersistentResource getRelation(String relation, String id) {
        Set<Predicate> filters;
        // Filtering not supported in Patxh extension
        if (requestScope instanceof PatchRequestScope) {
            filters = Collections.emptySet();
        } else {
            Class<?> entityType = dictionary.getParameterizedType(getResourceClass(), relation);
            if (entityType == null) {
                throw new InvalidAttributeException(relation, type);
            }
            Object idVal = CoerceUtil.coerce(id, dictionary.getIdType(entityType));
            String idField = dictionary.getIdFieldName(entityType);
            Predicate idFilter = new Predicate(idField, Operator.IN, Collections.singletonList(idVal));
            filters = Collections.singleton(idFilter);
        }

        /* getRelation performs read permission checks */
        Set<PersistentResource> resources = getRelation(relation, filters);
        for (PersistentResource childResource : resources) {
            if (childResource.matchesId(id)) {
                return childResource;
            }
        }
        throw new InvalidObjectIdentifierException(id, relation);
    }

    /**
     * Get collection of resources from relation field.
     *
     * @param relationName field
     * @return collection relation
     */
    public Set<PersistentResource> getRelation(String relationName) {
        if (requestScope.getTransaction() != null && !requestScope.getPredicates().isEmpty()) {
            final Class<?> entityClass = dictionary.getParameterizedType(obj, relationName);
            final String valType = dictionary.getBinding(entityClass);
            final Set<Predicate> filters = new HashSet<>(requestScope.getPredicatesOfType(valType));
            return getRelation(relationName, filters);
        } else {
            return getRelation(relationName, Collections.<Predicate>emptySet());
        }
    }

    /**
     * Get collection of resources from relation field.
     *
     * @param relationName field
     * @param filters A set of filters (possibly empty) to attempt to push down to the data store to filter
     *                the returned collection.
     * @return collection relation
     */
    protected Set<PersistentResource> getRelation(String relationName, Set<Predicate> filters) {
        List<String> relations = dictionary.getRelationships(obj);

        String realName = dictionary.getNameFromAlias(obj, relationName);
        relationName = (realName == null) ? relationName : realName;

        if (relationName == null || relations == null || !relations.contains(relationName)) {
            throw new InvalidAttributeException(relationName, type);
        }

        Set<PersistentResource<Object>> resources = Sets.newLinkedHashSet();

        // check for deny access on relationship to avoid iterating a lazy collection
        if (isDenyFilter(requestScope, dictionary.getParameterizedType(obj, relationName))) {
            checkFieldAwarePermissions(ReadPermission.class, relationName);
            return (Set) resources;
        }

        RelationshipType type = getRelationshipType(relationName);
        Object val = this.getValue(relationName);
        if (val == null) {
            return (Set) resources;
        } else if (val instanceof Collection) {
            Collection filteredVal = (Collection) val;

            if (!filters.isEmpty()) {
                final Class<?> entityClass = dictionary.getParameterizedType(obj, relationName);
                filteredVal = requestScope.getTransaction().filterCollection(filteredVal, entityClass, filters);
            }

            resources = new PersistentResourceSet(filteredVal, requestScope);
        } else if (type.isToOne()) {
            resources = new SingleElementSet(new PersistentResource(this, val, getRequestScope()));
        } else {
            resources.add(new PersistentResource(this, val, getRequestScope()));
        }

        return (Set) filter(ReadPermission.class, resources);
    }

    /**
     * If relationship collection type is denied, do not read lazy collection.
     *
     * @return true DENY check type
     */
    private static boolean isDenyFilter(RequestScope requestScope, Class<?> recordClass) {
        if (requestScope.getSecurityMode() == SecurityMode.SECURITY_INACTIVE) {
            return false;
        }

        EntityDictionary dictionary = requestScope.getDictionary();
        ReadPermission annotation = dictionary.getAnnotation(recordClass, ReadPermission.class);
        FilterScope filterScope = loadChecks(annotation, requestScope);

        if (filterScope.getUserPermission() != DENY) {
            return false;
        }

        // Temporary fix for UserLevel checks. This concept will be reworked in Elide 2.0
        // In short, check all fields.
        List<String> fields = new ArrayList<>();
        fields.addAll(dictionary.getAttributes(recordClass));
        fields.addAll(dictionary.getRelationships(recordClass));
        for (String field : fields) {
            Annotation fieldAnnotation = dictionary.getAttributeOrRelationAnnotation(recordClass,
                    ReadPermission.class, field);
            if (fieldAnnotation == null) {
                // no attribute to override this field
                continue;
            }
            FilterScope fieldFilterScope = loadChecks(fieldAnnotation, requestScope);
            if (fieldFilterScope.getUserPermission() != DENY) {
                return false;
            }
        }

        return true;
    }

    /**
     * Get a relationship type.
     *
     * @param relation Name of relationship
     * @return Relationship type. RelationshipType.NONE if not found.
     */
    public RelationshipType getRelationshipType(String relation) {
        return dictionary.getRelationshipType(obj, relation);
    }

    /**
     * Get the value for a particular attribute (i.e. non-relational field)
     * @param attr Attribute name
     * @return Object value for attribute
     */
    public Object getAttribute(String attr) {
        return this.getValue(attr);
    }

    /**
     * Wrapped Entity bean.
     *
     * @return bean object
     */
    public T getObject() {
        return obj;
    }

    /**
     * Sets object.
     * @param obj the obj
     */
    public void setObject(T obj) {
        this.obj = obj;
    }

    /**
     * Entity type.
     *
     * @return type resource class
     */
    @JsonIgnore
    public Class<T> getResourceClass() {
        return (Class) dictionary.lookupEntityClass(obj.getClass());
    }

    /**
     * Gets type.
     * @return the type
     */
    public String getType() {
        return type;
    }

    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        String id = dictionary.getId(getObject());
        result = prime * result + (uuid.isPresent() ? uuid.hashCode() : 0);
        result = prime * result + (id == null ? 0 : id.hashCode());
        result = prime * result + (type == null ? 0 : type.hashCode());
        return result;
    }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) {
            return true;
        }
        if (obj instanceof PersistentResource) {
            PersistentResource that = (PersistentResource) obj;
            String theirId = dictionary.getId(that.getObject());
            return this.matchesId(theirId) && Objects.equals(this.type, that.type);
        }
        return false;
    }

    /**
     * Gets lineage.
     * @return the lineage
     */
    public ResourceLineage getLineage() {
        return this.lineage;
    }

    /**
     * Gets dictionary.
     * @return the dictionary
     */
    public EntityDictionary getDictionary() {
        return dictionary;
    }

    /**
     * Gets request scope.
     * @return the request scope
     */
    public RequestScope getRequestScope() {
        return requestScope;
    }

    /**
     * Convert a persistent resource to a resource.
     *
     * @return a resource
     */
    public Resource toResource() {
        final Resource resource = new Resource(type,
                (obj == null) ? uuid.orElseThrow(() -> new InvalidEntityBodyException("No id found on object"))
                        : dictionary.getId(obj));
        resource.setRelationships(getRelationships());
        resource.setAttributes(getAttributes());
        return resource;
    }

    /**
     * Get relationship mappings.
     *
     * @return Relationship mapping
     */
    protected Map<String, Relationship> getRelationships() {
        final Map<String, Relationship> relationshipMap = new LinkedHashMap<>();
        final Set<String> relationshipFields = filterFields(ReadPermission.class, this,
                dictionary.getRelationships(obj));

        for (String field : relationshipFields) {
            Set<PersistentResource> relationships = getRelation(field);
            TreeMap<String, Resource> orderedById = new TreeMap<>(comparator);
            for (PersistentResource relationship : relationships) {
                orderedById.put(relationship.getId(),
                        new ResourceIdentifier(relationship.getType(), relationship.getId()).castToResource());

            }
            Collection<Resource> resources = orderedById.values();

            Data<Resource> data;
            RelationshipType relationshipType = getRelationshipType(field);
            if (relationshipType.isToOne()) {
                data = resources.isEmpty() ? new Data<>((Resource) null) : new Data<>(resources.iterator().next());
            } else {
                data = new Data<>(resources);
            }
            // TODO - links
            relationshipMap.put(field, new Relationship(null, data));
        }

        return relationshipMap;
    }

    /**
     * Get attributes mapping from entity.
     *
     * @return Mapping of attributes to objects
     */
    protected Map<String, Object> getAttributes() {
        final Map<String, Object> attributes = new LinkedHashMap<>();

        final Set<String> attrFields = filterFields(ReadPermission.class, this, dictionary.getAttributes(obj));
        for (String field : attrFields) {
            Object val = getAttribute(field);
            attributes.put(field, val);
        }
        return attributes;
    }

    /**
     * Sets value.
     * @param fieldName the field name
     * @param newValue the new value
     */
    protected void setValueChecked(String fieldName, Object newValue) {
        checkFieldAwarePermissions(UpdatePermission.class, fieldName);
        setValue(fieldName, newValue);
    }

    /**
     * Nulls the relationship or attribute and checks update permissions.
     * Invokes the set[fieldName] method on the target object OR set the field with the corresponding name.
     * @param fieldName the field name to set or invoke equivalent set method
     * @param oldValue the old value
     */
    protected void nullValue(String fieldName, PersistentResource oldValue) {
        if (oldValue == null) {
            return;
        }
        String inverseField = getInverseRelationField(fieldName);
        if (inverseField.length() > 0) {
            oldValue.checkFieldAwarePermissions(UpdatePermission.class, inverseField);
        }
        this.setValueChecked(fieldName, null);
    }

    /**
     * Gets a value from an entity and checks read permissions.
     * @param fieldName the field name
     * @return value value
     */
    protected Object getValue(String fieldName) {
        checkFieldAwarePermissions(ReadPermission.class, fieldName);
        return getValue(getObject(), fieldName, dictionary);
    }

    /**
     * Adds a new element to a collection and tests update permission.
     * @param collection the collection
     * @param collectionName the collection name
     * @param toAdd the to add
     */
    protected void addToCollection(Collection collection, String collectionName, PersistentResource toAdd) {
        checkFieldAwarePermissions(UpdatePermission.class, collectionName);
        toAdd.checkFieldAwarePermissions(ReadPermission.class, collectionName);

        if (collection == null) {
            collection = Collections.singleton(toAdd.getObject());
            this.setValueChecked(collectionName, collection);
        }

        collection.add(toAdd.getObject());
    }

    /**
     * Deletes an existing element in a collection and tests update and delete permissions.
     * @param collection the collection
     * @param collectionName the collection name
     * @param toDelete the to delete
     */
    protected void delFromCollection(Collection collection, String collectionName, PersistentResource toDelete) {

        if (collection == null) {
            return;
        }

        collection.remove(toDelete.getObject());
    }

    /**
     * Invoke the set[fieldName] method on the target object OR set the field with the corresponding name.
     * @param fieldName the field name to set or invoke equivalent set method
     * @param value the value to set
     */
    protected void setValue(String fieldName, Object value) {
        Class<?> targetClass = obj.getClass();
        try {
            Class<?> fieldClass = dictionary.getType(targetClass, fieldName);
            String realName = dictionary.getNameFromAlias(obj, fieldName);
            fieldName = (realName != null) ? realName : fieldName;
            String setMethod = "set" + WordUtils.capitalize(fieldName);
            Method method = EntityDictionary.findMethod(targetClass, setMethod, fieldClass);
            method.invoke(obj, coerce(value, fieldName, fieldClass));
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new InvalidAttributeException(fieldName, type);
        } catch (IllegalArgumentException | NoSuchMethodException noMethod) {
            try {
                Field field = targetClass.getField(fieldName);
                field.set(obj, coerce(value, fieldName, field.getType()));
            } catch (NoSuchFieldException | IllegalAccessException noField) {
                throw new InvalidAttributeException(fieldName, type);
            }
        }

        runTriggers(OnUpdate.class, fieldName);
        this.requestScope.queueCommitTrigger(this, fieldName);
    }

    <A extends Annotation> void runTriggers(Class<A> annotationClass) {
        runTriggers(annotationClass, "");
    }

    <A extends Annotation> void runTriggers(Class<A> annotationClass, String fieldName) {
        Class<?> targetClass = obj.getClass();

        Collection<Method> methods = dictionary.getTriggers(targetClass, annotationClass, fieldName);
        for (Method method : methods) {
            try {
                method.invoke(obj);
            } catch (ReflectiveOperationException e) {
                throw new IllegalArgumentException(e);
            }
        }
    }

    /**
     * Coerce provided value into expected class type.
     *
     * @param value provided value
     * @param fieldClass expected class type
     * @return coerced value
     */
    private Object coerce(Object value, String fieldName, Class<?> fieldClass) {
        if (fieldClass != null && Collection.class.isAssignableFrom(fieldClass) && value instanceof Collection) {
            return coerceCollection((Collection) value, fieldName, fieldClass);
        }

        if (fieldClass != null && Map.class.isAssignableFrom(fieldClass) && value instanceof Map) {
            return coerceMap((Map<?, ?>) value, fieldName, fieldClass);
        }

        return CoerceUtil.coerce(value, fieldClass);
    }

    private Collection coerceCollection(Collection<?> values, String fieldName, Class<?> fieldClass) {
        Class<?> providedType = dictionary.getParameterizedType(obj, fieldName);

        // check if collection is of and contains the correct types
        if (fieldClass.isAssignableFrom(values.getClass())) {
            boolean valid = true;
            for (Object member : values) {
                if (member != null && !providedType.isAssignableFrom(member.getClass())) {
                    valid = false;
                    break;
                }
            }
            if (valid) {
                return values;
            }
        }

        ArrayList<Object> list = new ArrayList<>(values.size());
        for (Object member : values) {
            list.add(CoerceUtil.coerce(member, providedType));
        }

        if (Set.class.isAssignableFrom(fieldClass)) {
            return new LinkedHashSet<>(list);
        }

        return list;
    }

    private Map coerceMap(Map<?, ?> values, String fieldName, Class<?> fieldClass) {
        Class<?> keyType = dictionary.getParameterizedType(obj, fieldName, 0);
        Class<?> valueType = dictionary.getParameterizedType(obj, fieldName, 1);

        // Verify the existing Map
        if (isValidParameterizedMap(values, keyType, valueType)) {
            return values;
        }

        LinkedHashMap<Object, Object> result = new LinkedHashMap<>(values.size());
        for (Map.Entry<?, ?> entry : values.entrySet()) {
            result.put(CoerceUtil.coerce(entry.getKey(), keyType), CoerceUtil.coerce(entry.getValue(), valueType));
        }

        return result;
    }

    private boolean isValidParameterizedMap(Map<?, ?> values, Class<?> keyType, Class<?> valueType) {
        for (Map.Entry<?, ?> entry : values.entrySet()) {
            Object key = entry.getKey();
            Object value = entry.getValue();
            if ((key != null && !keyType.isAssignableFrom(key.getClass()))
                    || (value != null && !valueType.isAssignableFrom(value.getClass()))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Invoke the get[fieldName] method on the target object OR get the field with the corresponding name.
     * @param target the object to get
     * @param fieldName the field name to get or invoke equivalent get method
     * @param dictionary the dictionary
     * @return the value
     */
    public static Object getValue(Object target, String fieldName, EntityDictionary dictionary) {
        AccessibleObject accessor = dictionary.getAccessibleObject(target, fieldName);
        try {
            if (accessor instanceof Method) {
                return ((Method) accessor).invoke(target);
            } else if (accessor instanceof Field) {
                return ((Field) accessor).get(target);
            }
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new InvalidAttributeException(fieldName, dictionary.getBinding(target.getClass()));
        }
        throw new InvalidAttributeException(fieldName, dictionary.getBinding(target.getClass()));
    }

    /**
     * If a bidirectional relationship exists, attempts to delete itself from the inverse
     * relationship. Given A to B as the relationship, A corresponds to this and B is the inverse.
     * @param relationName The name of the relationship on this (A) object.
     * @param inverseEntity The value (B) which has been deleted from this object.
     */
    protected void deleteInverseRelation(String relationName, Object inverseEntity) {
        String inverseRelationName = getInverseRelationField(relationName);

        if (!inverseRelationName.equals("")) {
            Class<?> inverseRelationType = dictionary.getType(inverseEntity.getClass(), inverseRelationName);

            PersistentResource inverseResource = new PersistentResource(this, inverseEntity, getRequestScope());
            Object inverseRelation = inverseResource.getValue(inverseRelationName);

            if (inverseRelation == null) {
                return;
            }

            if (inverseRelation instanceof Collection) {
                inverseResource.checkFieldAwarePermissions(UpdatePermission.class, inverseRelationName);
                inverseResource.delFromCollection((Collection) inverseRelation, inverseRelationName, this);
            } else if (inverseRelationType.equals(this.getResourceClass())) {
                inverseResource.nullValue(inverseRelationName, this);
            } else {
                throw new InternalServerErrorException("Relationship type mismatch");
            }
        }
    }

    private String getInverseRelationField(String relationName) {
        Class<?> entityClass = dictionary.lookupEntityClass(obj.getClass());
        return dictionary.getRelationInverse(entityClass, relationName);
    }

    /**
     * If a bidirectional relationship exists, attempts to add itself to the inverse
     * relationship. Given A to B as the relationship, A corresponds to this and B is the inverse.
     * @param relationName The name of the relationship on this (A) object.
     * @param relationValue The value (B) which has been added to this object.
     */
    protected void addInverseRelation(String relationName, Object relationValue) {
        Class<?> entityClass = dictionary.lookupEntityClass(obj.getClass());

        Object inverseEntity = relationValue; // Assigned to improve readability.
        String inverseRelationName = dictionary.getRelationInverse(entityClass, relationName);

        if (!inverseRelationName.equals("")) {
            Class<?> inverseRelationType = dictionary.getType(inverseEntity.getClass(), inverseRelationName);

            PersistentResource inverseResource = new PersistentResource(this, inverseEntity, getRequestScope());
            Object inverseRelation = inverseResource.getValue(inverseRelationName);

            if (Collection.class.isAssignableFrom(inverseRelationType)) {
                if (inverseRelation != null) {
                    inverseResource.addToCollection((Collection) inverseRelation, inverseRelationName, this);
                } else {
                    inverseResource.setValueChecked(inverseRelationName, Collections.singleton(this.getObject()));
                }
            } else if (inverseRelationType.equals(this.getResourceClass())) {
                inverseResource.setValueChecked(inverseRelationName, this.getObject());
            } else {
                throw new InternalServerErrorException("Relationship type mismatch");
            }
        }
    }

    /**
     * Filter a set of PersistentResources.
     *
     * @param <A> the type parameter
     * @param <T> the type parameter
     * @param permission the permission
     * @param resources  the resources
     * @return Filtered set of resources
     */
    protected static <A extends Annotation, T> Set<PersistentResource<T>> filter(Class<A> permission,
            Set<PersistentResource<T>> resources) {
        Set<PersistentResource<T>> filteredSet = new LinkedHashSet<>();
        for (PersistentResource<T> resource : resources) {
            try {
                resource.checkFieldAwarePermissions(permission);
                filteredSet.add(resource);
            } catch (ForbiddenAccessException e) {
                // Do nothing. Filter from set.
            }
        }
        // keep original SingleElementSet
        if (resources instanceof SingleElementSet && resources.equals(filteredSet)) {
            return resources;
        }
        return filteredSet;
    }

    /**
     * Filter a set of fields.
     *
     * @param <A> the type parameter
     * @param permission the permission
     * @param resource the resource
     * @param fields the fields
     * @return Filtered set of fields
     */
    protected static <A extends Annotation> Set<String> filterFields(Class<A> permission,
            PersistentResource resource, Collection<String> fields) {
        Set<String> filteredSet = new LinkedHashSet<>();
        // Hack: doNotDefer is a special flag to temporarily disable deferred checking. Presumably, this check
        // should not be running if it needs to be deferred (in which case, deferred checks would also be executing)
        // We should probably find a cleaner way to do this.
        boolean save = resource.getRequestScope().isNotDeferred();
        try {
            resource.getRequestScope().setNotDeferred(true);
            for (String field : fields) {
                try {
                    if (checkIncludeSparseField(resource.getRequestScope().getSparseFields(), resource.type,
                            field)) {
                        resource.checkFieldAwarePermissions(permission, field);
                        filteredSet.add(field);
                    }
                } catch (ForbiddenAccessException e) {
                    // Do nothing. Filter from set.
                }
            }
        } finally {
            resource.getRequestScope().setNotDeferred(save);
        }
        return filteredSet;
    }

    /**
     * Check provided access permission.
     *
     * @param annotationClass one of Create, Read, Update or Delete permission annotations
     * @param annotation the instance of the annotation
     * @param resource given resource
     * @see com.yahoo.elide.annotation.CreatePermission
     * @see com.yahoo.elide.annotation.ReadPermission
     * @see com.yahoo.elide.annotation.UpdatePermission
     * @see com.yahoo.elide.annotation.DeletePermission
     */
    static <A extends Annotation> void checkPermission(Class<A> annotationClass, A annotation,
            PersistentResource resource) {
        if (resource.getRequestScope().getSecurityMode() == SecurityMode.SECURITY_INACTIVE) {
            return;
        }
        Class<? extends Check>[] anyChecks;
        Class<? extends Check>[] allChecks;
        try {
            anyChecks = (Class<? extends Check>[]) annotationClass.getMethod("any").invoke(annotation,
                    (Object[]) null);
            allChecks = (Class<? extends Check>[]) annotationClass.getMethod("all").invoke(annotation,
                    (Object[]) null);
        } catch (ReflectiveOperationException e) {
            throw new InvalidSyntaxException("Unknown permission " + annotationClass.getName(), e);
        }

        if (anyChecks.length > 0) {
            resource.requestScope.checkPermissions(annotationClass, anyChecks, ANY, resource);
        } else if (allChecks.length > 0) {
            resource.requestScope.checkPermissions(annotationClass, allChecks, ALL, resource);
        } else {
            throw new InvalidSyntaxException("Unknown permission " + annotationClass.getName());
        }
    }

    static <A extends Annotation> FilterScope loadChecks(A annotation, RequestScope requestScope) {
        if (annotation == null) {
            return new FilterScope(requestScope);
        }

        Class<? extends Check>[] anyChecks;
        Class<? extends Check>[] allChecks;
        Class<? extends Annotation> annotationClass = annotation.getClass();
        try {
            anyChecks = (Class<? extends Check>[]) annotationClass.getMethod("any").invoke(annotation,
                    (Object[]) null);
            allChecks = (Class<? extends Check>[]) annotationClass.getMethod("all").invoke(annotation,
                    (Object[]) null);
        } catch (ReflectiveOperationException e) {
            throw new InvalidSyntaxException("Unknown permission " + annotationClass.getName(), e);
        }

        if (anyChecks.length > 0) {
            return new FilterScope(requestScope, ANY, anyChecks);
        } else if (allChecks.length > 0) {
            return new FilterScope(requestScope, ALL, allChecks);
        } else {
            throw new InvalidSyntaxException("Unknown permission " + annotationClass.getName());
        }
    }

    /**
     * Check provided access permission.
     *
     * @param annotationClass one of Create, Read, Update or Delete permission annotations
     * @param resource given resource
     * @see com.yahoo.elide.annotation.CreatePermission
     * @see com.yahoo.elide.annotation.ReadPermission
     * @see com.yahoo.elide.annotation.UpdatePermission
     * @see com.yahoo.elide.annotation.DeletePermission
     */
    static <A extends Annotation> void checkPermission(Class<A> annotationClass, PersistentResource resource) {
        A annotation = resource.getDictionary().getAnnotation(resource, annotationClass);
        if (annotation == null) {
            return;
        }
        checkPermission(annotationClass, annotation, resource);
    }

    private <A extends Annotation> void checkFieldAwarePermissions(Class<A> annotationClass) {
        requestScope.checkFieldAwarePermissions(annotationClass, this);
    }

    private <A extends Annotation> void checkFieldAwarePermissions(Class<A> annotationClass, String fieldName) {
        requestScope.checkFieldAwarePermissions(annotationClass, this, fieldName);
    }

    /**
     * Execute a set of permission checks.
     * @param checks Array of Check annotations
     * @param mode true if ANY, else ALL
     * @param resource provided PersistentResource to check
     */
    static void checkPermissions(Class<? extends Check>[] checks, boolean mode, PersistentResource resource) {
        for (Class<? extends Check> check : checks) {
            Check checkHandler;
            try {
                checkHandler = check.newInstance();
            } catch (InstantiationException | IllegalAccessException e) {
                throw new InvalidSyntaxException("Illegal permission check " + check.getName(), e);
            }
            boolean ok = resource.getRequestScope().getUser().ok(checkHandler, resource);

            if (ok && mode == ANY) {
                return;
            }

            if (!ok && mode == ALL) {
                String message = String.format("ForbiddenAccess %s %s#%s", check, resource.getType(),
                        resource.getId());
                log.debug(message);
                throw new ForbiddenAccessException(message);
            }
        }
        if (mode == ANY) {
            String message = String.format("ForbiddenAccess %s %s#%s", Arrays.asList(checks), resource.getType(),
                    resource.getId());
            log.debug(message);
            throw new ForbiddenAccessException(message);
        }
    }

    /**
     * Check a permission on a field.
     * @param <A> the type parameter
     * @param annotationClass the annotation class
     * @param resource the resource
     * @param fieldName the field name
     */
    protected static <A extends Annotation> void checkFieldPermission(Class<A> annotationClass,
            PersistentResource resource, String fieldName) {
        A annotation = resource.getDictionary().getAttributeOrRelationAnnotation(resource.getResourceClass(),
                annotationClass, fieldName);
        if (annotation == null) {
            return;
        }

        checkPermission(annotationClass, annotation, resource);
    }

    /**
     * Check a field permission if it exists. Otherwise, forbidden.
     *
     * @param <A> the type parameter
     * @param annotationClass the annotation class
     * @param resource the resource
     * @param fieldName the field name
     */
    protected static <A extends Annotation> void checkFieldPermissionIfExists(Class<A> annotationClass,
            PersistentResource resource, String fieldName) {
        A annotation = resource.getDictionary().getAttributeOrRelationAnnotation(resource.getResourceClass(),
                annotationClass, fieldName);
        if (annotation == null) {
            throw new ForbiddenAccessException("Unable to find " + annotationClass.getSimpleName()
                    + " annotation for " + resource.getResourceClass().getSimpleName() + "#" + fieldName);
        }

        checkFieldPermission(annotationClass, resource, fieldName);
    }

    protected static boolean checkIncludeSparseField(Map<String, Set<String>> sparseFields, String type,
            String fieldName) {
        if (!sparseFields.isEmpty()) {
            if (!sparseFields.containsKey(type)) {
                return false;
            }

            if (!sparseFields.get(type).contains(fieldName)) {
                return false;
            }
        }

        return true;
    }

    /**
     * Audit an action on a field.
     * @param fieldName the field name
     */
    protected void audit(String fieldName) {
        Audit[] annotations = dictionary.getAttributeOrRelationAnnotations(getResourceClass(), Audit.class,
                fieldName);

        if (annotations == null) {
            return;
        }
        for (Audit annotation : annotations) {
            if (annotation.action() == Audit.Action.UPDATE) {
                LogMessage message = new LogMessage(annotation, this);
                getRequestScope().getLogger().log(message);
            } else {
                throw new InvalidSyntaxException("Only Audit.Action.UPDATE is allowed on fields.");
            }
        }
    }

    /**
     * Audit an action on an entity.
     *
     * @param action the action
     */
    protected void audit(Audit.Action action) {
        Audit[] annotations = getResourceClass().getAnnotationsByType(Audit.class);

        if (annotations == null) {
            return;
        }
        for (Audit annotation : annotations) {
            if (annotation.action() == action) {
                LogMessage message = new LogMessage(annotation, this);
                getRequestScope().getLogger().log(message);
            }
        }
    }

    /**
     * Helper function for access to OpaqueUser in checks.
     * @return opaque user
     */
    public Object getOpaqueUser() {
        if (getRequestScope().getUser() == null) {
            return null;
        }

        return getRequestScope().getUser().getOpaqueUser();
    }
}