
com.yahoo.elide.core.PersistentResource Maven / Gradle / Ivy
/*
* Copyright 2018, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.core;
import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.CREATE;
import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.DELETE;
import static com.yahoo.elide.annotation.LifeCycleHookBinding.Operation.UPDATE;
import static com.yahoo.elide.core.dictionary.EntityBinding.EMPTY_BINDING;
import static com.yahoo.elide.core.dictionary.EntityDictionary.getType;
import static com.yahoo.elide.core.type.ClassType.COLLECTION_TYPE;
import com.yahoo.elide.annotation.Audit;
import com.yahoo.elide.annotation.CreatePermission;
import com.yahoo.elide.annotation.DeletePermission;
import com.yahoo.elide.annotation.LifeCycleHookBinding;
import com.yahoo.elide.annotation.NonTransferable;
import com.yahoo.elide.annotation.ReadPermission;
import com.yahoo.elide.annotation.UpdatePermission;
import com.yahoo.elide.core.audit.InvalidSyntaxException;
import com.yahoo.elide.core.audit.LogMessage;
import com.yahoo.elide.core.audit.LogMessageImpl;
import com.yahoo.elide.core.datastore.DataStoreIterable;
import com.yahoo.elide.core.datastore.DataStoreTransaction;
import com.yahoo.elide.core.dictionary.EntityBinding;
import com.yahoo.elide.core.dictionary.EntityDictionary;
import com.yahoo.elide.core.dictionary.RelationshipType;
import com.yahoo.elide.core.exceptions.BadRequestException;
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.exceptions.InvalidValueException;
import com.yahoo.elide.core.filter.expression.AndFilterExpression;
import com.yahoo.elide.core.filter.expression.FilterExpression;
import com.yahoo.elide.core.filter.predicates.InPredicate;
import com.yahoo.elide.core.filter.visitors.VerifyFieldAccessFilterExpressionVisitor;
import com.yahoo.elide.core.request.Argument;
import com.yahoo.elide.core.request.Attribute;
import com.yahoo.elide.core.request.EntityProjection;
import com.yahoo.elide.core.request.Pagination;
import com.yahoo.elide.core.request.Sorting;
import com.yahoo.elide.core.security.ChangeSpec;
import com.yahoo.elide.core.security.obfuscation.IdObfuscator;
import com.yahoo.elide.core.security.permissions.ExpressionResult;
import com.yahoo.elide.core.security.visitors.CanPaginateVisitor;
import com.yahoo.elide.core.type.ClassType;
import com.yahoo.elide.core.type.Type;
import com.yahoo.elide.core.utils.coerce.CoerceUtil;
import com.yahoo.elide.jsonapi.JsonApiSettings;
import com.yahoo.elide.jsonapi.document.processors.WithMetadata;
import com.yahoo.elide.jsonapi.models.Data;
import com.yahoo.elide.jsonapi.models.Meta;
import com.yahoo.elide.jsonapi.models.Relationship;
import com.yahoo.elide.jsonapi.models.Resource;
import com.yahoo.elide.jsonapi.models.ResourceIdentifier;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.collect.Sets;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IterableUtils;
import org.apache.commons.lang3.StringUtils;
import lombok.NonNull;
import reactor.core.publisher.Flux;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
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 java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* Resource wrapper around Entity bean.
*
* @param type of resource
*/
public class PersistentResource implements com.yahoo.elide.core.security.PersistentResource {
public static final Set ALL_FIELDS = null;
public static final String CLASS_NO_FIELD = "";
/**
* The Dictionary.
*/
protected final EntityDictionary dictionary;
private final Type type;
private final String typeName;
private final ResourceLineage lineage;
private final Optional uuid;
private final DataStoreTransaction transaction;
private final RequestScope requestScope;
/* Sort strings first by length then contents */
private final Comparator lengthFirstComparator = (string1, string2) -> {
int diff = string1.length() - string2.length();
return diff == 0 ? string1.compareTo(string2) : diff;
};
protected T obj;
private int hashCode = 0;
/**
* Construct a new resource from the ID provided.
*
* @param obj the obj
* @param parent the parent
* @param id the id
* @param parentRelationship The parent relationship traversed to this resource.
* @param scope the request scope
*/
public PersistentResource(
@NonNull T obj,
PersistentResource parent,
String parentRelationship,
String id,
@NonNull RequestScope scope
) {
this.obj = obj;
this.type = getType(obj);
this.uuid = Optional.ofNullable(id);
this.lineage = parent != null
? new ResourceLineage(parent.lineage, parent, parentRelationship)
: new ResourceLineage();
this.dictionary = scope.getDictionary();
this.typeName = dictionary.getJsonAliasFor(type);
this.transaction = scope.getTransaction();
this.requestScope = scope;
dictionary.initializeEntity(obj);
}
/**
* Construct a new resource from the ID provided.
*
* @param obj the obj
* @param id the id
* @param scope the request scope
*/
public PersistentResource(
@NonNull T obj,
String id,
@NonNull RequestScope scope
) {
this(obj, null, null, id, scope);
}
/**
* Create a resource in the database.
*
* @param entityClass the entity class
* @param requestScope the request scope
* @param uuid the (optional) uuid
* @param object type
* @return persistent resource
*/
public static PersistentResource createObject(
Type entityClass,
RequestScope requestScope,
Optional uuid) {
return createObject(null, null, entityClass, requestScope, uuid);
}
/**
* Create a resource in the database.
*
* @param parent - The immediate ancestor in the lineage or null if this is a root.
* @param parentRelationship - The name of the parent relationship traversed to create this object.
* @param entityClass the entity class
* @param requestScope the request scope
* @param uuid the (optional) uuid
* @param object type
* @return persistent resource
*/
public static PersistentResource createObject(
PersistentResource> parent,
String parentRelationship,
Type entityClass,
RequestScope requestScope,
Optional uuid) {
T obj = requestScope.getTransaction().createNewObject(entityClass, requestScope);
String id = uuid.orElse(null);
PersistentResource newResource = new PersistentResource<>(obj, parent, parentRelationship, id, requestScope);
//The ID must be assigned before we add it to the new resources set. Persistent resource
//hashcode and equals are only based on the ID/UUID & type.
assignId(newResource, id);
// Keep track of new resources for non-transferable resources
requestScope.getNewPersistentResources().add(newResource);
checkPermission(CreatePermission.class, newResource);
newResource.auditClass(Audit.Action.CREATE, new ChangeSpec(newResource, null, null, newResource.getObject()));
requestScope.publishLifecycleEvent(newResource, CREATE);
requestScope.setUUIDForObject(newResource.type, id, newResource.getObject());
// Initialize null ToMany collections
requestScope.getDictionary().getRelationships(entityClass).stream()
.filter(relationName -> newResource.getRelationshipType(relationName).isToMany()
&& newResource.getValueUnchecked(relationName) == null)
.forEach(relationName -> newResource.setValue(relationName, new LinkedHashSet<>()));
newResource.markDirty();
return newResource;
}
/**
* Load an single entity from the DB.
*
* @param projection What to load from the DB.
* @param id the id
* @param requestScope the request scope
* @param type of resource
* @return resource persistent resource
* @throws InvalidObjectIdentifierException the invalid object identifier exception
*/
@SuppressWarnings("resource")
@NonNull
public static PersistentResource loadRecord(
EntityProjection projection,
String id,
RequestScope requestScope
) throws InvalidObjectIdentifierException {
Preconditions.checkNotNull(projection);
Preconditions.checkNotNull(id);
Preconditions.checkNotNull(requestScope);
DataStoreTransaction tx = requestScope.getTransaction();
EntityDictionary dictionary = requestScope.getDictionary();
Type> loadClass = projection.getType();
// Check the resource cache if exists
Object obj = requestScope.getObjectById(loadClass, id);
if (obj == null) {
// try to load object
Optional permissionFilter = getPermissionFilterExpression(loadClass,
requestScope, projection.getRequestedFields());
projection = projection
.copyOf()
.filterExpression(permissionFilter.orElse(null))
.build();
Serializable idOrEntityId;
Type> entityIdType = dictionary.getEntityIdType(loadClass);
if (entityIdType != null) {
// If it is by entity id use it
idOrEntityId = (Serializable) CoerceUtil.coerce(id, entityIdType);
} else {
Type> idType = dictionary.getIdType(loadClass);
IdObfuscator idObfuscator = dictionary.getIdObfuscator();
if (idObfuscator != null) {
// If an obfuscator is present use it to deobfuscate the id
try {
idOrEntityId = (Serializable) idObfuscator.deobfuscate(id, idType);
} catch (RuntimeException e) {
throw new InvalidValueException(
"Invalid identifier " + id + " for " + dictionary.getJsonAliasFor(loadClass), e);
}
} else {
idOrEntityId = (Serializable) CoerceUtil.coerce(id, idType);
}
}
obj = tx.loadObject(projection, idOrEntityId, requestScope);
if (obj == null) {
throw new InvalidObjectIdentifierException(id, dictionary.getJsonAliasFor(loadClass));
}
}
PersistentResource resource = new PersistentResource<>(
(T) obj,
requestScope.getUUIDFor(obj),
requestScope);
// No need to have read access for a newly created object
if (!requestScope.getNewResources().contains(resource)) {
resource.checkFieldAwarePermissions(ReadPermission.class, projection.getRequestedFields());
}
return resource;
}
/**
* Get a FilterExpression parsed from FilterExpressionCheck.
*
* @param the type parameter
* @param loadClass the load class
* @param requestScope the request scope
* @param requestedFields The set of requested fields
* @return a FilterExpression defined by FilterExpressionCheck.
*/
private static Optional getPermissionFilterExpression(Type loadClass,
RequestScope requestScope,
Set requestedFields) {
try {
return requestScope.getPermissionExecutor().getReadPermissionFilter(loadClass, requestedFields);
} catch (ForbiddenAccessException e) {
return Optional.empty();
}
}
/**
* Load a collection from the datastore.
*
* @param projection the projection to load
* @param requestScope the request scope
* @param ids a list of object identifiers to optionally load. Can be empty.
* @return a filtered collection of resources loaded from the datastore.
*/
public static Flux loadRecords(
EntityProjection projection,
List ids,
RequestScope requestScope) {
Type> loadClass = projection.getType();
Pagination pagination = projection.getPagination();
Sorting sorting = projection.getSorting();
FilterExpression filterExpression = projection.getFilterExpression();
EntityDictionary dictionary = requestScope.getDictionary();
DataStoreTransaction tx = requestScope.getTransaction();
if (shouldSkipCollection(loadClass, ReadPermission.class, requestScope, projection.getRequestedFields())) {
if (ids.isEmpty()) {
return Flux.empty();
}
throw new InvalidObjectIdentifierException(ids.toString(), dictionary.getJsonAliasFor(loadClass));
}
Set requestedFields = projection.getRequestedFields();
if (pagination != null && !pagination.isDefaultInstance()
&& !CanPaginateVisitor.canPaginate(loadClass, dictionary, requestScope, requestedFields)) {
throw new BadRequestException(String.format("Cannot paginate %s",
dictionary.getJsonAliasFor(loadClass)));
}
Set newResources = new LinkedHashSet<>();
if (!ids.isEmpty()) {
String typeAlias = dictionary.getJsonAliasFor(loadClass);
newResources = requestScope.getNewPersistentResources().stream()
.filter(resource -> typeAlias.equals(resource.getTypeName())
&& ids.contains(resource.getUUID().orElse("")))
.collect(Collectors.toCollection(LinkedHashSet::new));
FilterExpression idExpression = buildIdFilterExpression(ids, loadClass, dictionary, requestScope);
// Combine filters if necessary
filterExpression = Optional.ofNullable(filterExpression)
.map(fe -> (FilterExpression) new AndFilterExpression(idExpression, fe))
.orElse(idExpression);
}
Optional permissionFilter = getPermissionFilterExpression(loadClass, requestScope,
requestedFields);
if (permissionFilter.isPresent()) {
if (filterExpression != null) {
filterExpression = new AndFilterExpression(filterExpression, permissionFilter.get());
} else {
filterExpression = permissionFilter.get();
}
}
EntityProjection modifiedProjection = projection
.copyOf()
.filterExpression(filterExpression)
.sorting(sorting)
.pagination(pagination)
.build();
Flux existingResources = filter(
ReadPermission.class,
Optional.ofNullable(modifiedProjection.getFilterExpression()),
projection.getRequestedFields(),
Flux.fromIterable(
new PersistentResourceSet(tx.loadObjects(modifiedProjection, requestScope), requestScope))
);
// TODO: Sort again in memory now that two sets are glommed together?
Flux allResources =
Flux.fromIterable(newResources).mergeWith(existingResources);
Set foundIds = new LinkedHashSet<>();
allResources = allResources.doOnNext((resource) -> {
String id = (String) resource.getUUID().orElseGet(resource::getId);
if (ids.contains(id)) {
foundIds.add(id);
}
});
allResources = allResources.doOnComplete(() -> {
Set missedIds = Sets.difference(new LinkedHashSet<>(ids), foundIds);
if (!missedIds.isEmpty()) {
throw new InvalidObjectIdentifierException(missedIds.toString(), dictionary.getJsonAliasFor(loadClass));
}
});
return allResources;
}
/**
* Build an id filter expression for a particular entity type.
*
* @param ids Ids to include in the filter expression
* @param entityType Type of entity
* @return Filter expression for given ids and type.
*/
private static FilterExpression buildIdFilterExpression(List ids,
Type> entityType,
EntityDictionary dictionary,
RequestScope scope) {
Type> entityIdType = dictionary.getEntityIdType(entityType);
Type> idType;
String idField;
if (entityIdType != null) {
idType = entityIdType;
idField = dictionary.getEntityIdFieldName(entityType);
} else {
idType = dictionary.getIdType(entityType);
idField = dictionary.getIdFieldName(entityType);
}
IdObfuscator idObfuscator = entityIdType != null ? null : dictionary.getIdObfuscator();
List
© 2015 - 2025 Weber Informatics LLC | Privacy Policy