Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.yahoo.elide.graphql.PersistentResourceFetcher Maven / Gradle / Ivy
/*
* Copyright 2017, Yahoo Inc.
* Licensed under the Apache License, Version 2.0
* See LICENSE file in project root for terms.
*/
package com.yahoo.elide.graphql;
import static com.yahoo.elide.graphql.ModelBuilder.ARGUMENT_OPERATION;
import com.yahoo.elide.core.PersistentResource;
import com.yahoo.elide.core.RequestScope;
import com.yahoo.elide.core.dictionary.EntityDictionary;
import com.yahoo.elide.core.exceptions.BadRequestException;
import com.yahoo.elide.core.exceptions.InvalidObjectIdentifierException;
import com.yahoo.elide.core.exceptions.InvalidValueException;
import com.yahoo.elide.core.request.EntityProjection;
import com.yahoo.elide.core.request.Relationship;
import com.yahoo.elide.core.type.ClassType;
import com.yahoo.elide.core.type.Type;
import com.yahoo.elide.graphql.containers.ConnectionContainer;
import com.yahoo.elide.graphql.containers.MapEntryContainer;
import com.google.common.collect.Sets;
import org.apache.commons.collections4.CollectionUtils;
import graphql.language.Field;
import graphql.language.FragmentSpread;
import graphql.schema.DataFetcher;
import graphql.schema.DataFetchingEnvironment;
import graphql.schema.GraphQLType;
import io.reactivex.Observable;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Queue;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.constraints.NotNull;
/**
* Invoked by GraphQL Java to fetch/mutate data from Elide.
*/
@Slf4j
public class PersistentResourceFetcher implements DataFetcher {
@Getter
private final NonEntityDictionary nonEntityDictionary;
public PersistentResourceFetcher(NonEntityDictionary nonEntityDictionary) {
this.nonEntityDictionary = nonEntityDictionary;
}
/**
* Override graphql-java's {@link DataFetcher} get method to execute
* the mutation and return some sensible output values.
* @param environment Graphql-java's execution environment
* @return a collection of {@link PersistentResource} objects
*/
@Override
public Object get(DataFetchingEnvironment environment) {
/* fetch arguments in mutation/query */
Map args = environment.getArguments();
/* fetch current operation */
RelationshipOp operation = (RelationshipOp) args.getOrDefault(ARGUMENT_OPERATION, RelationshipOp.FETCH);
/* build environment object, extracts required fields */
Environment context = new Environment(environment);
/* safe enable debugging */
if (log.isDebugEnabled()) {
logContext(operation, context);
}
/* sanity check for pagination/filtering/sorting arguments w any operation other than FETCH */
if (operation != RelationshipOp.FETCH) {
filterSortPaginateSanityCheck(context);
}
/* delegate request */
switch (operation) {
case FETCH:
return fetchObjects(context);
case UPSERT:
return upsertObjects(context);
case UPDATE:
return updateObjects(context);
case DELETE:
return deleteObjects(context);
case REMOVE:
return removeObjects(context);
case REPLACE:
return replaceObjects(context);
default:
throw new UnsupportedOperationException("Unknown operation: " + operation);
}
}
/**
* Checks whether sort/filter/pagination params are passed w unsupported operation.
* @param environment Environment encapsulating graphQL's request environment
*/
private void filterSortPaginateSanityCheck(Environment environment) {
if (environment.filters.isPresent() || environment.sort.isPresent() || environment.offset.isPresent()
|| environment.first.isPresent()) {
throw new BadRequestException("Pagination/Filtering/Sorting is only supported with FETCH operation");
}
}
/**
* log current context for debugging.
* @param operation Current operation
* @param environment Environment encapsulating graphQL's request environment
*/
private void logContext(RelationshipOp operation, Environment environment) {
List> children = (environment.field.getSelectionSet() != null)
? (List) environment.field.getSelectionSet().getChildren()
: new ArrayList<>();
List fieldName = new ArrayList<>();
if (CollectionUtils.isNotEmpty(children)) {
children.stream().forEach(i -> {
if (i.getClass().equals(Field.class)) {
fieldName.add(((Field) i).getName());
} else if (i.getClass().equals(FragmentSpread.class)) {
fieldName.add(((FragmentSpread) i).getName());
} else {
log.debug("A new type of Selection, other than Field and FragmentSpread was encountered, {}",
i.getClass());
}
});
}
String requestedFields = environment.field.getName() + fieldName;
GraphQLType parent = environment.parentType;
if (log.isDebugEnabled()) {
log.debug("{} {} fields with parent {}<{}>", operation, requestedFields,
EntityDictionary.getSimpleName(EntityDictionary.getType(parent)), parent.getName());
}
}
/**
* handle FETCH operation.
* @param context Environment encapsulating graphQL's request environment
* @return list of {@link PersistentResource} objects
*/
private Object fetchObjects(Environment context) {
/* sanity check for data argument w FETCH */
if (context.data.isPresent()) {
throw new BadRequestException("FETCH must not include data");
}
// Process fetch object for this container
return context.container.processFetch(context, this);
}
/**
* Fetches a root-level entity.
* @param requestScope Request scope
* @param projection constructed entityProjection for a class
* @param ids List of ids (can be NULL)
* @return {@link PersistentResource} object(s)
*/
public ConnectionContainer fetchObject(
RequestScope requestScope,
EntityProjection projection,
Optional> ids
) {
EntityDictionary dictionary = requestScope.getDictionary();
String typeName = dictionary.getJsonAliasFor(projection.getType());
/* fetching a collection */
Observable records = ids.map((idList) -> {
/* handle empty list of ids */
if (idList.isEmpty()) {
throw new BadRequestException("Empty list passed to ids");
}
return PersistentResource.loadRecords(projection, idList, requestScope);
}).orElseGet(() -> PersistentResource.loadRecords(projection, new ArrayList<>(), requestScope));
return new ConnectionContainer(records.toList(LinkedHashSet::new).blockingGet(),
Optional.ofNullable(projection.getPagination()), typeName);
}
/**
* Fetches a relationship for a top-level entity.
*
* @param parentResource Parent object
* @param relationship constructed relationship object with entityProjection
* @param ids List of ids
* @return persistence resource object(s)
*/
public Object fetchRelationship(
PersistentResource> parentResource,
@NotNull Relationship relationship,
Optional> ids
) {
EntityDictionary dictionary = parentResource.getRequestScope().getDictionary();
Type relationshipClass = dictionary.getParameterizedType(parentResource.getObject(), relationship.getName());
String relationshipType = dictionary.getJsonAliasFor(relationshipClass);
Set relationResources;
if (ids.isPresent()) {
relationResources =
parentResource.getRelation(ids.get(), relationship).toList(LinkedHashSet::new).blockingGet();
} else {
relationResources =
parentResource.getRelationCheckedFiltered(relationship).toList(LinkedHashSet::new).blockingGet();
}
return new ConnectionContainer(
relationResources,
Optional.ofNullable(relationship.getProjection().getPagination()),
relationshipType);
}
private ConnectionContainer upsertObjects(Environment context) {
return upsertOrUpdateObjects(
context,
this::upsertObject,
RelationshipOp.UPSERT);
}
private ConnectionContainer updateObjects(Environment context) {
return upsertOrUpdateObjects(
context,
this::updateObject,
RelationshipOp.UPDATE);
}
/**
* handle UPSERT or UPDATE operation.
* @param context Environment encapsulating graphQL's request environment
* @param updateFunc controls the behavior of how the update (or upsert) is performed.
* @return Connection object.
*/
private ConnectionContainer upsertOrUpdateObjects(
Environment context,
Executor> updateFunc,
RelationshipOp operation
) {
/* sanity check for id and data argument w UPSERT/UPDATE */
if (context.ids.isPresent()) {
throw new BadRequestException(operation + " must not include ids");
}
if (!context.data.isPresent()) {
throw new BadRequestException(operation + " must include data argument");
}
Type> entityClass;
EntityDictionary dictionary = context.requestScope.getDictionary();
if (context.isRoot()) {
entityClass = dictionary.getEntityClass(context.field.getName(), context.requestScope.getApiVersion());
} else {
assert context.parentResource != null;
entityClass = dictionary.getParameterizedType(
context.parentResource.getResourceType(),
context.field.getName());
}
/* form entities */
Optional parentEntity;
if (!context.isRoot()) {
assert context.parentResource != null;
parentEntity = Optional.of(new Entity(
Optional.empty(),
null,
context.parentResource.getResourceType(),
context.requestScope));
} else {
parentEntity = Optional.empty();
}
LinkedHashSet entitySet = new LinkedHashSet<>();
for (Map input : context.data.orElseThrow(IllegalStateException::new)) {
entitySet.add(new Entity(parentEntity, input, entityClass, context.requestScope));
}
/* apply function to upsert/update the object */
for (Entity entity : entitySet) {
graphWalker(entity, updateFunc, context);
}
/* fixup relationships */
for (Entity entity : entitySet) {
graphWalker(entity, this::updateRelationship, context);
PersistentResource> childResource = entity.toPersistentResource();
if (!context.isRoot()) {
/* add relation between parent and nested entity */
assert context.parentResource != null;
context.parentResource.addRelation(context.field.getName(), childResource);
}
}
String entityName = dictionary.getJsonAliasFor(entityClass);
Set resources = entitySet.stream()
.map(Entity::toPersistentResource)
.collect(Collectors.toCollection(LinkedHashSet::new));
return new ConnectionContainer(resources, Optional.empty(), entityName);
}
/**
* A function to handle upserting (update/create) objects.
* @param The return type of the function.
*/
@FunctionalInterface
private interface Executor {
/**
* Execute a function on the current entity with the current context.
* @param entity The current entity.
* @param context The request context.
* @return Depends on the function.
*/
T execute(Entity entity, Environment context);
}
/**
* Forms the graph from data {@param input} and executes a function {@param function} on all the nodes.
* @param entity Resource entity
* @param function Function to process nodes
* @param context the request context
* @return set of {@link PersistentResource} objects
*/
private void graphWalker(Entity entity, Executor> function, Environment context) {
Queue toVisit = new ArrayDeque<>();
Set visited = new LinkedHashSet<>();
toVisit.add(entity);
while (!toVisit.isEmpty()) {
Entity currentEntity = toVisit.remove();
if (visited.contains(currentEntity)) {
continue;
}
visited.add(currentEntity);
function.execute(currentEntity, context);
Set relationshipEntities = currentEntity.getRelationships();
/* loop over relationships */
for (Entity.Relationship relationship : relationshipEntities) {
toVisit.addAll(relationship.getValue());
}
}
}
/**
* update the relationship between {@param parent} and the resource loaded by given {@param id}.
* @param entity Resource entity
* @return {@link PersistentResource} object
*/
private PersistentResource> updateRelationship(Entity entity, Environment context) {
Set relationshipEntities = entity.getRelationships();
PersistentResource> resource = entity.toPersistentResource();
Set toUpdate;
/* loop over each relationship */
for (Entity.Relationship relationship : relationshipEntities) {
toUpdate = new LinkedHashSet<>();
for (Entity relation : relationship.getValue()) {
toUpdate.add(relation.toPersistentResource());
}
resource.updateRelation(relationship.getName(), toUpdate);
}
return resource;
}
/**
* updates or creates existing/new entities.
* @param entity Resource entity
* @param context The request context
* @return {@link PersistentResource} object
*/
private PersistentResource upsertObject(Entity entity, Environment context) {
Set attributes = entity.getAttributes();
Optional id = entity.getId();
RequestScope requestScope = entity.getRequestScope();
PersistentResource upsertedResource;
EntityDictionary dictionary = requestScope.getDictionary();
PersistentResource parentResource = entity.getParentResource().map(Entity::toPersistentResource).orElse(null);
if (!id.isPresent()) {
//If the ID is generated, it is safe to assign a temporary UUID. Otherwise the client must provide one.
if (dictionary.isIdGenerated(entity.getEntityClass())) {
entity.setId(); //Assign a temporary UUID.
id = entity.getId();
}
upsertedResource = PersistentResource.createObject(
parentResource,
context.field.getName(),
entity.getEntityClass(),
requestScope, id);
} else {
try {
Set loadedResource = fetchObject(
requestScope,
entity.getProjection(),
Optional.of(Collections.singletonList(id.get()))
).getPersistentResources();
upsertedResource = loadedResource.iterator().next();
// The ID doesn't exist yet. Let's create the object.
} catch (InvalidObjectIdentifierException | InvalidValueException e) {
upsertedResource = PersistentResource.createObject(
parentResource,
context.field.getName(),
entity.getEntityClass(), requestScope, id);
}
}
return updateAttributes(upsertedResource, entity, attributes);
}
private PersistentResource updateObject(Entity entity, Environment context) {
Set attributes = entity.getAttributes();
Optional id = entity.getId();
RequestScope requestScope = entity.getRequestScope();
PersistentResource> updatedResource;
if (!id.isPresent()) {
throw new BadRequestException("UPDATE data objects must include ids");
}
Set loadedResource = fetchObject(
requestScope,
entity.getProjection(),
Optional.of(Collections.singletonList(id.get()))
).getPersistentResources();
updatedResource = loadedResource.iterator().next();
return updateAttributes(updatedResource, entity, attributes);
}
/**
* Updates an object.
* @param toUpdate Entities to update
* @param entity Resource entity
* @param attributes Set of entity attributes
* @return Persistence Resource object
*/
private PersistentResource> updateAttributes(PersistentResource> toUpdate,
Entity entity,
Set attributes) {
EntityDictionary dictionary = entity.getRequestScope().getDictionary();
Type> entityClass = entity.getEntityClass();
String idFieldName = dictionary.getIdFieldName(entityClass);
/* iterate through each attribute provided */
for (Entity.Attribute attribute : attributes) {
if (dictionary.isAttribute(entityClass, attribute.getName())) {
Type> attributeType = dictionary.getType(entityClass, attribute.getName());
Object attributeValue;
if (ClassType.MAP_TYPE.isAssignableFrom(attributeType)) {
attributeValue = MapEntryContainer.translateFromGraphQLMap(attribute);
} else {
attributeValue = attribute.getValue();
}
toUpdate.updateAttribute(attribute.getName(), attributeValue);
} else if (!Objects.equals(attribute.getName(), idFieldName)) {
throw new IllegalStateException("Unrecognized attribute passed to 'data': " + attribute.getName());
}
}
return toUpdate;
}
/**
* Deletes a resource.
* @param context Environment encapsulating graphQL's request environment
* @return set of deleted {@link PersistentResource} object(s)
*/
private Object deleteObjects(Environment context) {
/* sanity check for id and data argument w DELETE */
if (context.data.isPresent()) {
throw new BadRequestException("DELETE must not include data argument");
}
if (!context.ids.isPresent()) {
throw new BadRequestException("DELETE must include ids argument");
}
ConnectionContainer connection = (ConnectionContainer) fetchObjects(context);
Set toDelete = connection.getPersistentResources();
toDelete.forEach(PersistentResource::deleteResource);
return new ConnectionContainer(
Collections.emptySet(),
Optional.empty(),
connection.getTypeName()
);
}
/**
* Removes a relationship, or deletes a root level resource.
* @param context Environment encapsulating graphQL's request environment
* @return set of removed {@link PersistentResource} object(s)
*/
private Object removeObjects(Environment context) {
/* sanity check for id and data argument w REPLACE */
if (context.data.isPresent()) {
throw new BadRequestException("REPLACE must not include data argument");
}
if (!context.ids.isPresent()) {
throw new BadRequestException("REPLACE must include ids argument");
}
ConnectionContainer connection = (ConnectionContainer) fetchObjects(context);
Set toRemove = connection.getPersistentResources();
if (!context.isRoot()) { /* has parent */
toRemove.forEach(item -> context.parentResource.removeRelation(context.field.getName(), item));
} else { /* is root */
toRemove.forEach(PersistentResource::deleteResource);
}
return new ConnectionContainer(
Collections.emptySet(),
Optional.empty(),
connection.getTypeName()
);
}
/**
* Replaces a resource, updates given resource and deletes the rest
* belonging to the the same type/relationship family.
* @param context Environment encapsulating graphQL's request environment
* @return set of replaced {@link PersistentResource} object(s)
*/
private ConnectionContainer replaceObjects(Environment context) {
/* sanity check for id and data argument w REPLACE */
if (!context.data.isPresent()) {
throw new BadRequestException("REPLACE must include data argument");
}
if (context.ids.isPresent()) {
throw new BadRequestException("REPLACE must not include ids argument");
}
ConnectionContainer existingObjects =
(ConnectionContainer) context.container.processFetch(context, this);
ConnectionContainer upsertedObjects = upsertObjects(context);
Set toDelete =
Sets.difference(existingObjects.getPersistentResources(), upsertedObjects.getPersistentResources());
if (!context.isRoot()) { /* has parent */
toDelete.forEach(item -> context.parentResource.removeRelation(context.field.getName(), item));
} else { /* is root */
toDelete.forEach(PersistentResource::deleteResource);
}
return upsertedObjects;
}
}