Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Historical fetcher #619

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
29 changes: 29 additions & 0 deletions elide-core/src/main/java/com/yahoo/elide/core/RequestScope.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
import com.yahoo.elide.security.User;
import com.yahoo.elide.security.executors.ActivePermissionExecutor;
import lombok.Getter;
import lombok.Setter;

import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;
Expand Down Expand Up @@ -74,6 +75,8 @@ public class RequestScope implements com.yahoo.elide.security.RequestScope {
@Getter private final boolean useFilterExpressions;
@Getter private final int updateStatusCode;
@Getter private final boolean mutatingMultipleEntities;
@Getter @Setter private Long historicalRevision = null;
@Getter @Setter private Long historicalDatestamp = null;

@Getter private final MultipleFilterDialect filterDialect;
private final Map<String, FilterExpression> expressionsByType;
Expand Down Expand Up @@ -187,6 +190,8 @@ public RequestScope(String path,
this.sparseFields = parseSparseFields(queryParams);
this.sorting = Sorting.parseQueryParams(queryParams);
this.pagination = Pagination.parseQueryParams(queryParams, this.getElideSettings());
this.setHistoricalDatestamp(parseHistoricalDatestamp(queryParams));
this.setHistoricalRevision(parseHistoricalRevisionNumber(queryParams));
} else {
this.sparseFields = Collections.emptyMap();
this.sorting = Sorting.getDefaultEmptyInstance();
Expand Down Expand Up @@ -263,6 +268,30 @@ private static Map<String, Set<String>> parseSparseFields(MultivaluedMap<String,
return result;
}

private static Long parseHistoricalDatestamp(MultivaluedMap<String, String> queryParams) {
Map<String, Set<String>> result = new HashMap<>();

for (Map.Entry<String, List<String>> kv : queryParams.entrySet()) {
String key = kv.getKey();
if (key.equals("__historicaldatestamp")) {
return Long.parseLong(kv.getValue().get(0));
}
}
return null;
}

private static Long parseHistoricalRevisionNumber(MultivaluedMap<String, String> queryParams) {
Map<String, Set<String>> result = new HashMap<>();

for (Map.Entry<String, List<String>> kv : queryParams.entrySet()) {
String key = kv.getKey();
if (key.equals("__historicalversion")) {
return Long.parseLong(kv.getValue().get(0));
}
}
return null;
}

/**
* Get filter expression for a specific collection type.
* @param type The name of the type
Expand Down
10 changes: 10 additions & 0 deletions elide-datastore/elide-datastore-hibernate/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
<version>4.0-beta-6</version>
</parent>

<properties>
<hibernate5.version>5.0.2.Final</hibernate5.version>
</properties>

<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
Expand All @@ -43,6 +47,12 @@
<groupId>com.yahoo.elide</groupId>
<artifactId>elide-core</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate5.version}</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package com.yahoo.elide.core.filter;


import com.yahoo.elide.annotation.Audit;
import com.yahoo.elide.core.EntityDictionary;
import com.yahoo.elide.core.Path;
import com.yahoo.elide.core.filter.expression.AndFilterExpression;
import com.yahoo.elide.core.filter.expression.FilterExpression;
import com.yahoo.elide.core.filter.expression.FilterExpressionVisitor;
import com.yahoo.elide.core.filter.expression.NotFilterExpression;
import com.yahoo.elide.core.filter.expression.OrFilterExpression;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.criteria.AuditCriterion;
import org.hibernate.envers.query.criteria.AuditProperty;

public class EnverseFilterOperation implements FilterOperation<AuditCriterion> {

private EntityDictionary dictionary;
private Class<?> entityClass;

public EnverseFilterOperation(EntityDictionary dictionary) {
this.dictionary = dictionary;
}
@Override
public AuditCriterion apply(FilterPredicate filterPredicate) {
if (filterPredicate.getPath().getPathElements().size() > 1) {
throw new RuntimeException("Entity traversal is not supported in revision datastore");
}
Path.PathElement field = filterPredicate.getPath().getPathElements().get(0);
String fieldName = field.getFieldName();
Class fieldType = field.getFieldType();
Class entityType = field.getType();

switch(filterPredicate.getOperator()) {
case IN:
AuditCriterion criterion = null;
if (dictionary.getRelationships(entityType).contains(fieldName)) {
if (dictionary.getRelationshipType(entityType, fieldName).isToMany()) {
throw new RuntimeException("FilterPath can move only along ToOne relationships");
}
for (Object value : filterPredicate.getValues()){
if (criterion == null){
criterion = AuditEntity.relatedId(fieldName).eq(value);
} else {
criterion = AuditEntity.or(criterion, AuditEntity.relatedId(fieldName).eq(value));
}
}
} else if (dictionary.getIdFieldName(entityType).equals(fieldName)){
for (Object value : filterPredicate.getValues()){
if (criterion == null){
criterion = AuditEntity.id().eq(value);
} else {
criterion = AuditEntity.or(criterion, AuditEntity.id().eq(value));
}
}

} else {
criterion = AuditEntity.property(fieldName).in(filterPredicate.getValues());
}
return criterion;
case GE:
return AuditEntity.property(fieldName).ge(filterPredicate.getParameters().get(0));
case LE:
return AuditEntity.property(fieldName).le(filterPredicate.getParameters().get(0));
case GT:
return AuditEntity.property(fieldName).gt(filterPredicate.getParameters().get(0));
case LT:
return AuditEntity.property(fieldName).lt(filterPredicate.getParameters().get(0));
case FALSE:
return AuditEntity.not(AuditEntity.property(fieldName).eqProperty(fieldName));
case TRUE:
return AuditEntity.property(fieldName).eqProperty(fieldName);
default:
throw new RuntimeException("unsupported operation");
}
}

public AuditCriterion apply(FilterExpression filterExpression) {
AuditCriterionVisitor visitor = new AuditCriterionVisitor();
return filterExpression.accept(visitor);

}


public class AuditCriterionVisitor implements FilterExpressionVisitor<AuditCriterion> {
public static final String TWO_NON_FILTERING_EXPRESSIONS =
"Cannot build a filter from two non-filtering expressions";
private boolean prefixWithAlias;

@Override
public AuditCriterion visitPredicate(FilterPredicate filterPredicate) {
return apply(filterPredicate);
}

@Override
public AuditCriterion visitAndExpression(AndFilterExpression expression) {
FilterExpression left = expression.getLeft();
FilterExpression right = expression.getRight();
return AuditEntity.and(left.accept(this), right.accept(this));
}

@Override
public AuditCriterion visitOrExpression(OrFilterExpression expression) {
FilterExpression left = expression.getLeft();
FilterExpression right = expression.getRight();
return AuditEntity.or(left.accept(this), right.accept(this));
}

@Override
public AuditCriterion visitNotExpression(NotFilterExpression expression) {
AuditCriterion negated = expression.getNegated().accept(this);
return AuditEntity.not(negated);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.yahoo.elide.core.hibernate.enverse;

import com.yahoo.elide.core.EntityDictionary;
import com.yahoo.elide.core.filter.expression.FilterExpression;

import java.util.Optional;

public class RootCollectionFetchRevisionQueryBuilder {

private Class<?> entityClass;
private EntityDictionary dictionary;
private FilterExpression filterExpression;

public RootCollectionFetchRevisionQueryBuilder(Class<?> entityClass,
EntityDictionary dictionary) {
this.entityClass = entityClass;
this.dictionary = dictionary;
}

public RootCollectionFetchRevisionQueryBuilder withPossibleFilterExpression(Optional<FilterExpression> filterExpression) {
this.filterExpression = filterExpression.get();
return this;
}




}
3 changes: 1 addition & 2 deletions elide-datastore/elide-datastore-hibernate5/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@
<artifactId>elide-integration-tests</artifactId>
<version>4.0-beta-6</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
Expand Down Expand Up @@ -160,7 +159,7 @@
<groupId>org.hibernate</groupId>
<artifactId>hibernate-envers</artifactId>
<version>${hibernate5.version}</version>
<scope>test</scope>
<scope>compile</scope>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.yahoo.elide.datastores.hibernate5;

import com.google.common.base.Preconditions;
import com.yahoo.elide.core.DataStore;
import com.yahoo.elide.core.DataStoreTransaction;
import com.yahoo.elide.core.EntityDictionary;
import org.hibernate.ScrollMode;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.envers.AuditReaderFactory;
import org.hibernate.jpa.HibernateEntityManager;
import org.hibernate.metadata.ClassMetadata;

import javax.persistence.Entity;
import javax.persistence.EntityManager;

public class HibernateRevisionsDataStore extends HibernateSessionFactoryStore {


public HibernateRevisionsDataStore(SessionFactory sessionFactory) {
super(sessionFactory, false, ScrollMode.SCROLL_SENSITIVE);
}

@Override
@SuppressWarnings("resource")
public DataStoreTransaction beginTransaction() {
Session session = sessionFactory.getCurrentSession();
Preconditions.checkNotNull(session);
session.beginTransaction();
return new HibernateRevisionsTransaction(AuditReaderFactory.get(session), session);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.yahoo.elide.datastores.hibernate5;

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.EnverseFilterOperation;
import com.yahoo.elide.core.filter.FilterPredicate;
import com.yahoo.elide.core.filter.Operator;
import com.yahoo.elide.core.filter.expression.AndFilterExpression;
import com.yahoo.elide.core.filter.expression.FilterExpression;
import com.yahoo.elide.core.hibernate.hql.RootCollectionFetchQueryBuilder;
import com.yahoo.elide.core.pagination.Pagination;
import com.yahoo.elide.core.sort.Sorting;
import com.yahoo.elide.datastores.hibernate5.porting.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.ObjectNotFoundException;
import org.hibernate.Query;
import org.hibernate.ScrollMode;
import org.hibernate.Session;
import org.hibernate.envers.AuditReader;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.criteria.AuditCriterion;

import java.io.Serializable;
import java.util.Collections;
import java.util.Optional;

@Slf4j
public class HibernateRevisionsTransaction extends HibernateTransaction {

private AuditReader auditReader;

public HibernateRevisionsTransaction(AuditReader auditReader, Session session) {
super(session, false, ScrollMode.SCROLL_INSENSITIVE);
this.auditReader = auditReader;
}

/**
* load a single record with id and filter.
*
* @param entityClass class of query object
* @param id id of the query object
* @param filterExpression FilterExpression contains the predicates
* @param scope Request scope associated with specific request
*/
@Override
public Object loadObject(Class<?> entityClass,
Serializable id,
Optional<FilterExpression> filterExpression,
RequestScope scope) {
if (!isHistory(scope)) {
return super.loadObject(entityClass, id, filterExpression, scope);
}
try {
EntityDictionary dictionary = scope.getDictionary();
Class<?> idType = dictionary.getIdType(entityClass);
String idField = dictionary.getIdFieldName(entityClass);

//Construct a predicate that selects an individual element of the relationship's parent (Author.id = 3).
FilterPredicate idExpression;
Path.PathElement idPath = new Path.PathElement(entityClass, idType, idField);
if (id != null) {
idExpression = new FilterPredicate(idPath, Operator.IN, Collections.singletonList(id));
} else {
idExpression = new FilterPredicate(idPath, Operator.FALSE, Collections.emptyList());
}

FilterExpression joinedExpression = filterExpression
.map(fe -> (FilterExpression) new AndFilterExpression(fe, idExpression))
.orElse(idExpression);

EnverseFilterOperation operation = new EnverseFilterOperation(scope.getDictionary());
AuditCriterion criteria = operation.apply(joinedExpression);
return auditReader.createQuery().forEntitiesAtRevision(entityClass, getRevision(scope)).add(criteria).getSingleResult();
} catch (ObjectNotFoundException e) {
return null;
}
}

@Override
public Iterable<Object> loadObjects(
Class<?> entityClass,
Optional<FilterExpression> filterExpression,
Optional<Sorting> sorting,
Optional<Pagination> pagination,
RequestScope scope) {
log.debug(String.format("Revision: %d", getRevision(scope)));
if (!isHistory(scope)) {
return super.loadObjects(entityClass, filterExpression, sorting, pagination, scope);
}

EnverseFilterOperation operation = new EnverseFilterOperation(scope.getDictionary());
if (filterExpression.isPresent()) {
AuditCriterion criteria = operation.apply(filterExpression.get());
return auditReader.createQuery().forEntitiesAtRevision(entityClass, getRevision(scope)).add(criteria).getResultList();
} else {
return auditReader.createQuery().forEntitiesAtRevision(entityClass, getRevision(scope)).getResultList();
}
}

private boolean isHistory(RequestScope scope) {
return scope.getHistoricalRevision() != null || scope.getHistoricalDatestamp() != null;
}

private Integer getRevision(RequestScope scope) {
if (scope.getHistoricalRevision() != null) {
return scope.getHistoricalRevision().intValue();
} else {
Query query = this.session.createSQLQuery("SELECT MAX(REV) from REVINFO WHERE REVTSTMP <= :timestamp");
query.setParameter("timestamp", scope.getHistoricalDatestamp());
log.debug(String.format("Query: %s", query.toString()));
log.debug(String.format("ts: %d", scope.getHistoricalDatestamp()));
return query.uniqueResult() != null
? (Integer) query.uniqueResult()
: 1;
}
}
}