diff --git a/changelog.md b/changelog.md index 34c1b7a03a..9d3349f7c0 100644 --- a/changelog.md +++ b/changelog.md @@ -1,7 +1,11 @@ # Change Log -## 4.3.4 +## 4.4.0 +**Features** + * Issue#763 Support for filtering & sorting on computed attributes + **Fixes** * Throw proper exception on invalid PersistentResource where id=null + * Issue#744 Elide returns wrong date parsing format in 400 error for non-default DateFormats **Features** * Added [JPA Data Store](https://github.com/yahoo/elide/pull/747) diff --git a/elide-core/src/main/java/com/yahoo/elide/Elide.java b/elide-core/src/main/java/com/yahoo/elide/Elide.java index a12e78f333..7f343c366d 100644 --- a/elide-core/src/main/java/com/yahoo/elide/Elide.java +++ b/elide-core/src/main/java/com/yahoo/elide/Elide.java @@ -11,6 +11,7 @@ import com.yahoo.elide.core.ErrorObjects; import com.yahoo.elide.core.HttpStatus; import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore; import com.yahoo.elide.core.exceptions.CustomErrorException; import com.yahoo.elide.core.exceptions.ForbiddenAccessException; import com.yahoo.elide.core.exceptions.HttpStatusException; @@ -68,7 +69,7 @@ public class Elide { public Elide(ElideSettings elideSettings) { this.elideSettings = elideSettings; this.auditLogger = elideSettings.getAuditLogger(); - this.dataStore = elideSettings.getDataStore(); + this.dataStore = new InMemoryDataStore(elideSettings.getDataStore()); this.dataStore.populateEntityDictionary(elideSettings.getDictionary()); this.mapper = elideSettings.getMapper(); diff --git a/elide-core/src/main/java/com/yahoo/elide/core/DataStoreTransaction.java b/elide-core/src/main/java/com/yahoo/elide/core/DataStoreTransaction.java index 17ba9802cb..258fda4098 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/DataStoreTransaction.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/DataStoreTransaction.java @@ -5,6 +5,8 @@ */ package com.yahoo.elide.core; +import com.yahoo.elide.core.filter.InPredicate; +import com.yahoo.elide.core.filter.expression.AndFilterExpression; import com.yahoo.elide.core.filter.expression.FilterExpression; import com.yahoo.elide.core.pagination.Pagination; import com.yahoo.elide.core.sort.Sorting; @@ -12,6 +14,7 @@ import java.io.Closeable; import java.io.Serializable; +import java.util.Iterator; import java.util.Optional; import java.util.Set; @@ -20,6 +23,15 @@ */ public interface DataStoreTransaction extends Closeable { + /** + * The extent to which the transaction supports a particular feature. + */ + public enum FeatureSupport { + FULL, + PARTIAL, + NONE + } + /** * Wrap the opaque user. * @@ -109,11 +121,31 @@ default T createNewObject(Class entityClass) { * It is optional for the data store to attempt evaluation. * @return the loaded object if it exists AND any provided security filters pass. */ - Object loadObject(Class entityClass, + default Object loadObject(Class entityClass, Serializable id, Optional filterExpression, - RequestScope scope); - + RequestScope scope) { + EntityDictionary dictionary = scope.getDictionary(); + Class idType = dictionary.getIdType(entityClass); + String idField = dictionary.getIdFieldName(entityClass); + FilterExpression idFilter = new InPredicate( + new Path.PathElement(entityClass, idType, idField), + id + ); + FilterExpression joinedFilterExpression = filterExpression + .map(fe -> (FilterExpression) new AndFilterExpression(idFilter, fe)) + .orElse(idFilter); + Iterable results = loadObjects(entityClass, + Optional.of(joinedFilterExpression), + Optional.empty(), + Optional.empty(), + scope); + Iterator it = results == null ? null : results.iterator(); + if (it != null && it.hasNext()) { + return it.next(); + } + return null; + } /** * Loads a collection of objects. @@ -240,4 +272,32 @@ default void setAttribute(Object entity, Object attributeValue, RequestScope scope) { } + + /** + * Whether or not the transaction can filter the provided class with the provided expression. + * @param entityClass The class to filter + * @param expression The filter expression + * @return FULL, PARTIAL, or NONE + */ + default FeatureSupport supportsFiltering(Class entityClass, FilterExpression expression) { + return FeatureSupport.FULL; + } + + /** + * Whether or not the transaction can sort the provided class. + * @param entityClass + * @return true if sorting is possible + */ + default boolean supportsSorting(Class entityClass, Sorting sorting) { + return true; + } + + /** + * Whether or not the transaction can paginate the provided class. + * @param entityClass + * @return true if pagination is possible + */ + default boolean supportsPagination(Class entityClass) { + return true; + } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java b/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java index 9bd2ee82a7..af40c21ae1 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/EntityDictionary.java @@ -988,6 +988,17 @@ public AccessibleObject getAccessibleObject(Object target, String fieldName) { return getAccessibleObject(target.getClass(), fieldName); } + public boolean isComputed(Class entityClass, String fieldName) { + AccessibleObject fieldOrMethod = getAccessibleObject(entityClass, fieldName); + + if (fieldOrMethod == null) { + return false; + } + + return (fieldOrMethod.isAnnotationPresent(ComputedAttribute.class) + || fieldOrMethod.isAnnotationPresent(ComputedRelationship.class)); + } + /** * Retrieve the accessible object for a field. * diff --git a/elide-core/src/main/java/com/yahoo/elide/core/PersistentResource.java b/elide-core/src/main/java/com/yahoo/elide/core/PersistentResource.java index b67a51e6f4..69a25c60ca 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/PersistentResource.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/PersistentResource.java @@ -24,7 +24,6 @@ import com.yahoo.elide.core.filter.InPredicate; import com.yahoo.elide.core.filter.expression.AndFilterExpression; import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.filter.expression.InMemoryFilterVisitor; import com.yahoo.elide.core.pagination.Pagination; import com.yahoo.elide.core.sort.Sorting; import com.yahoo.elide.jsonapi.models.Data; @@ -67,7 +66,6 @@ import java.util.Set; import java.util.TreeMap; import java.util.function.Function; -import java.util.function.Predicate; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -825,9 +823,8 @@ public Set getRelation(String relation, } else { if (!ids.isEmpty()) { // Fetch our set of new resources that we know about since we can't find them in the datastore - String typeAlias = dictionary.getJsonAliasFor(entityType); newResources = requestScope.getNewPersistentResources().stream() - .filter(resource -> typeAlias.equals(resource.getType()) + .filter(resource -> entityType.isAssignableFrom(resource.getResourceClass()) && ids.contains(resource.getUUID().orElse(""))) .collect(Collectors.toSet()); @@ -1011,30 +1008,16 @@ private Set getRelationUnchecked(String relationName, computedFilters = permissionFilter; } - - /* If we are mutating multiple entities, the data store transaction cannot perform filter & pagination directly. - * It must be done in memory by Elide as some newly created entities have not yet been persisted. - */ - Object val; - if (requestScope.isMutatingMultipleEntities()) { - val = transaction.getRelation(transaction, obj, relationName, - Optional.empty(), sorting, Optional.empty(), requestScope); - - if (val instanceof Collection) { - val = filterInMemory((Collection) val, computedFilters); - } - } else { - val = transaction.getRelation(transaction, obj, relationName, + Object val = transaction.getRelation(transaction, obj, relationName, computedFilters, sorting, computedPagination, requestScope); - } if (val == null) { return Collections.emptySet(); } Set resources = Sets.newLinkedHashSet(); - if (val instanceof Collection) { - Collection filteredVal = (Collection) val; + if (val instanceof Iterable) { + Iterable filteredVal = (Iterable) val; resources = new PersistentResourceSet(this, filteredVal, requestScope); } else if (type.isToOne()) { resources = new SingleElementSet( @@ -1046,30 +1029,6 @@ private Set getRelationUnchecked(String relationName, return resources; } - /** - * Filters a relationship collection in memory for scenarios where the data store transaction cannot do it. - * - * @param the type parameter - * @param collection the collection to filter - * @param filterExpression the filter expression - * @return the filtered collection - */ - protected Collection filterInMemory(Collection collection, Optional filterExpression) { - - if (! filterExpression.isPresent()) { - return collection; - } - - InMemoryFilterVisitor inMemoryFilterVisitor = new InMemoryFilterVisitor(requestScope); - @SuppressWarnings("unchecked") - Predicate inMemoryFilterFn = filterExpression.get().accept(inMemoryFilterVisitor); - // NOTE: We can safely _skip_ tests on NEWLY created objects. - // We assume a user can READ their object they are allowed to create. - return collection.stream() - .filter(e -> requestScope.isNewResource(e) || inMemoryFilterFn.test(e)) - .collect(Collectors.toList()); - } - /** * Determine whether or not to skip loading a collection. * diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java new file mode 100644 index 0000000000..2603b80b75 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapDataStore.java @@ -0,0 +1,75 @@ +/* + * Copyright 2017, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.datastore.inmemory; + +import com.yahoo.elide.core.DataStore; +import com.yahoo.elide.core.DataStoreTransaction; +import com.yahoo.elide.core.EntityDictionary; + +import org.reflections.Reflections; +import org.reflections.scanners.SubTypesScanner; +import org.reflections.scanners.TypeAnnotationsScanner; +import org.reflections.util.ClasspathHelper; +import org.reflections.util.ConfigurationBuilder; + +import lombok.Getter; + +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +import javax.persistence.Entity; + +/** + * Simple in-memory only database. + */ +public class HashMapDataStore implements DataStore { + private final Map, Map> dataStore = Collections.synchronizedMap(new HashMap<>()); + @Getter private EntityDictionary dictionary; + @Getter private final Package beanPackage; + @Getter private final ConcurrentHashMap, AtomicLong> typeIds = new ConcurrentHashMap<>(); + + public HashMapDataStore(Package beanPackage) { + this.beanPackage = beanPackage; + } + + @Override + public void populateEntityDictionary(EntityDictionary dictionary) { + Reflections reflections = new Reflections(new ConfigurationBuilder() + .addUrls(ClasspathHelper.forPackage(beanPackage.getName())) + .setScanners(new SubTypesScanner(), new TypeAnnotationsScanner())); + reflections.getTypesAnnotatedWith(Entity.class).stream() + .filter(entityAnnotatedClass -> entityAnnotatedClass.getPackage().getName() + .startsWith(beanPackage.getName())) + .forEach((cls) -> { + dictionary.bindEntity(cls); + dataStore.put(cls, Collections.synchronizedMap(new LinkedHashMap<>())); + }); + this.dictionary = dictionary; + } + + @Override + public DataStoreTransaction beginTransaction() { + return new HashMapStoreTransaction(dataStore, dictionary, typeIds); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Data store contents "); + for (Class cls : dataStore.keySet()) { + sb.append("\n Table ").append(cls).append(" contents \n"); + Map data = dataStore.get(cls); + for (Map.Entry e : data.entrySet()) { + sb.append(" Id: ").append(e.getKey()).append(" Value: ").append(e.getValue()); + } + } + return sb.toString(); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java new file mode 100644 index 0000000000..0523c666df --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/HashMapStoreTransaction.java @@ -0,0 +1,195 @@ +/* + * Copyright 2017, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.datastore.inmemory; + +import com.yahoo.elide.core.DataStoreTransaction; +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.PersistentResource; +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.core.pagination.Pagination; +import com.yahoo.elide.core.sort.Sorting; +import com.yahoo.elide.utils.coerce.CoerceUtil; + +import lombok.extern.slf4j.Slf4j; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Function; +import java.util.stream.Collectors; + +import javax.persistence.Id; + +/** + * HashMapDataStore transaction handler. + */ +@Slf4j +public class HashMapStoreTransaction implements DataStoreTransaction { + private final Map, Map> dataStore; + private final List operations; + private final EntityDictionary dictionary; + private final Map, AtomicLong> typeIds; + + public HashMapStoreTransaction(Map, Map> dataStore, + EntityDictionary dictionary, Map, AtomicLong> typeIds) { + this.dataStore = dataStore; + this.dictionary = dictionary; + this.operations = new ArrayList<>(); + this.typeIds = typeIds; + } + + @Override + public void flush(RequestScope requestScope) { + // Do nothing + } + + @Override + public void save(Object object, RequestScope requestScope) { + if (object == null) { + return; + } + String id = dictionary.getId(object); + if (id == null || "null".equals(id) || "0".equals(id)) { + createObject(object, requestScope); + } + id = dictionary.getId(object); + operations.add(new Operation(id, object, object.getClass(), false)); + } + + @Override + public void delete(Object object, RequestScope requestScope) { + if (object == null) { + return; + } + + String id = dictionary.getId(object); + operations.add(new Operation(id, object, object.getClass(), true)); + } + + @Override + public void commit(RequestScope scope) { + synchronized (dataStore) { + operations.stream() + .filter(op -> op.getInstance() != null) + .forEach(op -> { + Object instance = op.getInstance(); + String id = op.getId(); + Map data = dataStore.get(op.getType()); + if (op.getDelete()) { + data.remove(id); + } else { + data.put(id, instance); + } + }); + operations.clear(); + } + } + + @Override + public void createObject(Object entity, RequestScope scope) { + Class entityClass = entity.getClass(); + + // TODO: Id's are not necessarily numeric. + AtomicLong nextId; + synchronized (dataStore) { + nextId = typeIds.computeIfAbsent(entityClass, + (key) -> { + long maxId = dataStore.get(key).keySet().stream() + .mapToLong(Long::parseLong) + .max() + .orElse(0); + return new AtomicLong(maxId + 1); + }); + } + String id = String.valueOf(nextId.getAndIncrement()); + setId(entity, id); + operations.add(new Operation(id, entity, entity.getClass(), false)); + } + + public void setId(Object value, String id) { + for (Class cls = value.getClass(); cls != null; cls = cls.getSuperclass()) { + for (Method method : cls.getMethods()) { + if (method.isAnnotationPresent(Id.class) && method.getName().startsWith("get")) { + String setName = "set" + method.getName().substring(3); + for (Method setMethod : cls.getMethods()) { + if (setMethod.getName().equals(setName) && setMethod.getParameterCount() == 1) { + try { + setMethod.invoke(value, CoerceUtil.coerce(id, setMethod.getParameters()[0].getType())); + } catch (ReflectiveOperationException e) { + log.error("set {}", setMethod, e); + } + return; + } + } + } + } + } + } + + @Override + public Object getRelation(DataStoreTransaction relationTx, + Object entity, + String relationName, + Optional filterExpression, + Optional sorting, + Optional pagination, + RequestScope scope) { + Object values = PersistentResource.getValue(entity, relationName, scope); + + // Gather list of valid id's from this parent + Map idToChildResource = new HashMap<>(); + if (dictionary.getRelationshipType(entity, relationName).isToOne()) { + if (values == null) { + return null; + } + idToChildResource.put(dictionary.getId(values), values); + } else if (values instanceof Collection) { + idToChildResource.putAll((Map) ((Collection) values).stream() + .collect(Collectors.toMap(dictionary::getId, Function.identity()))); + } else { + throw new IllegalStateException("An unexpected error occurred querying a relationship"); + } + + return idToChildResource.values(); + } + + @Override + public Iterable loadObjects(Class entityClass, Optional filterExpression, + Optional sorting, Optional pagination, + RequestScope scope) { + synchronized (dataStore) { + Map data = dataStore.get(entityClass); + return data.values(); + } + } + + @Override + public void close() throws IOException { + operations.clear(); + } + + @Override + public FeatureSupport supportsFiltering(Class entityClass, FilterExpression expression) { + return FeatureSupport.NONE; + } + + @Override + public boolean supportsSorting(Class entityClass, Sorting sorting) { + return false; + } + + @Override + public boolean supportsPagination(Class entityClass) { + return false; + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryDataStore.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryDataStore.java index 3bbe744962..252abe8e3b 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryDataStore.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryDataStore.java @@ -1,75 +1,44 @@ /* - * Copyright 2017, Yahoo Inc. + * 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.datastore.inmemory; import com.yahoo.elide.core.DataStore; import com.yahoo.elide.core.DataStoreTransaction; import com.yahoo.elide.core.EntityDictionary; -import org.reflections.Reflections; -import org.reflections.scanners.SubTypesScanner; -import org.reflections.scanners.TypeAnnotationsScanner; -import org.reflections.util.ClasspathHelper; -import org.reflections.util.ConfigurationBuilder; - -import lombok.Getter; - -import java.util.Collections; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -import javax.persistence.Entity; - /** - * Simple in-memory only database. + * Data Store that wraps another store and provides in-memory filtering, soring, and pagination + * when the underlying store cannot perform the equivalent function. */ public class InMemoryDataStore implements DataStore { - private final Map, Map> dataStore = Collections.synchronizedMap(new HashMap<>()); - @Getter private EntityDictionary dictionary; - @Getter private final Package beanPackage; - @Getter private final ConcurrentHashMap, AtomicLong> typeIds = new ConcurrentHashMap<>(); + private DataStore wrappedStore; + + public InMemoryDataStore(DataStore wrappedStore) { + this.wrappedStore = wrappedStore; + } + + @Deprecated public InMemoryDataStore(Package beanPackage) { - this.beanPackage = beanPackage; + this(new HashMapDataStore(beanPackage)); } @Override public void populateEntityDictionary(EntityDictionary dictionary) { - Reflections reflections = new Reflections(new ConfigurationBuilder() - .addUrls(ClasspathHelper.forPackage(beanPackage.getName())) - .setScanners(new SubTypesScanner(), new TypeAnnotationsScanner())); - reflections.getTypesAnnotatedWith(Entity.class).stream() - .filter(entityAnnotatedClass -> entityAnnotatedClass.getPackage().getName() - .startsWith(beanPackage.getName())) - .forEach((cls) -> { - dictionary.bindEntity(cls); - dataStore.put(cls, Collections.synchronizedMap(new LinkedHashMap<>())); - }); - this.dictionary = dictionary; + wrappedStore.populateEntityDictionary(dictionary); } @Override public DataStoreTransaction beginTransaction() { - return new InMemoryTransaction(dataStore, dictionary, typeIds); + return new InMemoryStoreTransaction(wrappedStore.beginTransaction()); } @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb.append("Data store contents "); - for (Class cls : dataStore.keySet()) { - sb.append("\n Table ").append(cls).append(" contents \n"); - Map data = dataStore.get(cls); - for (Map.Entry e : data.entrySet()) { - sb.append(" Id: ").append(e.getKey()).append(" Value: ").append(e.getValue()); - } - } - return sb.toString(); + public DataStoreTransaction beginReadTransaction() { + return new InMemoryStoreTransaction(wrappedStore.beginReadTransaction()); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransaction.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransaction.java new file mode 100644 index 0000000000..73753e4dcc --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransaction.java @@ -0,0 +1,447 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core.datastore.inmemory; + +import com.yahoo.elide.core.DataStoreTransaction; +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.PersistentResource; +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.core.filter.expression.FilterPredicatePushdownExtractor; +import com.yahoo.elide.core.filter.expression.InMemoryExecutionVerifier; +import com.yahoo.elide.core.filter.expression.InMemoryFilterExecutor; +import com.yahoo.elide.core.pagination.Pagination; +import com.yahoo.elide.core.sort.Sorting; +import com.yahoo.elide.security.User; +import org.apache.commons.lang3.tuple.Pair; + +import java.io.IOException; +import java.io.Serializable; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +/** + * Data Store Transaction that wraps another transaction and provides in-memory filtering, soring, and pagination + * when the underlying transaction cannot perform the equivalent function. + */ +public class InMemoryStoreTransaction implements DataStoreTransaction { + + private DataStoreTransaction tx; + + /** + * Fetches data from the store. + */ + @FunctionalInterface + private interface DataFetcher { + Object fetch(Optional filterExpression, + Optional sorting, + Optional pagination, + RequestScope scope); + } + + + public InMemoryStoreTransaction(DataStoreTransaction tx) { + this.tx = tx; + } + + + @Override + public void save(Object entity, RequestScope scope) { + tx.save(entity, scope); + } + + @Override + public void delete(Object entity, RequestScope scope) { + tx.delete(entity, scope); + } + + @Override + public User accessUser(Object opaqueUser) { + return tx.accessUser(opaqueUser); + } + + @Override + public void preCommit() { + tx.preCommit(); + } + + @Override + public T createNewObject(Class entityClass) { + return tx.createNewObject(entityClass); + } + + @Override + public Object getRelation(DataStoreTransaction relationTx, + Object entity, + String relationName, + Optional filterExpression, + Optional sorting, + Optional pagination, + RequestScope scope) { + + Class relationClass = scope.getDictionary().getParameterizedType(entity, relationName); + + DataFetcher fetcher = new DataFetcher() { + @Override + public Object fetch(Optional filterExpression, + Optional sorting, + Optional pagination, + RequestScope scope) { + + return tx.getRelation(relationTx, entity, relationName, filterExpression, sorting, pagination, scope); + } + }; + + boolean filterInMemory = scope.isMutatingMultipleEntities(); + return fetchData(fetcher, relationClass, filterExpression, sorting, pagination, filterInMemory, scope); + } + + @Override + public void updateToManyRelation(DataStoreTransaction relationTx, + Object entity, + String relationName, + Set newRelationships, + Set deletedRelationships, + RequestScope scope) { + tx.updateToManyRelation(relationTx, entity, relationName, newRelationships, deletedRelationships, scope); + } + + @Override + public void updateToOneRelation(DataStoreTransaction relationTx, + Object entity, + String relationName, + Object relationshipValue, + RequestScope scope) { + tx.updateToOneRelation(relationTx, entity, relationName, relationshipValue, scope); + } + + @Override + public Object getAttribute(Object entity, String attributeName, RequestScope scope) { + return tx.getAttribute(entity, attributeName, scope); + } + + @Override + public void setAttribute(Object entity, String attributeName, Object attributeValue, RequestScope scope) { + tx.setAttribute(entity, attributeName, attributeValue, scope); + + } + + @Override + public void flush(RequestScope scope) { + tx.flush(scope); + } + + @Override + public void commit(RequestScope scope) { + tx.commit(scope); + } + + @Override + public void createObject(Object entity, RequestScope scope) { + tx.createObject(entity, scope); + } + + @Override + public Object loadObject(Class entityClass, + Serializable id, + Optional filterExpression, + RequestScope scope) { + if (filterExpression.isPresent() + && tx.supportsFiltering(entityClass, filterExpression.get()) == FeatureSupport.FULL) { + return tx.loadObject(entityClass, id, filterExpression, scope); + } else { + return DataStoreTransaction.super.loadObject(entityClass, id, filterExpression, scope); + } + } + + @Override + public Iterable loadObjects(Class entityClass, + Optional filterExpression, + Optional sorting, + Optional pagination, + RequestScope scope) { + + DataFetcher fetcher = new DataFetcher() { + @Override + public Iterable fetch(Optional filterExpression, + Optional sorting, + Optional pagination, + RequestScope scope) { + return tx.loadObjects(entityClass, filterExpression, sorting, pagination, scope); + } + }; + + return (Iterable) fetchData(fetcher, entityClass, + filterExpression, sorting, pagination, false, scope); + } + + @Override + public void close() throws IOException { + tx.close(); + } + + private Iterable filterLoadedData(Iterable loadedRecords, + Optional filterExpression, + RequestScope scope) { + if (! filterExpression.isPresent()) { + return loadedRecords; + } + + Predicate predicate = filterExpression.get().accept(new InMemoryFilterExecutor(scope)); + + return StreamSupport.stream(loadedRecords.spliterator(), false) + .filter(predicate::test) + .collect(Collectors.toList()); + } + + private Object fetchData(DataFetcher fetcher, + Class entityClass, + Optional filterExpression, + Optional sorting, + Optional pagination, + boolean filterInMemory, + RequestScope scope) { + + Pair, Optional> expressionSplit = splitFilterExpression( + entityClass, filterExpression, filterInMemory, scope); + + Optional dataStoreFilter = expressionSplit.getLeft(); + Optional inMemoryFilter = expressionSplit.getRight(); + + Pair, Optional> sortSplit = splitSorting(entityClass, + sorting, inMemoryFilter.isPresent()); + + Optional dataStoreSort = sortSplit.getLeft(); + Optional inMemorySort = sortSplit.getRight(); + + Pair, Optional> paginationSplit = splitPagination(entityClass, + pagination, inMemoryFilter.isPresent(), inMemorySort.isPresent()); + + + Optional dataStorePagination = paginationSplit.getLeft(); + Optional inMemoryPagination = paginationSplit.getRight(); + + Object result = fetcher.fetch(dataStoreFilter, dataStoreSort, dataStorePagination, scope); + + if (! (result instanceof Iterable)) { + return result; + } + + Iterable loadedRecords = (Iterable) result; + + if (inMemoryFilter.isPresent()) { + loadedRecords = filterLoadedData(loadedRecords, filterExpression, scope); + } + + + return sortAndPaginateLoadedData( + loadedRecords, + entityClass, + inMemorySort, + inMemoryPagination, + scope); + } + + + private Iterable sortAndPaginateLoadedData(Iterable loadedRecords, + Class entityClass, + Optional sorting, + Optional pagination, + RequestScope scope) { + + //Try to skip the data copy if possible + if (! sorting.isPresent() && ! pagination.isPresent()) { + return loadedRecords; + } + + EntityDictionary dictionary = scope.getDictionary(); + + Map sortRules = sorting + .map((s) -> s.getValidSortingRules(entityClass, dictionary)) + .orElse(new HashMap<>()); + + // No sorting required for this type & no pagination. + if (sortRules.isEmpty() && ! pagination.isPresent()) { + return loadedRecords; + } + //We need an in memory copy to sort or paginate. + List results = StreamSupport.stream(loadedRecords.spliterator(), false).collect(Collectors.toList()); + + if (! sortRules.isEmpty()) { + results = sortInMemory(results, sortRules, scope); + } + + if (pagination.isPresent()) { + results = paginateInMemory(results, pagination.get()); + } + + return results; + } + + private List paginateInMemory(List records, Pagination pagination) { + int offset = pagination.getOffset(); + int limit = pagination.getLimit(); + if (offset < 0 || offset >= records.size()) { + return Collections.emptyList(); + } + + int endIdx = offset + limit; + if (endIdx > records.size()) { + endIdx = records.size(); + } + + if (pagination.isGenerateTotals()) { + pagination.setPageTotals(records.size()); + } + return records.subList(offset, endIdx); + } + + private List sortInMemory(List records, + Map sortRules, + RequestScope scope) { + //Build a comparator that handles multiple comparison rules. + Comparator noSort = (left, right) -> 0; + + Comparator comp = sortRules.entrySet().stream() + .map(entry -> getComparator(entry.getKey(), entry.getValue(), scope)) + .reduce(noSort, (comparator1, comparator2) -> (left, right) -> { + int comparison = comparator1.compare(left, right); + if (comparison == 0) { + return comparator2.compare(left, right); + } + return comparison; + }); + + records.sort(comp); + return records; + } + + private Comparator getComparator(Path path, Sorting.SortOrder order, RequestScope requestScope) { + return (left, right) -> { + Object leftCompare = left; + Object rightCompare = right; + + // Drill down into path to find value for comparison + for (Path.PathElement pathElement : path.getPathElements()) { + leftCompare = PersistentResource.getValue(leftCompare, pathElement.getFieldName(), requestScope); + rightCompare = PersistentResource.getValue(rightCompare, pathElement.getFieldName(), requestScope); + } + + // Make sure value is comparable and perform comparison + if (leftCompare instanceof Comparable) { + int result = ((Comparable) leftCompare).compareTo(rightCompare); + if (order == Sorting.SortOrder.asc) { + return result; + } + return -result; + } + + throw new IllegalStateException("Trying to comparing non-comparable types!"); + }; + } + + /** + * Splits a filter expression into two components: + * - a component that should be pushed down to the data store + * - a component that should be executed in memory + * @param entityClass The class to filter + * @param filterExpression The filter expression + * @param filterInMemory Whether or not the transaction requires in memory filtering. + * @param scope The request context + * @return A pair of filter expressions (data store expression, in memory expression) + */ + private Pair, Optional> splitFilterExpression( + Class entityClass, + Optional filterExpression, + boolean filterInMemory, + RequestScope scope + ) { + + Optional inStoreFilterExpression = filterExpression; + Optional inMemoryFilterExpression = Optional.empty(); + + boolean transactionNeedsInMemoryFiltering = filterInMemory; + + if (filterExpression.isPresent()) { + FeatureSupport filterSupport = tx.supportsFiltering(entityClass, filterExpression.get()); + + boolean storeNeedsInMemoryFiltering = filterSupport != FeatureSupport.FULL; + + if (transactionNeedsInMemoryFiltering || filterSupport == FeatureSupport.NONE) { + inStoreFilterExpression = Optional.empty(); + } else { + inStoreFilterExpression = Optional.ofNullable( + FilterPredicatePushdownExtractor.extractPushDownPredicate(scope.getDictionary(), + filterExpression.get())); + } + + boolean expressionNeedsInMemoryFiltering = InMemoryExecutionVerifier.shouldExecuteInMemory( + scope.getDictionary(), filterExpression.get()); + + if (transactionNeedsInMemoryFiltering || storeNeedsInMemoryFiltering || expressionNeedsInMemoryFiltering) { + inMemoryFilterExpression = filterExpression; + } + } + + return Pair.of(inStoreFilterExpression, inMemoryFilterExpression); + } + + /** + * Splits a sorting object into two components: + * - a component that should be pushed down to the data store + * - a component that should be executed in memory + * @param entityClass The class to filter + * @param sorting The sorting object + * @param filteredInMemory Whether or not filtering was performed in memory + * @return A pair of sorting objects (data store sort, in memory sort) + */ + private Pair, Optional> splitSorting( + Class entityClass, + Optional sorting, + boolean filteredInMemory + ) { + if (sorting.isPresent() && (! tx.supportsSorting(entityClass, sorting.get()) || filteredInMemory)) { + return Pair.of(Optional.empty(), sorting); + } else { + return Pair.of(sorting, Optional.empty()); + } + } + + /** + * Splits a pagination object into two components: + * - a component that should be pushed down to the data store + * - a component that should be executed in memory + * @param entityClass The class to filter + * @param pagination The pagination object + * @param filteredInMemory Whether or not filtering was performed in memory + * @param sortedInMemory Whether or not sorting was performed in memory + * @return A pair of pagination objects (data store pagination, in memory pagination) + */ + private Pair, Optional> splitPagination( + Class entityClass, + Optional pagination, + boolean filteredInMemory, + boolean sortedInMemory + ) { + if (!tx.supportsPagination(entityClass) + || filteredInMemory + || sortedInMemory) { + return Pair.of(Optional.empty(), pagination); + } else { + return Pair.of(pagination, Optional.empty()); + } + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryTransaction.java b/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryTransaction.java deleted file mode 100644 index 64f48c68f3..0000000000 --- a/elide-core/src/main/java/com/yahoo/elide/core/datastore/inmemory/InMemoryTransaction.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Copyright 2017, Yahoo Inc. - * Licensed under the Apache License, Version 2.0 - * See LICENSE file in project root for terms. - */ -package com.yahoo.elide.core.datastore.inmemory; - -import com.yahoo.elide.core.DataStoreTransaction; -import com.yahoo.elide.core.EntityDictionary; -import com.yahoo.elide.core.Path; -import com.yahoo.elide.core.PersistentResource; -import com.yahoo.elide.core.RequestScope; -import com.yahoo.elide.core.filter.InPredicate; -import com.yahoo.elide.core.filter.expression.AndFilterExpression; -import com.yahoo.elide.core.filter.expression.FilterExpression; -import com.yahoo.elide.core.filter.expression.InMemoryFilterVisitor; -import com.yahoo.elide.core.pagination.Pagination; -import com.yahoo.elide.core.sort.Sorting; -import com.yahoo.elide.utils.coerce.CoerceUtil; - -import lombok.extern.slf4j.Slf4j; - -import java.io.IOException; -import java.io.Serializable; -import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.atomic.AtomicLong; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.stream.Collectors; - -import javax.persistence.Id; - -/** - * InMemoryDataStore transaction handler. - */ -@Slf4j -public class InMemoryTransaction implements DataStoreTransaction { - private final Map, Map> dataStore; - private final List operations; - private final EntityDictionary dictionary; - private final Map, AtomicLong> typeIds; - - public InMemoryTransaction(Map, Map> dataStore, - EntityDictionary dictionary, Map, AtomicLong> typeIds) { - this.dataStore = dataStore; - this.dictionary = dictionary; - this.operations = new ArrayList<>(); - this.typeIds = typeIds; - } - - @Override - public void flush(RequestScope requestScope) { - // Do nothing - } - - @Override - public void save(Object object, RequestScope requestScope) { - if (object == null) { - return; - } - String id = dictionary.getId(object); - if (id == null || "null".equals(id) || "0".equals(id)) { - createObject(object, requestScope); - } - id = dictionary.getId(object); - operations.add(new Operation(id, object, object.getClass(), false)); - } - - @Override - public void delete(Object object, RequestScope requestScope) { - if (object == null) { - return; - } - - String id = dictionary.getId(object); - operations.add(new Operation(id, object, object.getClass(), true)); - } - - @Override - public void commit(RequestScope scope) { - synchronized (dataStore) { - operations.stream() - .filter(op -> op.getInstance() != null) - .forEach(op -> { - Object instance = op.getInstance(); - String id = op.getId(); - Map data = dataStore.get(op.getType()); - if (op.getDelete()) { - data.remove(id); - } else { - data.put(id, instance); - } - }); - operations.clear(); - } - } - - @Override - public void createObject(Object entity, RequestScope scope) { - Class entityClass = entity.getClass(); - - // TODO: Id's are not necessarily numeric. - AtomicLong nextId; - synchronized (dataStore) { - nextId = typeIds.computeIfAbsent(entityClass, - (key) -> { - long maxId = dataStore.get(key).keySet().stream() - .mapToLong(Long::parseLong) - .max() - .orElse(0); - return new AtomicLong(maxId + 1); - }); - } - String id = String.valueOf(nextId.getAndIncrement()); - setId(entity, id); - operations.add(new Operation(id, entity, entity.getClass(), false)); - } - - public void setId(Object value, String id) { - for (Class cls = value.getClass(); cls != null; cls = cls.getSuperclass()) { - for (Method method : cls.getMethods()) { - if (method.isAnnotationPresent(Id.class) && method.getName().startsWith("get")) { - String setName = "set" + method.getName().substring(3); - for (Method setMethod : cls.getMethods()) { - if (setMethod.getName().equals(setName) && setMethod.getParameterCount() == 1) { - try { - setMethod.invoke(value, CoerceUtil.coerce(id, setMethod.getParameters()[0].getType())); - } catch (ReflectiveOperationException e) { - log.error("set {}", setMethod, e); - } - return; - } - } - } - } - } - } - - @Override - public Object getRelation(DataStoreTransaction relationTx, - Object entity, - String relationName, - Optional filterExpression, - Optional sorting, - Optional pagination, - RequestScope scope) { - Object values = PersistentResource.getValue(entity, relationName, scope); - - // Gather list of valid id's from this parent - Map idToChildResource = new HashMap<>(); - if (dictionary.getRelationshipType(entity, relationName).isToOne()) { - if (values == null) { - return null; - } - idToChildResource.put(dictionary.getId(values), values); - } else if (values instanceof Collection) { - idToChildResource.putAll((Map) ((Collection) values).stream() - .collect(Collectors.toMap(dictionary::getId, Function.identity()))); - } else { - throw new IllegalStateException("An unexpected error occurred querying a relationship"); - } - - Class entityClass = dictionary.getParameterizedType(entity, relationName); - - return processData(entityClass, idToChildResource, filterExpression, sorting, pagination, scope); - } - - @Override - public Object loadObject(Class entityClass, Serializable id, - Optional filterExpression, RequestScope scope) { - Class idType = dictionary.getIdType(entityClass); - String idField = dictionary.getIdFieldName(entityClass); - FilterExpression idFilter = new InPredicate( - new Path.PathElement(entityClass, idType, idField), - id - ); - FilterExpression joinedFilterExpression = filterExpression - .map(fe -> (FilterExpression) new AndFilterExpression(idFilter, fe)) - .orElse(idFilter); - Iterable results = loadObjects(entityClass, - Optional.of(joinedFilterExpression), - Optional.empty(), - Optional.empty(), - scope); - Iterator it = results == null ? null : results.iterator(); - if (it != null && it.hasNext()) { - return it.next(); - } - return null; - } - - @Override - public Iterable loadObjects(Class entityClass, Optional filterExpression, - Optional sorting, Optional pagination, - RequestScope scope) { - synchronized (dataStore) { - Map data = dataStore.get(entityClass); - return processData(entityClass, data, filterExpression, sorting, pagination, scope); - } - } - - @Override - public void close() throws IOException { - operations.clear(); - } - - /** - * Get the comparator for sorting. - * - * @param path Path to field for sorting - * @param order Order to sort - * @param requestScope Request scope - * @return Comparator for sorting - */ - private Comparator getComparator(Path path, Sorting.SortOrder order, RequestScope requestScope) { - return (left, right) -> { - Object leftCompare = left; - Object rightCompare = right; - - // Drill down into path to find value for comparison - for (Path.PathElement pathElement : path.getPathElements()) { - leftCompare = PersistentResource.getValue(leftCompare, pathElement.getFieldName(), requestScope); - rightCompare = PersistentResource.getValue(rightCompare, pathElement.getFieldName(), requestScope); - } - - // Make sure value is comparable and perform comparison - if (leftCompare instanceof Comparable) { - int result = ((Comparable) leftCompare).compareTo(rightCompare); - if (order == Sorting.SortOrder.asc) { - return result; - } - return -result; - } - - throw new IllegalStateException("Trying to comparing non-comparable types!"); - }; - } - - /** - * Process an in-memory map of data with filtering, sorting, and pagination. - * - * @param entityClass Entity for which the map of data exists - * @param data Map of data with id's as keys and instances as values - * @param filterExpression Filter expression for filtering data - * @param sorting Sorting object for sorting - * @param pagination Pagination object for type - * @param scope Request scope - * @return Filtered, sorted, and paginated version of the input data set. - */ - private Iterable processData(Class entityClass, - Map data, - Optional filterExpression, - Optional sorting, - Optional pagination, - RequestScope scope) { - // Support for filtering - List results = filterExpression - .map(fe -> { - Predicate predicate = fe.accept(new InMemoryFilterVisitor(scope)); - return data.values().stream().filter(predicate::test).collect(Collectors.toList()); - }) - .orElseGet(() -> new ArrayList<>(data.values())); - - // Support for sorting - Comparator noSort = (left, right) -> 0; - List sorted = sorting - .map(sort -> { - Map sortRules = sort.getValidSortingRules(entityClass, dictionary); - if (sortRules.isEmpty()) { - // No sorting - return results; - } - Comparator comp = sortRules.entrySet().stream() - .map(entry -> getComparator(entry.getKey(), entry.getValue(), scope)) - .reduce(noSort, (comparator1, comparator2) -> (left, right) -> { - int comparison = comparator1.compare(left, right); - if (comparison == 0) { - return comparator2.compare(left, right); - } - return comparison; - }); - results.sort(comp); - return results; - }) - .orElse(results); - - // Support for pagination. Should be done _after_ filtering - return pagination - .map(p -> { - int offset = p.getOffset(); - int limit = p.getLimit(); - if (offset < 0 || offset >= sorted.size()) { - return Collections.emptyList(); - } - int endIdx = offset + limit; - if (endIdx > sorted.size()) { - endIdx = sorted.size(); - } - if (p.isGenerateTotals()) { - p.setPageTotals(sorted.size()); - } - return sorted.subList(offset, endIdx); - }) - .orElse(sorted); - } -} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/Operator.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/Operator.java index 937637cdf4..523423c985 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/Operator.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/Operator.java @@ -164,7 +164,9 @@ public Predicate contextualize(String field, List values, Request LE.negated = GT; LT.negated = GE; IN.negated = NOT; + IN_INSENSITIVE.negated = NOT_INSENSITIVE; NOT.negated = IN; + NOT_INSENSITIVE.negated = IN_INSENSITIVE; TRUE.negated = FALSE; FALSE.negated = TRUE; ISNULL.negated = NOTNULL; @@ -210,12 +212,17 @@ private static Predicate in(String field, List values, RequestSco private static Predicate in(String field, List values, RequestScope requestScope, Function transform) { return (T entity) -> { - Class type = requestScope.getDictionary().getType(entity, field); - if (!type.isAssignableFrom(String.class)) { + Object fieldValue = getFieldValue(entity, field, requestScope); + + if (fieldValue == null) { + return false; + } + + if (!fieldValue.getClass().isAssignableFrom(String.class)) { throw new IllegalStateException("Cannot case insensitive compare non-string values"); } - String val = transform.apply((String) getFieldValue(entity, field, requestScope)); + String val = transform.apply((String) fieldValue); return val != null && values.stream() .map(v -> transform.apply(CoerceUtil.coerce(v, String.class))) .anyMatch(val::equals); diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/FilterPredicatePushdownExtractor.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/FilterPredicatePushdownExtractor.java new file mode 100644 index 0000000000..eb607b5c3b --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/FilterPredicatePushdownExtractor.java @@ -0,0 +1,91 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core.filter.expression; + +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.filter.FilterPredicate; +import com.yahoo.elide.parsers.expression.FilterExpressionNormalizationVisitor; + +/** + * Examines a FilterExpression to determine if some or all of it can be pushed to the data store. + */ +public class FilterPredicatePushdownExtractor implements FilterExpressionVisitor { + + private EntityDictionary dictionary; + + public FilterPredicatePushdownExtractor(EntityDictionary dictionary) { + this.dictionary = dictionary; + } + + @Override + public FilterExpression visitPredicate(FilterPredicate filterPredicate) { + + boolean filterInMemory = false; + for (Path.PathElement pathElement : filterPredicate.getPath().getPathElements()) { + Class entityClass = pathElement.getType(); + String fieldName = pathElement.getFieldName(); + + if (dictionary.isComputed(entityClass, fieldName)) { + filterInMemory = true; + } + } + + return (filterInMemory) ? null : filterPredicate; + } + + @Override + public FilterExpression visitAndExpression(AndFilterExpression expression) { + FilterExpression left = expression.getLeft().accept(this); + FilterExpression right = expression.getRight().accept(this); + + if (left == null) { + return right; + } + + if (right == null) { + return left; + } + + return expression; + } + + @Override + public FilterExpression visitOrExpression(OrFilterExpression expression) { + FilterExpression left = expression.getLeft().accept(this); + FilterExpression right = expression.getRight().accept(this); + + if (left == null || right == null) { + return null; + } + return expression; + } + + @Override + public FilterExpression visitNotExpression(NotFilterExpression expression) { + FilterExpression inner = expression.getNegated().accept(this); + + if (inner == null) { + return null; + } + + return expression; + } + + /** + * @param dictionary + * @param expression + * @return A filter expression that can be safely executed in the data store. + */ + public static FilterExpression extractPushDownPredicate(EntityDictionary dictionary, FilterExpression expression) { + FilterExpressionNormalizationVisitor normalizationVisitor = new FilterExpressionNormalizationVisitor(); + FilterExpression normalizedExpression = expression.accept(normalizationVisitor); + FilterPredicatePushdownExtractor verifier = new FilterPredicatePushdownExtractor(dictionary); + + return normalizedExpression.accept(verifier); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryExecutionVerifier.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryExecutionVerifier.java new file mode 100644 index 0000000000..9242411823 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryExecutionVerifier.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core.filter.expression; + +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.filter.FilterPredicate; + +/** + * Intended to specify whether the expression must be evaluated in memory or can be pushed to the DataStore. + * Constructs true if any part of the expression relies on a computed attribute or relationship. + * Otherwise constructs false. + */ +public class InMemoryExecutionVerifier implements FilterExpressionVisitor { + private EntityDictionary dictionary; + + public InMemoryExecutionVerifier(EntityDictionary dictionary) { + this.dictionary = dictionary; + } + + @Override + public Boolean visitPredicate(FilterPredicate filterPredicate) { + for (Path.PathElement pathElement : filterPredicate.getPath().getPathElements()) { + Class entityClass = pathElement.getType(); + String fieldName = pathElement.getFieldName(); + + if (dictionary.isComputed(entityClass, fieldName)) { + return true; + } + } + return false; + } + + @Override + public Boolean visitAndExpression(AndFilterExpression expression) { + Boolean left = expression.getLeft().accept(this); + Boolean right = expression.getRight().accept(this); + return (left || right); + } + + @Override + public Boolean visitOrExpression(OrFilterExpression expression) { + Boolean left = expression.getLeft().accept(this); + Boolean right = expression.getRight().accept(this); + return (left || right); + } + + @Override + public Boolean visitNotExpression(NotFilterExpression expression) { + return expression.getNegated().accept(this); + } + + /** + * @param dictionary + * @param expression + * @return Returns true if the filter expression must be evaluated in memory. + */ + public static boolean shouldExecuteInMemory(EntityDictionary dictionary, FilterExpression expression) { + InMemoryExecutionVerifier verifier = new InMemoryExecutionVerifier(dictionary); + + return expression.accept(verifier); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryFilterExecutor.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryFilterExecutor.java new file mode 100644 index 0000000000..2e342251e5 --- /dev/null +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryFilterExecutor.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.filter.expression; + +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.filter.FilterPredicate; + +import java.util.function.Predicate; + +/** + * Visitor for in memory filterExpressions. + */ +public class InMemoryFilterExecutor implements FilterExpressionVisitor { + private final RequestScope requestScope; + + public InMemoryFilterExecutor(RequestScope requestScope) { + this.requestScope = requestScope; + } + + @Override + public Predicate visitPredicate(FilterPredicate filterPredicate) { + return filterPredicate.apply(requestScope); + } + + @Override + public Predicate visitAndExpression(AndFilterExpression expression) { + Predicate leftPredicate = expression.getLeft().accept(this); + Predicate rightPredicate = expression.getRight().accept(this); + return t -> leftPredicate.and(rightPredicate).test(t); + } + + @Override + public Predicate visitOrExpression(OrFilterExpression expression) { + Predicate leftPredicate = expression.getLeft().accept(this); + Predicate rightPredicate = expression.getRight().accept(this); + return t -> leftPredicate.or(rightPredicate).test(t); + } + + @Override + public Predicate visitNotExpression(NotFilterExpression expression) { + Predicate predicate = expression.getNegated().accept(this); + return t -> predicate.negate().test(t); + } +} diff --git a/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryFilterVisitor.java b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryFilterVisitor.java index 3d2738364f..ff8654f60a 100644 --- a/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryFilterVisitor.java +++ b/elide-core/src/main/java/com/yahoo/elide/core/filter/expression/InMemoryFilterVisitor.java @@ -1,47 +1,20 @@ /* - * Copyright 2016, Yahoo Inc. + * Copyright 2019, Yahoo Inc. * Licensed under the Apache License, Version 2.0 * See LICENSE file in project root for terms. */ package com.yahoo.elide.core.filter.expression; import com.yahoo.elide.core.RequestScope; -import com.yahoo.elide.core.filter.FilterPredicate; - -import java.util.function.Predicate; /** - * Visitor for in memory filterExpressions. + * Deprecated visitor for in memory filterExpressions. + * @deprecated use {@link InMemoryFilterExecutor} */ -public class InMemoryFilterVisitor implements FilterExpressionVisitor { - private final RequestScope requestScope; +@Deprecated +public class InMemoryFilterVisitor extends InMemoryFilterExecutor { public InMemoryFilterVisitor(RequestScope requestScope) { - this.requestScope = requestScope; - } - - @Override - public Predicate visitPredicate(FilterPredicate filterPredicate) { - return filterPredicate.apply(requestScope); - } - - @Override - public Predicate visitAndExpression(AndFilterExpression expression) { - Predicate leftPredicate = expression.getLeft().accept(this); - Predicate rightPredicate = expression.getRight().accept(this); - return t -> leftPredicate.and(rightPredicate).test(t); - } - - @Override - public Predicate visitOrExpression(OrFilterExpression expression) { - Predicate leftPredicate = expression.getLeft().accept(this); - Predicate rightPredicate = expression.getRight().accept(this); - return t -> leftPredicate.or(rightPredicate).test(t); - } - - @Override - public Predicate visitNotExpression(NotFilterExpression expression) { - Predicate predicate = expression.getNegated().accept(this); - return t -> predicate.negate().test(t); + super(requestScope); } } diff --git a/elide-core/src/main/java/com/yahoo/elide/utils/coerce/converters/ISO8601DateSerde.java b/elide-core/src/main/java/com/yahoo/elide/utils/coerce/converters/ISO8601DateSerde.java index 25cd37d276..ef28ae115a 100644 --- a/elide-core/src/main/java/com/yahoo/elide/utils/coerce/converters/ISO8601DateSerde.java +++ b/elide-core/src/main/java/com/yahoo/elide/utils/coerce/converters/ISO8601DateSerde.java @@ -6,8 +6,8 @@ package com.yahoo.elide.utils.coerce.converters; import org.apache.commons.lang3.ClassUtils; +import org.apache.commons.lang3.time.FastDateFormat; -import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.TimeZone; @@ -17,7 +17,7 @@ */ public class ISO8601DateSerde implements Serde { - protected DateFormat df; + protected FastDateFormat df; protected Class targetType; public ISO8601DateSerde(SimpleDateFormat df) { @@ -25,8 +25,7 @@ public ISO8601DateSerde(SimpleDateFormat df) { } public ISO8601DateSerde(SimpleDateFormat df, Class targetType) { - this.df = df; - this.targetType = targetType; + this(df.toPattern(), df.getTimeZone(), targetType); } public ISO8601DateSerde(String formatString, TimeZone tz) { @@ -34,8 +33,7 @@ public ISO8601DateSerde(String formatString, TimeZone tz) { } public ISO8601DateSerde(String formatString, TimeZone tz, Class targetType) { - df = new SimpleDateFormat(formatString); - df.setTimeZone(tz); + this.df = FastDateFormat.getInstance(formatString, tz); this.targetType = targetType; } @@ -46,10 +44,11 @@ public ISO8601DateSerde() { @Override public Date deserialize(String val) { Date date; + try { date = df.parse(val); } catch (java.text.ParseException e) { - throw new IllegalArgumentException("Date strings must be formated as yyyy-MM-dd'T'HH:mm'Z'"); + throw new IllegalArgumentException("Date strings must be formated as " + df.getPattern()); } if (ClassUtils.isAssignable(targetType, java.sql.Date.class)) { diff --git a/elide-core/src/test/java/com/yahoo/elide/core/LifeCycleTest.java b/elide-core/src/test/java/com/yahoo/elide/core/LifeCycleTest.java index d232917741..fca4822f41 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/LifeCycleTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/LifeCycleTest.java @@ -39,6 +39,7 @@ import com.yahoo.elide.annotation.OnUpdatePreCommit; import com.yahoo.elide.annotation.OnUpdatePreSecurity; import com.yahoo.elide.audit.AuditLogger; +import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore; import com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore; import com.yahoo.elide.functions.LifeCycleHook; import com.yahoo.elide.security.ChangeSpec; @@ -57,6 +58,7 @@ import org.testng.annotations.Test; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Map; @@ -178,11 +180,12 @@ public void testElideGet() throws Exception { DataStore store = mock(DataStore.class); DataStoreTransaction tx = mock(DataStoreTransaction.class); Book book = mock(Book.class); + when(book.getId()).thenReturn(1L); Elide elide = getElide(store, dictionary, MOCK_AUDIT_LOGGER); when(store.beginReadTransaction()).thenReturn(tx); - when(tx.loadObject(eq(Book.class), eq(1L), any(), any())).thenReturn(book); + when(tx.loadObjects(eq(Book.class), any(), any(), any(), isA(RequestScope.class))).thenReturn(Collections.singletonList(book)); MultivaluedMap headers = new MultivaluedHashMap<>(); elide.get("/book/1", headers, null); @@ -211,7 +214,7 @@ public void testElidePatch() throws Exception { when(book.getId()).thenReturn(1L); when(store.beginTransaction()).thenReturn(tx); - when(tx.loadObject(eq(Book.class), eq(1L), any(), any())).thenReturn(book); + when(tx.loadObjects(eq(Book.class), any(), any(), any(), isA(RequestScope.class))).thenReturn(Collections.singletonList(book)); String bookBody = "{\"data\":{\"type\":\"book\",\"id\":1,\"attributes\": {\"title\":\"Grapes of Wrath\"}}}"; @@ -250,7 +253,7 @@ public void testElideDelete() throws Exception { when(book.getId()).thenReturn(1L); when(store.beginTransaction()).thenReturn(tx); - when(tx.loadObject(eq(Book.class), eq(1L), any(), any())).thenReturn(book); + when(tx.loadObjects(eq(Book.class), any(), any(), any(), isA(RequestScope.class))).thenReturn(Collections.singletonList(book)); elide.delete("/book/1", "", null); /* @@ -943,14 +946,15 @@ public void deletePostCommit(RequestScope scope) { */ @Test public void testAddToCollectionTrigger() { - InMemoryDataStore store = new InMemoryDataStore(Book.class.getPackage()); + HashMapDataStore wrapped = new HashMapDataStore(Book.class.getPackage()); + InMemoryDataStore store = new InMemoryDataStore(wrapped); HashMap> checkMappings = new HashMap<>(); checkMappings.put("Book operation check", Book.BookOperationCheck.class); checkMappings.put("Field path editor check", Editor.FieldPathFilterExpression.class); store.populateEntityDictionary(new EntityDictionary(checkMappings)); DataStoreTransaction tx = store.beginTransaction(); - RequestScope scope = new RequestScope(null, null, tx, new User(1), null, getElideSettings(null, store.getDictionary(), MOCK_AUDIT_LOGGER), false); + RequestScope scope = new RequestScope(null, null, tx, new User(1), null, getElideSettings(null, wrapped.getDictionary(), MOCK_AUDIT_LOGGER), false); PersistentResource publisherResource = PersistentResource.createObject(null, Publisher.class, scope, Optional.of("1")); PersistentResource book1Resource = PersistentResource.createObject(publisherResource, Book.class, scope, Optional.of("1")); @@ -966,7 +970,7 @@ public void testAddToCollectionTrigger() { /* Only the creat hooks should be triggered */ assertFalse(publisher.isUpdateHookInvoked()); - scope = new RequestScope(null, null, tx, new User(1), null, getElideSettings(null, store.getDictionary(), MOCK_AUDIT_LOGGER), false); + scope = new RequestScope(null, null, tx, new User(1), null, getElideSettings(null, wrapped.getDictionary(), MOCK_AUDIT_LOGGER), false); PersistentResource book2Resource = PersistentResource.createObject(publisherResource, Book.class, scope, Optional.of("2")); publisherResource = PersistentResource.loadRecord(Publisher.class, "1", scope); @@ -983,14 +987,15 @@ public void testAddToCollectionTrigger() { */ @Test public void testRemoveFromCollectionTrigger() { - InMemoryDataStore store = new InMemoryDataStore(Book.class.getPackage()); + HashMapDataStore wrapped = new HashMapDataStore(Book.class.getPackage()); + InMemoryDataStore store = new InMemoryDataStore(wrapped); HashMap> checkMappings = new HashMap<>(); checkMappings.put("Book operation check", Book.BookOperationCheck.class); checkMappings.put("Field path editor check", Editor.FieldPathFilterExpression.class); store.populateEntityDictionary(new EntityDictionary(checkMappings)); DataStoreTransaction tx = store.beginTransaction(); - RequestScope scope = new RequestScope(null, null, tx, new User(1), null, getElideSettings(null, store.getDictionary(), MOCK_AUDIT_LOGGER), false); + RequestScope scope = new RequestScope(null, null, tx, new User(1), null, getElideSettings(null, wrapped.getDictionary(), MOCK_AUDIT_LOGGER), false); PersistentResource publisherResource = PersistentResource.createObject(null, Publisher.class, scope, Optional.of("1")); PersistentResource book1Resource = PersistentResource.createObject(publisherResource, Book.class, scope, Optional.of("1")); @@ -1007,7 +1012,7 @@ public void testRemoveFromCollectionTrigger() { /* Only the creat hooks should be triggered */ assertFalse(publisher.isUpdateHookInvoked()); - scope = new RequestScope(null, null, tx, new User(1), null, getElideSettings(null, store.getDictionary(), MOCK_AUDIT_LOGGER), false); + scope = new RequestScope(null, null, tx, new User(1), null, getElideSettings(null, wrapped.getDictionary(), MOCK_AUDIT_LOGGER), false); book2Resource = PersistentResource.createObject(publisherResource, Book.class, scope, Optional.of("2")); publisherResource = PersistentResource.loadRecord(Publisher.class, "1", scope); diff --git a/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceTest.java b/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceTest.java index ce0114103b..84fd6cd100 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/PersistentResourceTest.java @@ -102,9 +102,9 @@ public class PersistentResourceTest extends PersistenceResourceTestSetup { private final RequestScope badUserScope; public PersistentResourceTest() { - goodUserScope = new RequestScope(null, null, mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS), + goodUserScope = new RequestScope(null, null, mock(DataStoreTransaction.class), new User(1), null, elideSettings, false); - badUserScope = new RequestScope(null, null, mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS), + badUserScope = new RequestScope(null, null, mock(DataStoreTransaction.class), new User(-1), null, elideSettings, false); } @@ -131,6 +131,8 @@ public void init() { dictionary.bindEntity(ContainerWithPackageShare.class); dictionary.bindEntity(ShareableWithPackageShare.class); dictionary.bindEntity(UnshareableWithEntityUnshare.class); + + reset(goodUserScope.getTransaction()); } @Test @@ -140,7 +142,7 @@ public void testUpdateToOneRelationHookInAddRelation() { User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); @@ -159,7 +161,7 @@ public void testUpdateToOneRelationHookInUpdateRelation() { fun.setRelation1(Sets.newHashSet(child1)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); @@ -178,7 +180,7 @@ public void testUpdateToOneRelationHookInRemoveRelation() { fun.setRelation3(child); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); @@ -196,7 +198,9 @@ public void testUpdateToOneRelationHookInClearRelation() { fun.setRelation3(child1); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(fun), eq("relation3"), any(), any(), any(), any())).thenReturn(child1); + RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); funResource.clearRelation("relation3"); @@ -212,7 +216,7 @@ public void testUpdateToManyRelationHookInAddRelationBidirection() { User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource parentResource = new PersistentResource<>(parent, null, "3", goodScope); @@ -232,7 +236,7 @@ public void testUpdateToManyRelationHookInRemoveRelationBidirection() { child.setParents(Sets.newHashSet(parent)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource parentResource = new PersistentResource<>(parent, null, "3", goodScope); @@ -250,12 +254,14 @@ public void testUpdateToManyRelationHookInClearRelationBidirection() { Parent parent = new Parent(); Child child1 = newChild(1); Child child2 = newChild(2); - parent.setChildren(Sets.newHashSet(child1, child2)); + Set children = Sets.newHashSet(child1, child2); + parent.setChildren(children); child1.setParents(Sets.newHashSet(parent)); child2.setParents(Sets.newHashSet(parent)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(children); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource parentResource = new PersistentResource<>(parent, null, "3", goodScope); @@ -275,12 +281,14 @@ public void testUpdateToManyRelationHookInUpdateRelationBidirection() { Child child1 = newChild(1); Child child2 = newChild(2); Child child3 = newChild(3); - parent.setChildren(Sets.newHashSet(child1, child2)); + Set children = Sets.newHashSet(child1, child2); + parent.setChildren(children); child1.setParents(Sets.newHashSet(parent)); child2.setParents(Sets.newHashSet(parent)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(children); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource parentResource = new PersistentResource<>(parent, null, "3", goodScope); @@ -301,7 +309,7 @@ public void testUpdateToManyRelationHookInUpdateRelationBidirection() { @Test public void testSetAttributeHookInUpdateAttribute() { Parent parent = newParent(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -339,7 +347,7 @@ public void testGetRelationships() { public void testNoCreate() { Assert.assertNotNull(dictionary); NoCreateEntity noCreate = new NoCreateEntity(); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); when(tx.createNewObject(NoCreateEntity.class)).thenReturn(noCreate); @@ -586,7 +594,7 @@ public void testAddBidirectionalRelation() { @Test public void testSuccessfulOneToOneRelationshipAdd() throws Exception { User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); Left left = new Left(); Right right = new Right(); left.setId(2); @@ -659,7 +667,8 @@ public void testSuccessfulOneToOneRelationshipAddNull() throws Exception { */ public void testSuccessfulManyToManyRelationshipUpdate() throws Exception { User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + Parent parent = new Parent(); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -681,6 +690,8 @@ public void testSuccessfulManyToManyRelationshipUpdate() throws Exception { parent.setChildren(allChildren); parent.setSpouses(Sets.newHashSet()); + when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(allChildren); + PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); //Requested = (3,6) @@ -772,10 +783,13 @@ public void testGetRelationSuccess() { Child child1 = newChild(1); Child child2 = newChild(2); Child child3 = newChild(3); - fun.setRelation2(Sets.newHashSet(child1, child2, child3)); + Set children = Sets.newHashSet(child1, child2, child3); + fun.setRelation2(children); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodUserScope); + when(goodUserScope.getTransaction().getRelation(any(), eq(fun), eq("relation2"), any(), any(), any(), any())).thenReturn(children); + Set results = getRelation(funResource, "relation2"); Assert.assertEquals(results.size(), 3, "All of relation elements should be returned."); @@ -787,10 +801,14 @@ public void testGetRelationFilteredSuccess() { Child child1 = newChild(1); Child child2 = newChild(-2); Child child3 = newChild(3); + + Set children = Sets.newHashSet(child1, child2, child3); fun.setRelation2(Sets.newHashSet(child1, child2, child3)); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodUserScope); + when(goodUserScope.getTransaction().getRelation(any(), eq(fun), eq("relation2"), any(), any(), any(), any())).thenReturn(children); + Set results = getRelation(funResource, "relation2"); Assert.assertEquals(results.size(), 2, "Only filtered relation elements should be returned."); @@ -828,10 +846,10 @@ public void testGetSingleRelationInMemory() { Child child1 = newChild(1, "paul john"); Child child2 = newChild(2, "john buzzard"); Child child3 = newChild(3, "chris smith"); - parent.setChildren(Sets.newHashSet(child1, child2, child3)); + Set children = Sets.newHashSet(child1, child2, child3); + parent.setChildren(children); - DataStoreTransaction tx = mock(DataStoreTransaction.class); - when(tx.getRelation(eq(tx), any(), any(), any(), any(), any(), any())).thenReturn(Sets.newHashSet(child1, child2, child3)); + when(goodUserScope.getTransaction().getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(children); PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodUserScope); @@ -971,7 +989,7 @@ void testDeleteResourceSuccess() { Parent parent = newParent(1); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); @@ -991,7 +1009,7 @@ void testDeleteCascades() { item.setInvoice(invoice); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource invoiceResource = new PersistentResource<>(invoice, null, "1", goodScope); @@ -1011,12 +1029,16 @@ void testDeleteResourceUpdateRelationshipSuccess() { Child child = newChild(100); parent.setChildren(Sets.newHashSet(child)); parent.setSpouses(Sets.newHashSet()); + + Set parents = Sets.newHashSet(parent); child.setParents(Sets.newHashSet(parent)); Assert.assertFalse(parent.getChildren().isEmpty()); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(child), eq("parents"), any(), any(), any(), any())).thenReturn(parents); + RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); @@ -1035,7 +1057,7 @@ void testDeleteResourceForbidden() { NoDeleteEntity nodelete = new NoDeleteEntity(); nodelete.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1056,7 +1078,7 @@ void testAddRelationSuccess() { User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); @@ -1079,7 +1101,7 @@ void testAddRelationForbiddenByField() { User badUser = new User(-1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope badScope = new RequestScope(null, null, tx, badUser, null, elideSettings, false); PersistentResource funResource = new PersistentResource<>(fun, null, "3", badScope); PersistentResource childResource = new PersistentResource<>(child, null, "1", badScope); @@ -1093,7 +1115,7 @@ void testAddRelationForbiddenByEntity() { Child child = newChild(2); noUpdate.setChildren(Sets.newHashSet()); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1110,7 +1132,7 @@ public void testAddRelationInvalidRelation() { User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource funResource = new PersistentResource<>(fun, null, "3", goodScope); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); @@ -1126,7 +1148,7 @@ public void testRemoveToManyRelationSuccess() { Parent parent3 = newParent(3, child); child.setParents(Sets.newHashSet(parent1, parent2, parent3)); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); @@ -1151,7 +1173,7 @@ public void testRemoveToOneRelationSuccess() { Child child = newChild(1); fun.setRelation3(child); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1176,14 +1198,22 @@ public void testNoSaveNonModifications() { Parent parent = new Parent(); fun.setRelation3(child); - fun.setRelation1(Sets.newHashSet(child)); - parent.setChildren(Sets.newHashSet(child)); + Set children1 = Sets.newHashSet(child); + fun.setRelation1(children1); + + Set children2 = Sets.newHashSet(child); + parent.setChildren(children2); parent.setFirstName("Leeroy"); child.setReadNoAccess(secret); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(fun), eq("relation3"), any(), any(), any(), any())).thenReturn(child); + when(tx.getRelation(any(), eq(fun), eq("relation1"), any(), any(), any(), any())).thenReturn(children1); + when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(children2); + when(tx.getRelation(any(), eq(child), eq("readNoAccess"), any(), any(), any(), any())).thenReturn(secret); + User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1236,7 +1266,7 @@ public void testRemoveNonexistingToOneRelation() { Child unownedChild = newChild(2); fun.setRelation3(ownedChild); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1261,7 +1291,7 @@ public void testRemoveNonexistingToManyRelation() { Parent unownedParent = newParent(4, null); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource childResource = new PersistentResource<>(child, null, "1", goodScope); @@ -1285,9 +1315,12 @@ public void testClearToManyRelationSuccess() { Parent parent1 = newParent(1, child); Parent parent2 = newParent(2, child); Parent parent3 = newParent(3, child); - child.setParents(Sets.newHashSet(parent1, parent2, parent3)); + Set parents = Sets.newHashSet(parent1, parent2, parent3); + child.setParents(parents); + + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(child), eq("parents"), any(), any(), any(), any())).thenReturn(parents); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1313,7 +1346,10 @@ public void testClearToOneRelationSuccess() { Child child = newChild(1); fun.setRelation3(child); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + + when(tx.getRelation(any(), eq(fun), eq("relation3"), any(), any(), any(), any())).thenReturn(child); + User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodScope); @@ -1329,7 +1365,7 @@ public void testClearToOneRelationSuccess() { @Test() public void testClearRelationFilteredByReadAccess() { User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); Parent parent = new Parent(); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1353,6 +1389,8 @@ public void testClearRelationFilteredByReadAccess() { parent.setChildren(allChildren); parent.setSpouses(Sets.newHashSet()); + when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(allChildren); + PersistentResource parentResource = new PersistentResource<>(parent, null, "1", goodScope); //Final set after operation = (4,5) @@ -1389,7 +1427,7 @@ public void testClearRelationInvalidToOneUpdatePermission() { left.setNoUpdateOne2One(right); right.setNoUpdateOne2One(left); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource leftResource = new PersistentResource<>(left, null, "1", goodScope); @@ -1407,7 +1445,7 @@ public void testNoChangeRelationInvalidToOneUpdatePermission() { left.setNoUpdateOne2One(right); right.setNoUpdateOne2One(left); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource leftResource = new PersistentResource<>(left, null, "1", goodScope); @@ -1424,12 +1462,16 @@ public void testClearRelationInvalidToManyUpdatePermission() { right1.setId(1); Right right2 = new Right(); right2.setId(2); - left.setNoInverseUpdate(Sets.newHashSet(right1, right2)); + + Set noInverseUpdate = Sets.newHashSet(right1, right2); + left.setNoInverseUpdate(noInverseUpdate); right1.setNoUpdate(Sets.newHashSet(left)); right2.setNoUpdate(Sets.newHashSet(left)); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + + when(tx.getRelation(any(), eq(left), eq("noInverseUpdate"), any(), any(), any(), any())).thenReturn(noInverseUpdate); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource leftResource = new PersistentResource<>(left, null, "1", goodScope); @@ -1446,7 +1488,9 @@ public void testClearRelationInvalidToOneDeletePermission() { noDelete.setId(1); left.setNoDeleteOne2One(noDelete); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + + when(tx.getRelation(any(), eq(left), eq("noDeleteOne2One"), any(), any(), any(), any())).thenReturn(noDelete); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource leftResource = new PersistentResource<>(left, null, "1", goodScope); @@ -1461,7 +1505,7 @@ public void testClearRelationInvalidRelation() { Child child = newChild(1); fun.setRelation3(child); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource funResource = new PersistentResource<>(fun, null, "1", goodScope); @@ -1472,7 +1516,7 @@ public void testClearRelationInvalidRelation() { public void testUpdateAttributeSuccess() { Parent parent = newParent(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1489,7 +1533,7 @@ public void testUpdateAttributeSuccess() { public void testUpdateAttributeInvalidAttribute() { Parent parent = newParent(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1503,7 +1547,7 @@ public void testUpdateAttributeInvalidUpdatePermission() { fun.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User badUser = new User(-1); RequestScope badScope = new RequestScope(null, null, tx, badUser, null, elideSettings, false); @@ -1519,7 +1563,7 @@ public void testUpdateAttributeInvalidUpdatePermissionNoChange() { FunWithPermissions fun = new FunWithPermissions(); fun.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User badUser = new User(-1); RequestScope badScope = new RequestScope(null, null, tx, badUser, null, elideSettings, false); @@ -1538,7 +1582,7 @@ public void testLoadRecords() { Child child4 = newChild(4); Child child5 = newChild(5); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); when(tx.loadObjects(eq(Child.class), any(), any(), any(), any(RequestScope.class))) @@ -1564,7 +1608,7 @@ public void testLoadRecords() { public void testLoadRecordSuccess() { Child child1 = newChild(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); when(tx.loadObject(eq(Child.class), eq(1L), any(), any())).thenReturn(child1); @@ -1577,7 +1621,7 @@ public void testLoadRecordSuccess() { @Test(expectedExceptions = InvalidObjectIdentifierException.class) public void testLoadRecordInvalidId() { - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); when(tx.loadObject(eq(Child.class), eq("1"), any(), any())).thenReturn(null); @@ -1590,7 +1634,7 @@ public void testLoadRecordInvalidId() { public void testLoadRecordForbidden() { NoReadEntity noRead = new NoReadEntity(); noRead.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); when(tx.loadObject(eq(NoReadEntity.class), eq(1L), any(), any())).thenReturn(noRead); @@ -1602,7 +1646,7 @@ public void testLoadRecordForbidden() { @Test() public void testCreateObjectSuccess() { Parent parent = newParent(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); when(tx.createNewObject(Parent.class)).thenReturn(parent); @@ -1621,7 +1665,7 @@ public void testCreateObjectSuccess() { public void testCreateObjectForbidden() { NoCreateEntity noCreate = new NoCreateEntity(); noCreate.setId(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User goodUser = new User(1); when(tx.createNewObject(NoCreateEntity.class)).thenReturn(noCreate); @@ -1638,12 +1682,15 @@ public void testDeletePermissionCheckedOnInverseRelationship() { Right right = new Right(); right.setId(2); + Set rights = Sets.newHashSet(right); left.setFieldLevelDelete(Sets.newHashSet(right)); right.setAllowDeleteAtFieldLevel(Sets.newHashSet(left)); //Bad User triggers the delete permission failure User badUser = new User(-1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(left), eq("fieldLevelDelete"), any(), any(), any(), any())).thenReturn(rights); + RequestScope badScope = new RequestScope(null, null, tx, badUser, null, elideSettings, false); PersistentResource leftResource = new PersistentResource<>(left, null, badScope.getUUIDFor(left), badScope); @@ -1658,14 +1705,17 @@ public void testUpdatePermissionCheckedOnInverseRelationship() { left.setId(1); Right right = new Right(); - left.setNoInverseUpdate(Sets.newHashSet(right)); + Set rights = Sets.newHashSet(right); + left.setNoInverseUpdate(rights); right.setNoUpdate(Sets.newHashSet(left)); List empty = new ArrayList<>(); Relationship ids = new Relationship(null, new Data<>(empty)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(left), eq("noInverseUpdate"), any(), any(), any(), any())).thenReturn(rights); + RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource leftResource = new PersistentResource<>(left, null, goodScope.getUUIDFor(left), goodScope); @@ -1725,7 +1775,9 @@ public void testOwningRelationshipInverseUpdates() { Child child = newChild(2); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(parent.getChildren()); + RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource parentResource = new PersistentResource<>(parent, null, goodScope.getUUIDFor(parent), goodScope); @@ -1745,6 +1797,8 @@ public void testOwningRelationshipInverseUpdates() { Assert.assertTrue(child.getParents().contains(parent), "The non-owning relationship should also be updated"); reset(tx); + when(tx.getRelation(any(), eq(parent), eq("children"), any(), any(), any(), any())).thenReturn(parent.getChildren()); + parentResource.clearRelation("children"); goodScope.saveOrCreateObjects(); @@ -1782,7 +1836,7 @@ public void testSharePermissionErrorOnUpdateSingularRelationship() { Relationship ids = new Relationship(null, new Data<>(idList)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); when(tx.loadObject(eq(NoShareEntity.class), eq(1L), any(), any())).thenReturn(noShare); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1803,7 +1857,7 @@ public void testSharePermissionErrorOnUpdateRelationshipPackageLevel() { Relationship unShareales = new Relationship(null, new Data<>(unShareableList)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); when(tx.loadObject(eq(UnshareableWithEntityUnshare.class), eq(1L), any(), any())).thenReturn(unshareableWithEntityUnshare); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1824,7 +1878,7 @@ public void testSharePermissionSuccessOnUpdateManyRelationshipPackageLevel() { Relationship shareables = new Relationship(null, new Data<>(shareableList)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); when(tx.loadObject(eq(ShareableWithPackageShare.class), eq(1L), any(), any())).thenReturn(shareableWithPackageShare); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1852,7 +1906,7 @@ public void testSharePermissionErrorOnUpdateManyRelationship() { Relationship ids = new Relationship(null, new Data<>(idList)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); when(tx.loadObject(eq(NoShareEntity.class), eq(1L), any(), any())).thenReturn(noShare1); when(tx.loadObject(eq(NoShareEntity.class), eq(2L), any(), any())).thenReturn(noShare2); @@ -1881,8 +1935,9 @@ public void testSharePermissionSuccessOnUpdateManyRelationship() { Relationship ids = new Relationship(null, new Data<>(idList)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); when(tx.loadObject(eq(NoShareEntity.class), eq(1L), any(), any())).thenReturn(noShare1); + when(tx.getRelation(any(), eq(userModel), eq("noShares"), any(), any(), any(), any())).thenReturn(noshares); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource userResource = new PersistentResource<>(userModel, null, goodScope.getUUIDFor(userModel), goodScope); @@ -1909,7 +1964,9 @@ public void testSharePermissionSuccessOnUpdateSingularRelationship() { Relationship ids = new Relationship(null, new Data<>(idList)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + + when(tx.getRelation(any(), eq(userModel), eq("noShare"), any(), any(), any(), any())).thenReturn(noShare); when(tx.loadObject(eq(NoShareEntity.class), eq(1L), any(), any())).thenReturn(noShare); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); @@ -1935,7 +1992,8 @@ public void testSharePermissionSuccessOnClearSingularRelationship() { Relationship ids = new Relationship(null, new Data<>(empty)); User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); + when(tx.getRelation(any(), eq(userModel), eq("noShare"), any(), any(), any(), any())).thenReturn(noShare); RequestScope goodScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); PersistentResource userResource = new PersistentResource<>(userModel, null, goodScope.getUUIDFor(userModel), goodScope); @@ -1955,10 +2013,17 @@ public void testCollectionChangeSpecType() { } return condFn.apply((Collection) spec.getOriginal(), (Collection) spec.getModified()); }; + + DataStoreTransaction tx = mock(DataStoreTransaction.class); // Ensure that change specs coming from collections work properly - PersistentResource model = bootstrapPersistentResource(new ChangeSpecModel((spec) -> collectionCheck + + ChangeSpecModel csModel = new ChangeSpecModel((spec) -> collectionCheck .apply("testColl") - .apply(spec, (original, modified) -> original == null && modified.equals(Arrays.asList("a", "b", "c"))))); + .apply(spec, (original, modified) -> original == null && modified.equals(Arrays.asList("a", "b", "c")))); + + PersistentResource model = bootstrapPersistentResource(csModel, tx); + + when(tx.getRelation(any(), eq(model.obj), eq("otherKids"), any(), any(), any(), any())).thenReturn(new HashSet<>()); /* Attributes */ // Set new data from null @@ -1977,20 +2042,29 @@ public void testCollectionChangeSpecType() { /* ToMany relationships */ // Learn about the other kids model.getObject().checkFunction = (spec) -> collectionCheck.apply("otherKids").apply(spec, (original, modified) -> (original == null || original.isEmpty()) && modified.size() == 1 && modified.contains(new ChangeSpecChild(1))); - Assert.assertTrue(model.updateRelation("otherKids", Sets.newHashSet(bootstrapPersistentResource(new ChangeSpecChild(1))))); + + ChangeSpecChild child1 = new ChangeSpecChild(1); + Assert.assertTrue(model.updateRelation("otherKids", Sets.newHashSet(bootstrapPersistentResource(child1)))); // Add individual model.getObject().checkFunction = (spec) -> collectionCheck.apply("otherKids").apply(spec, (original, modified) -> original.equals(Collections.singletonList(new ChangeSpecChild(1))) && modified.size() == 2 && modified.contains(new ChangeSpecChild(1)) && modified.contains(new ChangeSpecChild(2))); - model.addRelation("otherKids", bootstrapPersistentResource(new ChangeSpecChild(2))); + + ChangeSpecChild child2 = new ChangeSpecChild(2); + model.addRelation("otherKids", bootstrapPersistentResource(child2)); + model.getObject().checkFunction = (spec) -> collectionCheck.apply("otherKids").apply(spec, (original, modified) -> original.size() == 2 && original.contains(new ChangeSpecChild(1)) && original.contains(new ChangeSpecChild(2)) && modified.size() == 3 && modified.contains(new ChangeSpecChild(1)) && modified.contains(new ChangeSpecChild(2)) && modified.contains(new ChangeSpecChild(3))); - model.addRelation("otherKids", bootstrapPersistentResource(new ChangeSpecChild(3))); + + ChangeSpecChild child3 = new ChangeSpecChild(3); + model.addRelation("otherKids", bootstrapPersistentResource(child3)); // Remove one model.getObject().checkFunction = (spec) -> collectionCheck.apply("otherKids").apply(spec, (original, modified) -> original.size() == 3 && original.contains(new ChangeSpecChild(1)) && original.contains(new ChangeSpecChild(2)) && original.contains(new ChangeSpecChild(3)) && modified.size() == 2 && modified.contains(new ChangeSpecChild(1)) && modified.contains(new ChangeSpecChild(3))); - model.removeRelation("otherKids", bootstrapPersistentResource(new ChangeSpecChild(2))); + model.removeRelation("otherKids", bootstrapPersistentResource(child2)); + when(tx.getRelation(any(), eq(model.obj), eq("otherKids"), any(), any(), any(), any())).thenReturn(Sets.newHashSet(child1, child3)); // Clear the rest - model.getObject().checkFunction = (spec) -> collectionCheck.apply("otherKids").apply(spec, (original, modified) -> original.size() <= 2 && modified.size() < original.size()); + model.getObject().checkFunction = (spec) -> collectionCheck.apply("otherKids").apply(spec, (original, modified) + -> original.size() <= 2 && modified.size() < original.size()); model.clearRelation("otherKids"); } @@ -2018,23 +2092,40 @@ public void testAttrChangeSpecType() { @Test public void testRelationChangeSpecType() { - BiFunction, Boolean> relCheck = (spec, checkFn) -> { - if (!(spec.getModified() instanceof ChangeSpecChild) && spec.getModified() != null) { - return false; - } - if (!"child".equals(spec.getFieldName())) { - return false; - } - return checkFn.apply((ChangeSpecChild) spec.getOriginal(), (ChangeSpecChild) spec.getModified()); - }; - PersistentResource model = bootstrapPersistentResource(new ChangeSpecModel((spec) -> relCheck.apply(spec, (original, modified) -> (original == null) && new ChangeSpecChild(1).equals(modified)))); - Assert.assertTrue(model.updateRelation("child", Sets.newHashSet(bootstrapPersistentResource(new ChangeSpecChild(1))))); + try { + BiFunction, Boolean> relCheck = (spec, checkFn) -> { + if (!(spec.getModified() instanceof ChangeSpecChild) && spec.getModified() != null) { + return false; + } + if (!"child".equals(spec.getFieldName())) { + return false; + } + return checkFn.apply((ChangeSpecChild) spec.getOriginal(), (ChangeSpecChild) spec.getModified()); + }; + DataStoreTransaction tx = mock(DataStoreTransaction.class); + + PersistentResource model = bootstrapPersistentResource(new ChangeSpecModel((spec) + -> relCheck.apply(spec, (original, modified) + -> (original == null) && new ChangeSpecChild(1).equals(modified))), tx); + + when(tx.getRelation(any(), eq(model.obj), eq("child"), any(), any(), any(), any())).thenReturn(null); + + ChangeSpecChild child1 = new ChangeSpecChild(1); + Assert.assertTrue(model.updateRelation("child", Sets.newHashSet(bootstrapPersistentResource(child1, tx)))); + when(tx.getRelation(any(), eq(model.obj), eq("child"), any(), any(), any(), any())).thenReturn(child1); + + model.getObject().checkFunction = (spec) -> relCheck.apply(spec, (original, modified) -> new ChangeSpecChild(1).equals(original) && new ChangeSpecChild(2).equals(modified)); - model.getObject().checkFunction = (spec) -> relCheck.apply(spec, (original, modified) -> new ChangeSpecChild(1).equals(original) && new ChangeSpecChild(2).equals(modified)); - Assert.assertTrue(model.updateRelation("child", Sets.newHashSet(bootstrapPersistentResource(new ChangeSpecChild(2))))); + ChangeSpecChild child2 = new ChangeSpecChild(2); + Assert.assertTrue(model.updateRelation("child", Sets.newHashSet(bootstrapPersistentResource(child2, tx)))); - model.getObject().checkFunction = (spec) -> relCheck.apply(spec, (original, modified) -> new ChangeSpecChild(2).equals(original) && modified == null); - Assert.assertTrue(model.updateRelation("child", null)); + when(tx.getRelation(any(), eq(model.obj), eq("child"), any(), any(), any(), any())).thenReturn(child2); + + model.getObject().checkFunction = (spec) -> relCheck.apply(spec, (original, modified) -> new ChangeSpecChild(2).equals(original) && modified == null); + Assert.assertTrue(model.updateRelation("child", null)); + } catch (ForbiddenAccessException e) { + e.printStackTrace(); + } } @Test @@ -2064,8 +2155,11 @@ public void testEqualsAndHashcode() { } private PersistentResource bootstrapPersistentResource(T obj) { + return bootstrapPersistentResource(obj, mock(DataStoreTransaction.class)); + } + + private PersistentResource bootstrapPersistentResource(T obj, DataStoreTransaction tx) { User goodUser = new User(1); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); RequestScope requestScope = new RequestScope(null, null, tx, goodUser, null, elideSettings, false); return new PersistentResource<>(obj, null, requestScope.getUUIDFor(obj), requestScope); } diff --git a/elide-core/src/test/java/com/yahoo/elide/core/UpdateOnCreateTest.java b/elide-core/src/test/java/com/yahoo/elide/core/UpdateOnCreateTest.java index ca260b4349..c0268e4b26 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/UpdateOnCreateTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/UpdateOnCreateTest.java @@ -20,7 +20,6 @@ import example.Publisher; import example.UpdateAndCreate; -import org.mockito.Answers; import org.testng.annotations.BeforeTest; import org.testng.annotations.Test; @@ -53,7 +52,7 @@ public void init() { publisher.setEditor(editor); - DataStoreTransaction tx = mock(DataStoreTransaction.class, Answers.CALLS_REAL_METHODS); + DataStoreTransaction tx = mock(DataStoreTransaction.class); User userOne = new User(1); userOneScope = new RequestScope(null, null, tx, userOne, null, elideSettings, false); diff --git a/elide-core/src/test/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransactionTest.java b/elide-core/src/test/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransactionTest.java new file mode 100644 index 0000000000..bc8c0c90a2 --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/datastore/inmemory/InMemoryStoreTransactionTest.java @@ -0,0 +1,515 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ +package com.yahoo.elide.core.datastore.inmemory; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.yahoo.elide.ElideSettings; +import com.yahoo.elide.ElideSettingsBuilder; +import com.yahoo.elide.core.DataStoreTransaction; +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.RequestScope; +import com.yahoo.elide.core.filter.InPredicate; +import com.yahoo.elide.core.filter.expression.AndFilterExpression; +import com.yahoo.elide.core.filter.expression.FilterExpression; +import com.yahoo.elide.core.pagination.Pagination; +import com.yahoo.elide.core.sort.Sorting; + +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import example.Author; +import example.Book; +import example.Editor; +import example.Publisher; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class InMemoryStoreTransactionTest { + + private DataStoreTransaction wrappedTransaction = mock(DataStoreTransaction.class); + private RequestScope scope = mock(RequestScope.class); + private InMemoryStoreTransaction inMemoryStoreTransaction = new InMemoryStoreTransaction(wrappedTransaction); + private EntityDictionary dictionary; + private Set books = new HashSet<>(); + private Book book1; + private Book book2; + private Book book3; + private Author author; + private ElideSettings elideSettings; + + @BeforeTest + public void init() { + dictionary = new EntityDictionary(new HashMap<>()); + dictionary.bindEntity(Book.class); + dictionary.bindEntity(Author.class); + dictionary.bindEntity(Editor.class); + dictionary.bindEntity(Publisher.class); + + elideSettings = new ElideSettingsBuilder(null).build(); + + author = new Author(); + + Editor editor1 = new Editor(); + editor1.setFirstName("Jon"); + editor1.setLastName("Doe"); + + Editor editor2 = new Editor(); + editor2.setFirstName("Jane"); + editor2.setLastName("Doe"); + + Publisher publisher1 = new Publisher(); + publisher1.setEditor(editor1); + + Publisher publisher2 = new Publisher(); + publisher2.setEditor(editor2); + + book1 = new Book(1, + "Book 1", + "Literary Fiction", + "English", + System.currentTimeMillis(), + Sets.newHashSet(author), + publisher1); + + book2 = new Book(2, + "Book 2", + "Science Fiction", + "English", + System.currentTimeMillis(), + Sets.newHashSet(author), + publisher1); + + book3 = new Book(3, + "Book 3", + "Literary Fiction", + "English", + System.currentTimeMillis(), + Sets.newHashSet(author), + publisher2); + + books.add(book1); + books.add(book2); + books.add(book3); + + author.setBooks(new ArrayList(books)); + + when(scope.getDictionary()).thenReturn(dictionary); + } + + @BeforeMethod + public void resetMocks() { + reset(wrappedTransaction); + } + + @Test + public void testFullFilterPredicatePushDown() { + FilterExpression expression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.of(expression)), + eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.of(expression), + Optional.empty(), + Optional.empty(), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.of(expression)), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 3); + Assert.assertTrue(loaded.contains(book1)); + Assert.assertTrue(loaded.contains(book2)); + Assert.assertTrue(loaded.contains(book3)); + } + + @Test + public void testTransactionRequiresInMemoryFilterDuringGetRelation() { + FilterExpression expression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + when(scope.isMutatingMultipleEntities()).thenReturn(true); + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); + when(wrappedTransaction.getRelation(eq(inMemoryStoreTransaction), eq(author), eq("books"), + eq(Optional.empty()), eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.getRelation( + inMemoryStoreTransaction, + author, + "books", + Optional.of(expression), + Optional.empty(), + Optional.empty(), + scope); + + verify(wrappedTransaction, times(1)).getRelation( + eq(inMemoryStoreTransaction), + eq(author), + eq("books"), + eq(Optional.empty()), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 2); + Assert.assertTrue(loaded.contains(book1)); + Assert.assertTrue(loaded.contains(book3)); + } + + @Test + public void testTransactionRequiresInMemoryFilterDuringLoad() { + FilterExpression expression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + when(scope.isMutatingMultipleEntities()).thenReturn(true); + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.of(expression)), + eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.of(expression), + Optional.empty(), + Optional.empty(), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.of(expression)), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 3); + Assert.assertTrue(loaded.contains(book1)); + Assert.assertTrue(loaded.contains(book2)); + Assert.assertTrue(loaded.contains(book3)); + } + + @Test + public void testDataStoreRequiresTotalInMemoryFilter() { + FilterExpression expression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.NONE); + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), + eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.of(expression), + Optional.empty(), + Optional.empty(), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.empty()), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 2); + Assert.assertTrue(loaded.contains(book1)); + Assert.assertTrue(loaded.contains(book3)); + } + + @Test + public void testDataStoreRequiresPartialInMemoryFilter() { + FilterExpression expression1 = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + FilterExpression expression2 = + new InPredicate(new Path(Book.class, dictionary, "editor.firstName"), "Jane"); + FilterExpression expression = new AndFilterExpression(expression1, expression2); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.PARTIAL); + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.of(expression1)), + eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.of(expression), + Optional.empty(), + Optional.empty(), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.of(expression1)), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 1); + Assert.assertTrue(loaded.contains(book3)); + } + + @Test + public void testSortingPushDown() { + Map sortOrder = new HashMap<>(); + sortOrder.put("title", Sorting.SortOrder.asc); + + Sorting sorting = new Sorting(sortOrder); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); + when(wrappedTransaction.supportsSorting(eq(Book.class), + any())).thenReturn(true); + + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), + eq(Optional.of(sorting)), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.empty(), + Optional.of(sorting), + Optional.empty(), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.empty()), + eq(Optional.of(sorting)), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 3); + } + + @Test + public void testDataStoreRequiresInMemorySorting() { + Map sortOrder = new HashMap<>(); + sortOrder.put("title", Sorting.SortOrder.asc); + + Sorting sorting = new Sorting(sortOrder); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); + when(wrappedTransaction.supportsSorting(eq(Book.class), + any())).thenReturn(false); + + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), + eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.empty(), + Optional.of(sorting), + Optional.empty(), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.empty()), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 3); + + List bookTitles = loaded.stream().map((o) -> ((Book) o).getTitle()).collect(Collectors.toList()); + Assert.assertEquals(bookTitles, Lists.newArrayList("Book 1", "Book 2", "Book 3")); + } + + @Test + public void testFilteringRequiresInMemorySorting() { + FilterExpression expression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + Map sortOrder = new HashMap<>(); + sortOrder.put("title", Sorting.SortOrder.asc); + + Sorting sorting = new Sorting(sortOrder); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.NONE); + when(wrappedTransaction.supportsSorting(eq(Book.class), + any())).thenReturn(true); + + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), + eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.of(expression), + Optional.of(sorting), + Optional.empty(), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.empty()), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 2); + + List bookTitles = loaded.stream().map((o) -> ((Book) o).getTitle()).collect(Collectors.toList()); + Assert.assertEquals(bookTitles, Lists.newArrayList("Book 1", "Book 3")); + } + + @Test + public void testPaginationPushDown() { + Pagination pagination = Pagination.getDefaultPagination(elideSettings); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); + when(wrappedTransaction.supportsPagination(eq(Book.class))).thenReturn(true); + + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), + eq(Optional.empty()), eq(Optional.of(pagination)), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.empty(), + Optional.empty(), + Optional.of(pagination), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.empty()), + eq(Optional.empty()), + eq(Optional.of(pagination)), + eq(scope)); + + Assert.assertEquals(loaded.size(), 3); + } + + @Test + public void testDataStoreRequiresInMemoryPagination() { + Pagination pagination = Pagination.getDefaultPagination(elideSettings); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); + when(wrappedTransaction.supportsPagination(eq(Book.class))).thenReturn(false); + + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), + eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.empty(), + Optional.empty(), + Optional.of(pagination), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.empty()), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 3); + Assert.assertTrue(loaded.contains(book1)); + Assert.assertTrue(loaded.contains(book2)); + Assert.assertTrue(loaded.contains(book3)); + } + + @Test + public void testFilteringRequiresInMemoryPagination() { + FilterExpression expression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + Pagination pagination = Pagination.getDefaultPagination(elideSettings); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.NONE); + when(wrappedTransaction.supportsPagination(eq(Book.class))).thenReturn(true); + + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), + eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.of(expression), + Optional.empty(), + Optional.of(pagination), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.empty()), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 2); + Assert.assertTrue(loaded.contains(book1)); + Assert.assertTrue(loaded.contains(book3)); + } + + @Test + public void testSortingRequiresInMemoryPagination() { + Pagination pagination = Pagination.getDefaultPagination(elideSettings); + + Map sortOrder = new HashMap<>(); + sortOrder.put("title", Sorting.SortOrder.asc); + + Sorting sorting = new Sorting(sortOrder); + + when(wrappedTransaction.supportsFiltering(eq(Book.class), + any())).thenReturn(DataStoreTransaction.FeatureSupport.FULL); + when(wrappedTransaction.supportsSorting(eq(Book.class), + any())).thenReturn(false); + when(wrappedTransaction.supportsPagination(eq(Book.class))).thenReturn(true); + + when(wrappedTransaction.loadObjects(eq(Book.class), eq(Optional.empty()), + eq(Optional.empty()), eq(Optional.empty()), eq(scope))).thenReturn(books); + + Collection loaded = (Collection) inMemoryStoreTransaction.loadObjects( + Book.class, + Optional.empty(), + Optional.of(sorting), + Optional.of(pagination), + scope); + + verify(wrappedTransaction, times(1)).loadObjects( + eq(Book.class), + eq(Optional.empty()), + eq(Optional.empty()), + eq(Optional.empty()), + eq(scope)); + + Assert.assertEquals(loaded.size(), 3); + Assert.assertTrue(loaded.contains(book1)); + Assert.assertTrue(loaded.contains(book2)); + Assert.assertTrue(loaded.contains(book3)); + } +} diff --git a/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/FilterPredicatePushdownExtractorTest.java b/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/FilterPredicatePushdownExtractorTest.java new file mode 100644 index 0000000000..7f7372a6db --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/FilterPredicatePushdownExtractorTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core.filter.expression; + +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.filter.InPredicate; +import example.Author; +import example.Book; +import example.Editor; +import example.Publisher; +import org.testng.Assert; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.util.HashMap; + +public class FilterPredicatePushdownExtractorTest { + + private EntityDictionary dictionary; + + @BeforeTest + public void init() { + dictionary = new EntityDictionary(new HashMap<>()); + dictionary.bindEntity(Book.class); + dictionary.bindEntity(Author.class); + dictionary.bindEntity(Editor.class); + dictionary.bindEntity(Publisher.class); + } + + @Test + public void testFullPredicateExtraction() { + FilterExpression expression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + FilterExpression extracted = FilterPredicatePushdownExtractor.extractPushDownPredicate(dictionary, expression); + + Assert.assertEquals(extracted, expression); + } + + @Test + public void testNoPredicateExtraction() { + FilterExpression dataStoreExpression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + FilterExpression inMemoryExpression = + new InPredicate(new Path(Book.class, dictionary, "editor.firstName"), "Literary Fiction"); + + FilterExpression finalExpression = new NotFilterExpression(new AndFilterExpression(dataStoreExpression, inMemoryExpression)); + + FilterExpression extracted = FilterPredicatePushdownExtractor.extractPushDownPredicate(dictionary, finalExpression); + + Assert.assertNull(extracted); + } + + @Test + public void testPartialPredicateExtraction() { + FilterExpression dataStoreExpression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + FilterExpression inMemoryExpression = + new InPredicate(new Path(Book.class, dictionary, "editor.firstName"), "Literary Fiction"); + + FilterExpression finalExpression = new NotFilterExpression(new OrFilterExpression(dataStoreExpression, inMemoryExpression)); + + FilterExpression extracted = FilterPredicatePushdownExtractor.extractPushDownPredicate(dictionary, finalExpression); + + Assert.assertEquals(extracted, ((InPredicate) dataStoreExpression).negate()); + } +} diff --git a/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/InMemoryExecutionVerifierTest.java b/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/InMemoryExecutionVerifierTest.java new file mode 100644 index 0000000000..5d929acaab --- /dev/null +++ b/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/InMemoryExecutionVerifierTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2019, Yahoo Inc. + * Licensed under the Apache License, Version 2.0 + * See LICENSE file in project root for terms. + */ + +package com.yahoo.elide.core.filter.expression; + +import com.yahoo.elide.core.EntityDictionary; +import com.yahoo.elide.core.Path; +import com.yahoo.elide.core.filter.InPredicate; +import example.Author; +import example.Book; +import example.Editor; +import example.Publisher; +import org.testng.Assert; +import org.testng.annotations.BeforeTest; +import org.testng.annotations.Test; + +import java.util.HashMap; + +public class InMemoryExecutionVerifierTest { + + private EntityDictionary dictionary; + + @BeforeTest + public void init() { + dictionary = new EntityDictionary(new HashMap<>()); + dictionary.bindEntity(Book.class); + dictionary.bindEntity(Author.class); + dictionary.bindEntity(Editor.class); + dictionary.bindEntity(Publisher.class); + } + + @Test + public void testNoInMemoryExecution() { + FilterExpression expression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + Assert.assertFalse(InMemoryExecutionVerifier.shouldExecuteInMemory(dictionary, expression)); + } + + @Test + public void testFullInMemoryExecution() { + FilterExpression dataStoreExpression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + FilterExpression inMemoryExpression = + new InPredicate(new Path(Book.class, dictionary, "editor.firstName"), "Literary Fiction"); + + FilterExpression finalExpression = new NotFilterExpression(new AndFilterExpression(dataStoreExpression, inMemoryExpression)); + + Assert.assertTrue(InMemoryExecutionVerifier.shouldExecuteInMemory(dictionary, finalExpression)); + } + + @Test + public void testPartialInMemoryExecution() { + FilterExpression dataStoreExpression = + new InPredicate(new Path(Book.class, dictionary, "genre"), "Literary Fiction"); + + FilterExpression inMemoryExpression = + new InPredicate(new Path(Book.class, dictionary, "editor.firstName"), "Literary Fiction"); + + FilterExpression finalExpression = new NotFilterExpression(new OrFilterExpression(dataStoreExpression, inMemoryExpression)); + + Assert.assertTrue(InMemoryExecutionVerifier.shouldExecuteInMemory(dictionary, finalExpression)); + } +} diff --git a/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/InMemoryFilterVisitorTest.java b/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/InMemoryFilterExecutorTest.java similarity index 98% rename from elide-core/src/test/java/com/yahoo/elide/core/filter/expression/InMemoryFilterVisitorTest.java rename to elide-core/src/test/java/com/yahoo/elide/core/filter/expression/InMemoryFilterExecutorTest.java index b46674985b..8c788782a4 100644 --- a/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/InMemoryFilterVisitorTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/core/filter/expression/InMemoryFilterExecutorTest.java @@ -43,11 +43,11 @@ import java.util.function.Predicate; /** - * Tests InMemoryFilterVisitor + * Tests InMemoryFilterExecutor. */ -public class InMemoryFilterVisitorTest { +public class InMemoryFilterExecutorTest { private Author author; - private final InMemoryFilterVisitor visitor; + private final InMemoryFilterExecutor visitor; private FilterExpression expression; private Predicate fn; @@ -73,12 +73,12 @@ public Class lookupEntityClass(Class objClass) { } - InMemoryFilterVisitorTest() { + InMemoryFilterExecutorTest() { EntityDictionary dictionary = new TestEntityDictionary(new HashMap<>()); dictionary.bindEntity(Author.class); RequestScope requestScope = Mockito.mock(RequestScope.class); when(requestScope.getDictionary()).thenReturn(dictionary); - visitor = new InMemoryFilterVisitor(requestScope); + visitor = new InMemoryFilterExecutor(requestScope); } @Test diff --git a/elide-core/src/test/java/com/yahoo/elide/utils/coerce/converters/ISO8601DateSerdeTest.java b/elide-core/src/test/java/com/yahoo/elide/utils/coerce/converters/ISO8601DateSerdeTest.java index 32d9f2daea..f38f6a81f4 100644 --- a/elide-core/src/test/java/com/yahoo/elide/utils/coerce/converters/ISO8601DateSerdeTest.java +++ b/elide-core/src/test/java/com/yahoo/elide/utils/coerce/converters/ISO8601DateSerdeTest.java @@ -71,4 +71,14 @@ public void testInvalidDateDeserialization() { ISO8601DateSerde serde = new ISO8601DateSerde(); serde.deserialize("1"); } + + @Test + public void testInvalidDateDeserializationMessage() { + ISO8601DateSerde serde = new ISO8601DateSerde("yyyy-MM-dd'T'HH:mm:ss'Z'", TimeZone.getTimeZone("UTC")); + try { + serde.deserialize("2019-01-01T00:00Z"); + } catch (IllegalArgumentException e) { + Assert.assertEquals(e.getMessage(), "Date strings must be formated as yyyy-MM-dd'T'HH:mm:ss'Z'"); + } + } } diff --git a/elide-core/src/test/java/example/Book.java b/elide-core/src/test/java/example/Book.java index d95c64b324..e645b2664a 100644 --- a/elide-core/src/test/java/example/Book.java +++ b/elide-core/src/test/java/example/Book.java @@ -29,6 +29,8 @@ import com.yahoo.elide.security.ChangeSpec; import com.yahoo.elide.security.RequestScope; import com.yahoo.elide.security.checks.OperationCheck; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; import java.util.ArrayList; import java.util.Collection; @@ -57,6 +59,8 @@ operation = 10, logStatement = "{0}", logExpressions = {"${book.title}"}) +@AllArgsConstructor +@NoArgsConstructor public class Book { private long id; private String title; diff --git a/elide-core/src/test/java/example/Editor.java b/elide-core/src/test/java/example/Editor.java index 823aae5c23..486618230e 100644 --- a/elide-core/src/test/java/example/Editor.java +++ b/elide-core/src/test/java/example/Editor.java @@ -6,6 +6,7 @@ package example; import com.yahoo.elide.annotation.Audit; +import com.yahoo.elide.annotation.ComputedAttribute; import com.yahoo.elide.annotation.ComputedRelationship; import com.yahoo.elide.annotation.Exclude; import com.yahoo.elide.annotation.FilterExpressionPath; @@ -50,6 +51,12 @@ public class Editor { @Exclude private String naturalKey = UUID.randomUUID().toString(); + @Getter @Setter + private String firstName; + + @Getter @Setter + private String lastName; + @Override public int hashCode() { return naturalKey.hashCode(); @@ -64,8 +71,10 @@ public boolean equals(Object obj) { return ((Editor) obj).naturalKey.equals(naturalKey); } - @Getter @Setter - private String name; + @ComputedAttribute + public String getFullName() { + return firstName + " " + lastName; + } @Transient @ComputedRelationship diff --git a/elide-core/testng.xml b/elide-core/testng.xml deleted file mode 100644 index 70d2f0ab37..0000000000 --- a/elide-core/testng.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/InMemoryTransaction.java b/elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/HashMapStoreTransaction.java similarity index 54% rename from elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/InMemoryTransaction.java rename to elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/HashMapStoreTransaction.java index 4e78fe9549..d86a0c497e 100644 --- a/elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/InMemoryTransaction.java +++ b/elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/HashMapStoreTransaction.java @@ -11,13 +11,13 @@ import java.util.concurrent.atomic.AtomicLong; /** - * InMemoryDataStore transaction handler. - * @deprecated Use {@link com.yahoo.elide.core.datastore.inmemory.InMemoryTransaction} + * HashMapDataStore transaction handler. + * @deprecated Use {@link com.yahoo.elide.core.datastore.inmemory.HashMapStoreTransaction} */ @Deprecated -public class InMemoryTransaction extends com.yahoo.elide.core.datastore.inmemory.InMemoryTransaction { - public InMemoryTransaction(Map, Map> dataStore, - EntityDictionary dictionary, Map, AtomicLong> typeIds) { +public class HashMapStoreTransaction extends com.yahoo.elide.core.datastore.inmemory.HashMapStoreTransaction { + public HashMapStoreTransaction(Map, Map> dataStore, + EntityDictionary dictionary, Map, AtomicLong> typeIds) { super(dataStore, dictionary, typeIds); } } diff --git a/elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/InMemoryDataStore.java b/elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/InMemoryDataStore.java index 10c0d830f6..cc0c9ff1ed 100644 --- a/elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/InMemoryDataStore.java +++ b/elide-datastore/elide-datastore-inmemorydb/src/main/java/com/yahoo/elide/datastores/inmemory/InMemoryDataStore.java @@ -5,12 +5,14 @@ */ package com.yahoo.elide.datastores.inmemory; +import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore; + /** * Simple non-persistent in-memory database. - * @deprecated Use {@link com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore} + * @deprecated Use {@link HashMapDataStore} */ @Deprecated -public class InMemoryDataStore extends com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore { +public class InMemoryDataStore extends HashMapDataStore { public InMemoryDataStore(Package beanPackage) { super(beanPackage); } diff --git a/elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/InMemoryDataStoreTest.java b/elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/HashMapDataStoreTest.java similarity index 98% rename from elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/InMemoryDataStoreTest.java rename to elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/HashMapDataStoreTest.java index c6c9ba336b..70df9f4f31 100644 --- a/elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/InMemoryDataStoreTest.java +++ b/elide-datastore/elide-datastore-inmemorydb/src/test/java/com/yahoo/elide/datastores/inmemory/HashMapDataStoreTest.java @@ -29,9 +29,9 @@ import java.util.Set; /** - * InMemoryDataStore tests. + * HashMapDataStore tests. */ -public class InMemoryDataStoreTest { +public class HashMapDataStoreTest { private InMemoryDataStore inMemoryDataStore; @BeforeMethod diff --git a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexTransaction.java b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexTransaction.java index cad7626479..e18b212bff 100644 --- a/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexTransaction.java +++ b/elide-datastore/elide-datastore-multiplex/src/main/java/com/yahoo/elide/datastores/multiplex/MultiplexTransaction.java @@ -219,6 +219,21 @@ public void setAttribute(Object entity, String attributeName, Object attributeVa transaction.setAttribute(entity, attributeName, attributeValue, scope); } + @Override + public FeatureSupport supportsFiltering(Class entityClass, FilterExpression expression) { + return getTransaction(entityClass).supportsFiltering(entityClass, expression); + } + + @Override + public boolean supportsSorting(Class entityClass, Sorting sorting) { + return getTransaction(entityClass).supportsSorting(entityClass, sorting); + } + + @Override + public boolean supportsPagination(Class entityClass) { + return getTransaction(entityClass).supportsPagination(entityClass); + } + private Serializable extractId(FilterExpression filterExpression, String idFieldName, Class relationClass) { diff --git a/elide-graphql/src/test/groovy/com/yahoo/elide/graphql/GraphQLEndointTest.groovy b/elide-graphql/src/test/groovy/com/yahoo/elide/graphql/GraphQLEndointTest.groovy index 111ddc8fe8..2683a61926 100644 --- a/elide-graphql/src/test/groovy/com/yahoo/elide/graphql/GraphQLEndointTest.groovy +++ b/elide-graphql/src/test/groovy/com/yahoo/elide/graphql/GraphQLEndointTest.groovy @@ -9,7 +9,7 @@ import com.yahoo.elide.Elide import com.yahoo.elide.ElideSettingsBuilder import com.yahoo.elide.audit.AuditLogger import com.yahoo.elide.core.EntityDictionary -import com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore +import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore import com.yahoo.elide.resources.DefaultOpaqueUserFunction import com.fasterxml.jackson.databind.JsonNode @@ -78,7 +78,7 @@ class GraphQLEndointTest { @BeforeMethod void setupTest() throws Exception { - InMemoryDataStore inMemoryStore = new InMemoryDataStore(Book.class.getPackage()) + HashMapDataStore inMemoryStore = new HashMapDataStore(Book.class.getPackage()) Map checkMappings = new HashMap<>() checkMappings[UserChecks.IS_USER_1] = UserChecks.IsUserId.One.class checkMappings[UserChecks.IS_USER_2] = UserChecks.IsUserId.Two.class diff --git a/elide-graphql/src/test/java/com/yahoo/elide/graphql/PersistentResourceFetcherTest.java b/elide-graphql/src/test/java/com/yahoo/elide/graphql/PersistentResourceFetcherTest.java index 5c0c8d3e27..9268ce83b4 100644 --- a/elide-graphql/src/test/java/com/yahoo/elide/graphql/PersistentResourceFetcherTest.java +++ b/elide-graphql/src/test/java/com/yahoo/elide/graphql/PersistentResourceFetcherTest.java @@ -7,8 +7,9 @@ import com.yahoo.elide.ElideSettings; import com.yahoo.elide.ElideSettingsBuilder; +import com.yahoo.elide.core.DataStoreTransaction; +import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore; import com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore; -import com.yahoo.elide.core.datastore.inmemory.InMemoryTransaction; import com.yahoo.elide.core.filter.dialect.RSQLFilterDialect; import com.yahoo.elide.utils.coerce.CoerceUtil; @@ -67,19 +68,21 @@ public void setupFetcherTest() { CoerceUtil.register(targetType, serde); }); - InMemoryDataStore store = new InMemoryDataStore(Author.class.getPackage()); + InMemoryDataStore store = new InMemoryDataStore( + new HashMapDataStore(Author.class.getPackage()) + ); store.populateEntityDictionary(dictionary); ModelBuilder builder = new ModelBuilder(dictionary, new PersistentResourceFetcher(settings)); api = new GraphQL(builder.build()); - InMemoryTransaction tx = (InMemoryTransaction) store.beginTransaction(); + DataStoreTransaction tx = store.beginTransaction(); initTestData(tx); requestScope = new GraphQLRequestScope(tx, null, settings); } - private void initTestData(InMemoryTransaction tx) { + private void initTestData(DataStoreTransaction tx) { Publisher publisher1 = new Publisher(); publisher1.setId(1L); publisher1.setName("The Guy"); diff --git a/elide-integration-tests/src/test/groovy/com/yahoo/elide/tests/FilterIT.groovy b/elide-integration-tests/src/test/groovy/com/yahoo/elide/tests/FilterIT.groovy index 94b2e66e56..87a7f228d7 100644 --- a/elide-integration-tests/src/test/groovy/com/yahoo/elide/tests/FilterIT.groovy +++ b/elide-integration-tests/src/test/groovy/com/yahoo/elide/tests/FilterIT.groovy @@ -15,6 +15,10 @@ import org.testng.Assert import org.testng.annotations.AfterTest import org.testng.annotations.BeforeClass import org.testng.annotations.Test + +import static org.hamcrest.Matchers.contains +import static org.hamcrest.Matchers.hasSize; + /** * Tests for Filters */ @@ -115,10 +119,22 @@ class FilterIT extends AbstractIntegrationTestInitializer { "name": "Default publisher" } } + }, + { + "op": "add", + "path": "/book/12345678-1234-1234-1234-1234567890ac/publisher/12345678-1234-1234-1234-1234567890ae/editor", + "value": { + "type": "editor", + "id": "12345678-1234-1234-1234-1234567890ba", + "attributes": { + "firstName": "John", + "lastName": "Doe" + } + } } ] ''') - .patch("/").then().statusCode(HttpStatus.SC_OK) + .patch("/").then().log().all().statusCode(HttpStatus.SC_OK) RestAssured .given() @@ -1593,6 +1609,53 @@ class FilterIT extends AbstractIntegrationTestInitializer { } } + /** + * Tests a computed relationship filter. + */ + @Test + void testFilterBookByEditor() { + RestAssured + .given() + .get("/book?filter[book]=editor.firstName=='John'") + .then() + .log().all() + .statusCode(org.apache.http.HttpStatus.SC_OK) + .body("data.attributes.title", contains("The Old Man and the Sea")) + .body("data", hasSize(1)); + + RestAssured + .given() + .get("/book?filter[book]=editor.firstName=='Foobar'") + .then() + .log().all() + .statusCode(org.apache.http.HttpStatus.SC_OK) + .body("data", hasSize(0)); + } + + /** + * Tests a computed attribute filter. + */ + @Test + void testFilterEditorByFullName() { + RestAssured + .given() + .get("/editor?filter[editor]=fullName=='John Doe'") + .then() + .log().all() + .statusCode(org.apache.http.HttpStatus.SC_OK) + .body("data.attributes.firstName", contains("John")) + .body("data.attributes.lastName", contains("Doe")) + .body("data", hasSize(1)); + + RestAssured + .given() + .get("/editor?filter[editor]=fullName=='Foobar'") + .then() + .log().all() + .statusCode(org.apache.http.HttpStatus.SC_OK) + .body("data", hasSize(0)); + } + @Test void testFailFilterAuthorBookByChapter() { /* Test default */ diff --git a/elide-integration-tests/src/test/java/example/Book.java b/elide-integration-tests/src/test/java/example/Book.java index 69b4ce8e6c..ec91e52538 100644 --- a/elide-integration-tests/src/test/java/example/Book.java +++ b/elide-integration-tests/src/test/java/example/Book.java @@ -145,7 +145,11 @@ public void setPublisher(Publisher publisher) { @FilterExpressionPath("publisher.editor") @ReadPermission(expression = "Field path editor check") public Editor getEditor() { - return getPublisher().getEditor(); + if (publisher != null) { + return getPublisher().getEditor(); + } + + return null; } @Override public String toString() { diff --git a/elide-integration-tests/src/test/java/example/Editor.java b/elide-integration-tests/src/test/java/example/Editor.java index 823aae5c23..486618230e 100644 --- a/elide-integration-tests/src/test/java/example/Editor.java +++ b/elide-integration-tests/src/test/java/example/Editor.java @@ -6,6 +6,7 @@ package example; import com.yahoo.elide.annotation.Audit; +import com.yahoo.elide.annotation.ComputedAttribute; import com.yahoo.elide.annotation.ComputedRelationship; import com.yahoo.elide.annotation.Exclude; import com.yahoo.elide.annotation.FilterExpressionPath; @@ -50,6 +51,12 @@ public class Editor { @Exclude private String naturalKey = UUID.randomUUID().toString(); + @Getter @Setter + private String firstName; + + @Getter @Setter + private String lastName; + @Override public int hashCode() { return naturalKey.hashCode(); @@ -64,8 +71,10 @@ public boolean equals(Object obj) { return ((Editor) obj).naturalKey.equals(naturalKey); } - @Getter @Setter - private String name; + @ComputedAttribute + public String getFullName() { + return firstName + " " + lastName; + } @Transient @ComputedRelationship diff --git a/elide-standalone/src/test/java/com/yahoo/elide/standalone/ElideStandaloneTest.java b/elide-standalone/src/test/java/com/yahoo/elide/standalone/ElideStandaloneTest.java index 0de39392ed..043ba5b15a 100644 --- a/elide-standalone/src/test/java/com/yahoo/elide/standalone/ElideStandaloneTest.java +++ b/elide-standalone/src/test/java/com/yahoo/elide/standalone/ElideStandaloneTest.java @@ -11,7 +11,7 @@ import com.yahoo.elide.ElideSettings; import com.yahoo.elide.ElideSettingsBuilder; import com.yahoo.elide.core.EntityDictionary; -import com.yahoo.elide.core.datastore.inmemory.InMemoryDataStore; +import com.yahoo.elide.core.datastore.inmemory.HashMapDataStore; import com.yahoo.elide.core.filter.dialect.RSQLFilterDialect; import com.yahoo.elide.standalone.config.ElideStandaloneSettings; import com.yahoo.elide.standalone.models.Post; @@ -38,7 +38,7 @@ public void init() throws Exception { @Override public ElideSettings getElideSettings(ServiceLocator injector) { EntityDictionary dictionary = new EntityDictionary(getCheckMappings()); - InMemoryDataStore dataStore = new InMemoryDataStore(Post.class.getPackage()); + HashMapDataStore dataStore = new HashMapDataStore(Post.class.getPackage()); dataStore.populateEntityDictionary(dictionary); ElideSettingsBuilder builder = new ElideSettingsBuilder(dataStore)