Skip to content

Commit

Permalink
Merge branch 'master' into feature/datastore-jpa
Browse files Browse the repository at this point in the history
  • Loading branch information
aklish authored Apr 7, 2019
2 parents 1975555 + cddb883 commit f0114e6
Show file tree
Hide file tree
Showing 37 changed files with 2,047 additions and 648 deletions.
6 changes: 5 additions & 1 deletion changelog.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
3 changes: 2 additions & 1 deletion elide-core/src/main/java/com/yahoo/elide/Elide.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
*/
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;
import com.yahoo.elide.security.User;

import java.io.Closeable;
import java.io.Serializable;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;

Expand All @@ -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.
*
Expand Down Expand Up @@ -109,11 +121,31 @@ default <T> T createNewObject(Class<T> 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> 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<Object> results = loadObjects(entityClass,
Optional.of(joinedFilterExpression),
Optional.empty(),
Optional.empty(),
scope);
Iterator<Object> it = results == null ? null : results.iterator();
if (it != null && it.hasNext()) {
return it.next();
}
return null;
}

/**
* Loads a collection of objects.
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -825,9 +823,8 @@ public Set<PersistentResource> 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());

Expand Down Expand Up @@ -1011,30 +1008,16 @@ private Set<PersistentResource> 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<PersistentResource> 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(
Expand All @@ -1046,30 +1029,6 @@ private Set<PersistentResource> getRelationUnchecked(String relationName,
return resources;
}

/**
* Filters a relationship collection in memory for scenarios where the data store transaction cannot do it.
*
* @param <T> the type parameter
* @param collection the collection to filter
* @param filterExpression the filter expression
* @return the filtered collection
*/
protected <T> Collection<T> filterInMemory(Collection<T> collection, Optional<FilterExpression> filterExpression) {

if (! filterExpression.isPresent()) {
return collection;
}

InMemoryFilterVisitor inMemoryFilterVisitor = new InMemoryFilterVisitor(requestScope);
@SuppressWarnings("unchecked")
Predicate<T> 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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Class<?>, Map<String, Object>> dataStore = Collections.synchronizedMap(new HashMap<>());
@Getter private EntityDictionary dictionary;
@Getter private final Package beanPackage;
@Getter private final ConcurrentHashMap<Class<?>, 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<String, Object> data = dataStore.get(cls);
for (Map.Entry<String, Object> e : data.entrySet()) {
sb.append(" Id: ").append(e.getKey()).append(" Value: ").append(e.getValue());
}
}
return sb.toString();
}
}
Loading

0 comments on commit f0114e6

Please sign in to comment.