DATACMNS-135 - Added basic infrastructure to support historiography of data.

Added interfaces for common abstractions needed to implement historiography. A Revision is an entity associated with some RevisionMetadata. That in turn consists of a revision number and date at least and is backed by a implementation specific delegate. The Revisions wrapper class allows accessing the latest revision from a list of Revision instances no matter what order they are in. A RevisionRepository allows access the revision an entity was changed in the last time, access all Revisions of an entity as well as paged access to it.
This commit is contained in:
Oliver Gierke
2012-03-20 11:22:57 +01:00
parent 2f39b74cbc
commit f8d5e310a7
9 changed files with 688 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.history;
import java.lang.annotation.Annotation;
import org.joda.time.DateTime;
import org.springframework.data.util.AnnotationDetectionFieldCallback;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* A {@link RevisionMetadata} implementation that inspects the given object for fields with the configured annotations
* and returns the field's values on calls to {@link #getRevisionDate()} and {@link #getRevisionNumber()}.
*
* @author Oliver Gierke
*/
public class AnnotationRevisionMetadata<N extends Number & Comparable<N>> implements RevisionMetadata<N> {
private final Object entity;
private final N revisionNumber;
private final DateTime revisionDate;
/**
* Creates a new {@link AnnotationRevisionMetadata} inspecing the given entity for the given annotations. If no
* annotations will be provided these values will not be looked up from the entity and return {@literal null}.
*
* @param entity must not be {@literal null}.
* @param revisionNumberAnnotation
* @param revisionTimeStampAnnotation
*/
public AnnotationRevisionMetadata(final Object entity, Class<? extends Annotation> revisionNumberAnnotation,
Class<? extends Annotation> revisionTimeStampAnnotation) {
Assert.notNull(entity);
this.entity = entity;
if (revisionNumberAnnotation != null) {
AnnotationDetectionFieldCallback numberCallback = new AnnotationDetectionFieldCallback(revisionNumberAnnotation);
ReflectionUtils.doWithFields(entity.getClass(), numberCallback);
this.revisionNumber = numberCallback.getValue(entity);
} else {
this.revisionNumber = null;
}
if (revisionTimeStampAnnotation != null) {
AnnotationDetectionFieldCallback revisionCallback = new AnnotationDetectionFieldCallback(
revisionTimeStampAnnotation);
ReflectionUtils.doWithFields(entity.getClass(), revisionCallback);
this.revisionDate = new DateTime(revisionCallback.getValue(entity));
} else {
this.revisionDate = null;
}
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.RevisionMetadata#getRevisionNumber()
*/
public N getRevisionNumber() {
return revisionNumber;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.RevisionMetadata#getRevisionDate()
*/
public DateTime getRevisionDate() {
return revisionDate;
}
/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.RevisionMetadata#getDelegate()
*/
@SuppressWarnings("unchecked")
public <T> T getDelegate() {
return (T) entity;
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.history;
import org.springframework.util.Assert;
/**
* Wrapper to contain {@link RevisionMetadata} as well as the revisioned entity.
*
* @author Oliver Gierke
* @author Philipp Huegelmeyer
*/
public final class Revision<N extends Number & Comparable<N>, T> implements Comparable<Revision<N, ?>> {
private final RevisionMetadata<? extends N> metadata;
private final T entity;
/**
* Creates a new {@link Revision} consisting of the given {@link RevisionMetadata} and entity.
*
* @param metadata must not be {@literal null}.
* @param entity must not be {@literal null}.
*/
public Revision(RevisionMetadata<? extends N> metadata, T entity) {
Assert.notNull(metadata);
Assert.notNull(entity);
this.metadata = metadata;
this.entity = entity;
}
/**
* Returns the revision number of the revision.
*
* @return the revision number.
*/
public N getRevisionNumber() {
return metadata.getRevisionNumber();
}
/**
* Returns the underlying entity.
*
* @return the entity
*/
public T getEntity() {
return entity;
}
/*
* (non-Javadoc)
* @see java.lang.Comparable#compareTo(java.lang.Object)
*/
public int compareTo(Revision<N, ?> that) {
return getRevisionNumber().compareTo(that.getRevisionNumber());
}
/* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
@SuppressWarnings("unchecked")
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (!(obj instanceof Revision)) {
return false;
}
Revision<N, T> that = (Revision<N, T>) obj;
boolean sameRevisionNumber = this.metadata.getRevisionNumber().equals(that.metadata.getRevisionNumber());
return !sameRevisionNumber ? false : this.entity.equals(that.entity);
}
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
int result = 17;
result += 31 * metadata.hashCode();
result += 31 * entity.hashCode();
return result;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return String.format("Revision %s of entity %s - Revision metadata %s", getRevisionNumber(), entity, metadata);
}
}

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.history;
import org.joda.time.DateTime;
/**
* Metadata about a revision.
*
* @author Philipp Huegelmeyer
* @author Oliver Gierke
*/
public interface RevisionMetadata<N extends Number & Comparable<N>> {
/**
* Returns the revision number of the revision.
*
* @return
*/
N getRevisionNumber();
/**
* Returns the date of the revision.
*
* @return
*/
DateTime getRevisionDate();
/**
* Returns the underlying revision metadata which might provider more detailed implementation specific information.
*
* @return
*/
<T> T getDelegate();
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.history;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.springframework.util.Assert;
/**
* Simple wrapper class for a {@link List} of {@link Revisions} allowing to canonically access the latest revision.
* Allows iterating over the underlying {@link Revisions} starting with older revisions.
*
* @author Oliver Gierke
*/
public class Revisions<N extends Number & Comparable<N>, T> implements Iterable<Revision<N, T>> {
private final List<Revision<N, T>> revisions;
private final boolean latestLast;
/**
* Creates a new {@link Revisions} instance containing the given revisions. Will make sure they are ordered
* ascendingly.
*
* @param revisions must not be {@literal null}.
*/
public Revisions(List<? extends Revision<N, T>> revisions) {
this(revisions, true);
}
/**
* Creates a new {@link Revisions} instance using the given revisions.
*
* @param revisions must not be {@literal null}.
* @param latestLast
*/
private Revisions(List<? extends Revision<N, T>> revisions, boolean latestLast) {
Assert.notNull(revisions);
this.revisions = new ArrayList<Revision<N, T>>(revisions);
this.latestLast = latestLast;
Collections.sort(this.revisions);
if (!latestLast) {
Collections.reverse(this.revisions);
}
}
/**
* Returns the latest revision of the revisions backing the wrapper independently of the order.
*
* @return
*/
public Revision<N, T> getLatestRevision() {
int index = latestLast ? revisions.size() - 1 : 0;
return revisions.get(index);
}
/**
* Reverses the current {@link Revisions}. By default this will return the revisions with the latest revision first.
*
* @return
*/
public Revisions<N, T> reverse() {
List<Revision<N, T>> result = new ArrayList<Revision<N, T>>(revisions);
Collections.reverse(result);
return new Revisions<N, T>(result, !latestLast);
}
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
public Iterator<Revision<N, T>> iterator() {
return revisions.iterator();
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.repository.history;
import java.io.Serializable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.history.Revision;
import org.springframework.data.history.Revisions;
import org.springframework.data.repository.NoRepositoryBean;
/**
* A repository which can access entities held in a variety of {@link Revisions}.
*
* @author Oliver Gierke
* @author Philipp Huegelmeyer
*/
@NoRepositoryBean
public interface RevisionRepository<T, ID extends Serializable, N extends Number & Comparable<N>> {
/**
* Returns the revision of the entity it was last changed in.
*
* @param id must not be {@literal null}.
* @return
*/
Revision<N, T> findLastChangeRevision(ID id);
/**
* Returns all {@link Revisions} of an entity with the given id.
*
* @param id must not be {@literal null}.
* @return
*/
Revisions<N, T> findRevisions(ID id);
/**
* Returns a {@link Page} of revisions for the entity with the given id.
*
* @param id must not be {@literal null}.
* @param pageable
* @return
*/
Page<Revision<N, T>> findRevisions(ID id, Pageable pageable);
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.repository.history.support;
/**
* Information about a revision entity which is the class carrying revision information for an entity.
*
* @author Oliver Gierke
*/
public interface RevisionEntityInformation {
Class<?> getRevisionNumberType();
boolean isDefaultRevisionEntity();
Class<?> getRevisionEntityClass();
}

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.util;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.FieldCallback;
/**
* A {@link FieldCallback} that will inspect each field for a given annotation. Thie fields type can then be accessed
* afterwards.
*
* @author Oliver Gierke
*/
public class AnnotationDetectionFieldCallback implements FieldCallback {
private final Class<? extends Annotation> annotationType;
private Field field;
/**
* Creates a new {@link AnnotationDetectionFieldCallback} scanning for an annotation of the given type.
*
* @param annotationType must not be {@literal null}.
*/
public AnnotationDetectionFieldCallback(Class<? extends Annotation> annotationType) {
Assert.notNull(annotationType);
this.annotationType = annotationType;
}
/*
* (non-Javadoc)
* @see org.springframework.util.ReflectionUtils.FieldCallback#doWith(java.lang.reflect.Field)
*/
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
if (this.field != null) {
return;
}
Annotation annotation = field.getAnnotation(annotationType);
if (annotation != null) {
this.field = field;
}
}
/**
* Returns the type of the field.
*
* @return
*/
public Class<?> getType() {
return field == null ? null : field.getType();
}
/**
* Retrieves the value of the field by reflection.
*
* @param source must not be {@literal null}.
* @return
*/
@SuppressWarnings("unchecked")
public <T> T getValue(Object source) {
Assert.notNull(source);
return field == null ? null : (T) ReflectionUtils.getField(field, source);
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.history;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
/**
* Unit tests for {@link RevisionMetadata}.
*
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class RevisionUnitTests {
@Mock
RevisionMetadata<Integer> firstMetadata, secondMetadata;
@Test
@SuppressWarnings("unchecked")
public void comparesCorrectly() {
when(firstMetadata.getRevisionNumber()).thenReturn(1);
when(secondMetadata.getRevisionNumber()).thenReturn(2);
Revision<Integer, Object> first = new Revision<Integer, Object>(firstMetadata, new Object());
Revision<Integer, Object> second = new Revision<Integer, Object>(secondMetadata, new Object());
List<Revision<Integer, Object>> revisions = Arrays.asList(second, first);
Collections.sort(revisions);
assertThat(revisions.get(0), is(first));
assertThat(revisions.get(1), is(second));
}
}

View File

@@ -0,0 +1,110 @@
/*
* Copyright 2012 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.history;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Iterator;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
/**
* Unit tests for {@link Revisions}.
*
* @author Oliver Gierke
*/
@RunWith(MockitoJUnitRunner.class)
public class RevisionsUnitTest {
@Mock
RevisionMetadata<Integer> first, second;
Revision<Integer, Object> firstRevision, secondRevision;
@Before
public void setUp() {
when(first.getRevisionNumber()).thenReturn(0);
when(second.getRevisionNumber()).thenReturn(10);
firstRevision = new Revision<Integer, Object>(first, new Object());
secondRevision = new Revision<Integer, Object>(second, new Object());
}
@Test
@SuppressWarnings("unchecked")
public void returnsCorrectLatestRevision() {
Revisions<Integer, Object> revisions = new Revisions<Integer, Object>(Arrays.asList(firstRevision, secondRevision));
assertThat(revisions.getLatestRevision(), is(secondRevision));
}
@Test
@SuppressWarnings("unchecked")
public void iteratesInCorrectOrder() {
Revisions<Integer, Object> revisions = new Revisions<Integer, Object>(Arrays.asList(firstRevision, secondRevision));
Iterator<Revision<Integer, Object>> iterator = revisions.iterator();
assertThat(iterator.hasNext(), is(true));
assertThat(iterator.next(), is(firstRevision));
assertThat(iterator.hasNext(), is(true));
assertThat(iterator.next(), is(secondRevision));
assertThat(iterator.hasNext(), is(false));
}
@Test
@SuppressWarnings("unchecked")
public void reversedRevisionsStillReturnsCorrectLatestRevision() {
Revisions<Integer, Object> revisions = new Revisions<Integer, Object>(Arrays.asList(firstRevision, secondRevision));
assertThat(revisions.reverse().getLatestRevision(), is(secondRevision));
}
@Test
@SuppressWarnings("unchecked")
public void iteratesReversedRevisionsInCorrectOrder() {
Revisions<Integer, Object> revisions = new Revisions<Integer, Object>(Arrays.asList(firstRevision, secondRevision));
Iterator<Revision<Integer, Object>> iterator = revisions.reverse().iterator();
assertThat(iterator.hasNext(), is(true));
assertThat(iterator.next(), is(secondRevision));
assertThat(iterator.hasNext(), is(true));
assertThat(iterator.next(), is(firstRevision));
assertThat(iterator.hasNext(), is(false));
}
@Test
@SuppressWarnings("unchecked")
public void forcesInvalidlyOrderedRevisionsToBeOrdered() {
Revisions<Integer, Object> revisions = new Revisions<Integer, Object>(Arrays.asList(secondRevision, firstRevision));
Iterator<Revision<Integer, Object>> iterator = revisions.iterator();
assertThat(iterator.hasNext(), is(true));
assertThat(iterator.next(), is(firstRevision));
assertThat(iterator.hasNext(), is(true));
assertThat(iterator.next(), is(secondRevision));
assertThat(iterator.hasNext(), is(false));
}
}