Skip to content

#47 - Populate the RevisionType with information from Envers. #195

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-envers</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version>
<version>2.2.0.gh-47-SNAPSHOT</version>

<parent>
<groupId>org.springframework.data.build</groupId>
Expand Down Expand Up @@ -55,6 +55,12 @@
<version>${springdata.jpa}</version>
</dependency>

<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.2.0.DATACMNS-1550-SNAPSHOT</version>
</dependency>

<!-- Hibernate -->
<dependency>
<groupId>org.hibernate</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
package org.springframework.data.envers.repository.support;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NonNull;
import lombok.Value;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Objects;
import java.util.Optional;

import org.hibernate.envers.DefaultRevisionEntity;
Expand All @@ -31,14 +33,23 @@
/**
* {@link RevisionMetadata} working with a {@link DefaultRevisionEntity}.
*
* The entity/delegate itself gets ignored for {@link #equals(Object)} and {@link #hashCode()} since they depend on the
* way they were obtained.
*
* @author Oliver Gierke
* @author Philip Huegelmeyer
* @author Jens Schauder
*/
@Value
@AllArgsConstructor
public class DefaultRevisionMetadata implements RevisionMetadata<Integer> {

private final @NonNull @Getter(AccessLevel.NONE) DefaultRevisionEntity entity;
private final RevisionType revisionType;

public DefaultRevisionMetadata(DefaultRevisionEntity entity) {
this(entity, RevisionType.UNKNOWN);
}

/*
* (non-Javadoc)
Expand All @@ -57,7 +68,6 @@ public Optional<LocalDateTime> getRevisionDate() {
return getRevisionInstant().map(instant -> LocalDateTime.ofInstant(instant, ZoneOffset.systemDefault()));
}


/*
* (non-Javadoc)
* @see org.springframework.data.history.RevisionMetadata#getRevisionInstant()
Expand All @@ -75,4 +85,22 @@ public Optional<Instant> getRevisionInstant() {
public <T> T getDelegate() {
return (T) entity;
}

public RevisionType getRevisionType() {
return revisionType;
}
@Override
public boolean equals(Object o) {

if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DefaultRevisionMetadata that = (DefaultRevisionMetadata) o;
return getRevisionNumber().equals(that.getRevisionNumber())
&& getRevisionInstant().equals(that.getRevisionInstant())
&& revisionType.equals(that.getRevisionType());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,9 @@
*/
package org.springframework.data.envers.repository.support;

import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.persistence.EntityManager;

Expand All @@ -29,6 +26,10 @@
import org.hibernate.envers.DefaultRevisionEntity;
import org.hibernate.envers.RevisionNumber;
import org.hibernate.envers.RevisionTimestamp;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.AuditQuery;
import org.hibernate.envers.query.order.AuditOrder;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
Expand All @@ -41,10 +42,11 @@
import org.springframework.data.repository.core.EntityInformation;
import org.springframework.data.repository.history.RevisionRepository;
import org.springframework.data.repository.history.support.RevisionEntityInformation;
import org.springframework.data.util.StreamUtils;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import static org.springframework.data.history.RevisionMetadata.RevisionType.*;

/**
* Repository implementation using Hibernate Envers to implement revision specific query methods.
*
Expand Down Expand Up @@ -87,164 +89,134 @@ public EnversRevisionRepositoryImpl(JpaEntityInformation<T, ?> entityInformation
@SuppressWarnings("unchecked")
public Optional<Revision<N, T>> findLastChangeRevision(ID id) {

Class<T> type = entityInformation.getJavaType();
AuditReader reader = AuditReaderFactory.get(entityManager);

List<Number> revisions = getRevisions(id, type, reader);

if (revisions.isEmpty()) {
return Optional.empty();
}

N latestRevision = (N) revisions.get(revisions.size() - 1);
List<Object[]> singleResult = createBaseQuery(id) //
.addOrder(AuditEntity.revisionProperty("timestamp").desc()) //
.setMaxResults(1) //
.getResultList();

Class<?> revisionEntityClass = revisionEntityInformation.getRevisionEntityClass();
Assert.state(singleResult.size() <= 1, "We expect at most one result.");

Object revisionEntity = reader.findRevision(revisionEntityClass, latestRevision);
RevisionMetadata<N> metadata = (RevisionMetadata<N>) getRevisionMetadata(revisionEntity);

return Optional.of(Revision.of(metadata, reader.find(type, id, latestRevision)));
return singleResult.stream() //
.findFirst() //
.map(QueryResult::new) //
.map(this::createRevision);
}

/*
* (non-Javadoc)
* @see org.springframework.data.envers.repository.support.EnversRevisionRepository#findRevision(java.io.Serializable, java.lang.Number)
*/
@Override
@SuppressWarnings("unchecked")
public Optional<Revision<N, T>> findRevision(ID id, N revisionNumber) {

Assert.notNull(id, "Identifier must not be null!");
Assert.notNull(revisionNumber, "Revision number must not be null!");

return getEntityForRevision(revisionNumber, id, AuditReaderFactory.get(entityManager));
List<Object[]> singleResult = (List<Object[]>) createBaseQuery(id) //
.add(AuditEntity.revisionNumber().eq(revisionNumber)) //
.getResultList();

Assert.state(singleResult.size() <= 1, "We expect at most one result.");

return singleResult.stream() //
.findFirst() //
.map(QueryResult::new) //
.map(this::createRevision);

}

/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.RevisionRepository#findRevisions(java.io.Serializable)
*/
@SuppressWarnings("unchecked")
public Revisions<N, T> findRevisions(ID id) {

Class<T> type = entityInformation.getJavaType();
AuditReader reader = AuditReaderFactory.get(entityManager);
List<? extends Number> revisionNumbers = getRevisions(id, type, reader);
List<Object[]> resultList = createBaseQuery(id) //
.getResultList();

List<Revision<N, T>> revisionList = resultList.stream() //
.map(QueryResult::new) //
.map(this::createRevision) //
.collect(Collectors.toList());

return Revisions.of(revisionList);

return revisionNumbers.isEmpty() ? Revisions.none()
: getEntitiesForRevisions((List<N>) revisionNumbers, id, reader);
}

/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.RevisionRepository#findRevisions(java.io.Serializable, org.springframework.data.domain.Pageable)
*/
@SuppressWarnings("unchecked")
public Page<Revision<N, T>> findRevisions(ID id, Pageable pageable) {

Class<T> type = entityInformation.getJavaType();
AuditReader reader = AuditReaderFactory.get(entityManager);
List<Number> revisionNumbers = getRevisions((ID) id, (Class<T>) type, reader);
boolean isDescending = RevisionSort.getRevisionDirection(pageable.getSort()).isDescending();

if (isDescending) {
Collections.reverse(revisionNumbers);
}

if (
pageable.getOffset() >= revisionNumbers.size()) {
return Page.empty(pageable);
}

long upperBound = pageable.getOffset() + pageable.getPageSize();
upperBound = upperBound > revisionNumbers.size() ? revisionNumbers.size() : upperBound;
AuditOrder sorting = RevisionSort.getRevisionDirection(pageable.getSort()).isDescending() //
? AuditEntity.revisionNumber().desc() //
: AuditEntity.revisionNumber().asc();

List<? extends Number> subList = revisionNumbers.subList(toInt(pageable.getOffset()), toInt(upperBound));
Revisions<N, T> revisions = getEntitiesForRevisions((List<N>) subList, id, reader);
List<Object[]> resultList = createBaseQuery(id) //
.addOrder(sorting) //
.setFirstResult((int) pageable.getOffset()) //
.setMaxResults(pageable.getPageSize()) //
.getResultList();

revisions = isDescending ? revisions.reverse() : revisions;
Long count = (Long) createBaseQuery(id) //
.addProjection(AuditEntity.revisionNumber().count()).getSingleResult();

return new PageImpl<Revision<N, T>>(revisions.getContent(), pageable, revisionNumbers.size());
}
List<Revision<N, T>> revisions = resultList.stream()
.map(singleResult -> createRevision(new QueryResult(singleResult))).collect(Collectors.toList());

List<Number> getRevisions(ID id, Class<T> type, AuditReader reader) {
return reader.getRevisions(type, id);
return new PageImpl<>(revisions, pageable, count);
}

/**
* Returns the entities in the given revisions for the entitiy with the given id.
*
* @param revisionNumbers
* @param id
* @param reader
* @return
*/
@SuppressWarnings("unchecked")
private Revisions<N, T> getEntitiesForRevisions(List<N> revisionNumbers, ID id, AuditReader reader) {
private AuditQuery createBaseQuery(ID id) {

Class<T> type = entityInformation.getJavaType();
Map<N, T> revisions = new HashMap<N, T>(revisionNumbers.size());

Class<?> revisionEntityClass = revisionEntityInformation.getRevisionEntityClass();
Map<Number, Object> revisionEntities = (Map<Number, Object>) reader.findRevisions(revisionEntityClass,
new HashSet<Number>(revisionNumbers));

for (Number number : revisionNumbers) {
revisions.put((N) number, reader.find(type, type.getName(), id, number, true));
}
AuditReader reader = AuditReaderFactory.get(entityManager);

return Revisions.of(toRevisions(revisions, revisionEntities));
return reader.createQuery() //
.forRevisionsOfEntity(type, false, true) //
.add(AuditEntity.id().eq(id)) ;
}

/**
* Returns an entity in the given revision for the given entity-id.
*
* @param revisionNumber
* @param id
* @param reader
* @return
*/
@SuppressWarnings("unchecked")
private Optional<Revision<N, T>> getEntityForRevision(N revisionNumber, ID id, AuditReader reader) {

Class<?> type = revisionEntityInformation.getRevisionEntityClass();

T revision = (T) reader.findRevision(type, revisionNumber);
Optional<Object> entity = Optional.ofNullable(reader.find(entityInformation.getJavaType(), id, revisionNumber));
private Revision<N, T> createRevision(QueryResult queryResult) {

return entity.map(it -> Revision.of((RevisionMetadata<N>) getRevisionMetadata(revision), (T) it));
return Revision.of(queryResult.createRevisionMetadata(), queryResult.entity);
}

@SuppressWarnings("unchecked")
private List<Revision<N, T>> toRevisions(Map<N, T> source, Map<Number, Object> revisionEntities) {

return source.entrySet().stream() //
.map(entry -> Revision.of( //
(RevisionMetadata<N>) getRevisionMetadata(revisionEntities.get(entry.getKey())), //
entry.getValue())) //
.sorted() //
.collect(StreamUtils.toUnmodifiableList());
}
private class QueryResult {

private final T entity;
private final Object metadata;
private final RevisionMetadata.RevisionType revisionType;

QueryResult(Object[] data) {

Assert.notNull(data, "Data must not be null");
Assert.isTrue( //
data.length == 3, //
() -> String.format("Data must have length three, but has length %d.", data.length));
Assert.isTrue( //
data[2] instanceof RevisionType, //
() -> String.format("The third array element must be of type Revision type, but is of type %s",
data[2].getClass()));

entity = (T) data[0];
metadata = data[1];
revisionType = convertRevisionType(data[2]);
}

/**
* Returns the {@link RevisionMetadata} wrapper depending on the type of the given object.
*
* @param object
* @return
*/
private RevisionMetadata<?> getRevisionMetadata(Object object) {
private RevisionMetadata.RevisionType convertRevisionType(Object datum) {

return object instanceof DefaultRevisionEntity //
? new DefaultRevisionMetadata((DefaultRevisionEntity) object) //
: new AnnotationRevisionMetadata<N>(object, RevisionNumber.class, RevisionTimestamp.class);
}
switch ((RevisionType) datum){
case ADD:return INSERT;
case MOD:return UPDATE;
case DEL:return DELETE;
default:return UNKNOWN;
}
}

private static int toInt(long value) {
RevisionMetadata<N> createRevisionMetadata() {

if (value > Integer.MAX_VALUE) {
throw new IllegalStateException(String.format("%s can't be mapped to an integer, too large!", value));
return metadata instanceof DefaultRevisionEntity //
? (RevisionMetadata<N>) new DefaultRevisionMetadata((DefaultRevisionEntity) metadata, revisionType) //
: new AnnotationRevisionMetadata<>(metadata, RevisionNumber.class, RevisionTimestamp.class);
}

return Long.valueOf(value).intValue();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/**
* Spring Data JPA specific converter infrastructure.
*/
@org.springframework.lang.NonNullApi
package org.springframework.data.envers.repository.support;
Loading