diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/history/AnnotationRevisionMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/history/AnnotationRevisionMetadata.java new file mode 100755 index 000000000..1be211f20 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/history/AnnotationRevisionMetadata.java @@ -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> implements RevisionMetadata { + + 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 revisionNumberAnnotation, + Class 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 getDelegate() { + return (T) entity; + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/history/Revision.java b/spring-data-commons-core/src/main/java/org/springframework/data/history/Revision.java new file mode 100755 index 000000000..d530b0348 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/history/Revision.java @@ -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, T> implements Comparable> { + + private final RevisionMetadata 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 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 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 that = (Revision) 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); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/history/RevisionMetadata.java b/spring-data-commons-core/src/main/java/org/springframework/data/history/RevisionMetadata.java new file mode 100755 index 000000000..6d8cd64cf --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/history/RevisionMetadata.java @@ -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> { + + /** + * 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 getDelegate(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/history/Revisions.java b/spring-data-commons-core/src/main/java/org/springframework/data/history/Revisions.java new file mode 100644 index 000000000..5a1495717 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/history/Revisions.java @@ -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, T> implements Iterable> { + + private final List> 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> 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> revisions, boolean latestLast) { + + Assert.notNull(revisions); + this.revisions = new ArrayList>(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 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 reverse() { + List> result = new ArrayList>(revisions); + Collections.reverse(result); + return new Revisions(result, !latestLast); + } + + /* + * (non-Javadoc) + * @see java.lang.Iterable#iterator() + */ + public Iterator> iterator() { + return revisions.iterator(); + } +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/history/RevisionRepository.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/history/RevisionRepository.java new file mode 100755 index 000000000..ef30cffaf --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/history/RevisionRepository.java @@ -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> { + + /** + * Returns the revision of the entity it was last changed in. + * + * @param id must not be {@literal null}. + * @return + */ + Revision findLastChangeRevision(ID id); + + /** + * Returns all {@link Revisions} of an entity with the given id. + * + * @param id must not be {@literal null}. + * @return + */ + Revisions 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> findRevisions(ID id, Pageable pageable); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/repository/history/support/RevisionEntityInformation.java b/spring-data-commons-core/src/main/java/org/springframework/data/repository/history/support/RevisionEntityInformation.java new file mode 100644 index 000000000..e96072afc --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/repository/history/support/RevisionEntityInformation.java @@ -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(); +} diff --git a/spring-data-commons-core/src/main/java/org/springframework/data/util/AnnotationDetectionFieldCallback.java b/spring-data-commons-core/src/main/java/org/springframework/data/util/AnnotationDetectionFieldCallback.java new file mode 100755 index 000000000..be52b1326 --- /dev/null +++ b/spring-data-commons-core/src/main/java/org/springframework/data/util/AnnotationDetectionFieldCallback.java @@ -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 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 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 getValue(Object source) { + + Assert.notNull(source); + return field == null ? null : (T) ReflectionUtils.getField(field, source); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/history/RevisionUnitTests.java b/spring-data-commons-core/src/test/java/org/springframework/data/history/RevisionUnitTests.java new file mode 100755 index 000000000..445cd6a02 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/history/RevisionUnitTests.java @@ -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 firstMetadata, secondMetadata; + + @Test + @SuppressWarnings("unchecked") + public void comparesCorrectly() { + + when(firstMetadata.getRevisionNumber()).thenReturn(1); + when(secondMetadata.getRevisionNumber()).thenReturn(2); + + Revision first = new Revision(firstMetadata, new Object()); + Revision second = new Revision(secondMetadata, new Object()); + + List> revisions = Arrays.asList(second, first); + Collections.sort(revisions); + + assertThat(revisions.get(0), is(first)); + assertThat(revisions.get(1), is(second)); + } +} diff --git a/spring-data-commons-core/src/test/java/org/springframework/data/history/RevisionsUnitTest.java b/spring-data-commons-core/src/test/java/org/springframework/data/history/RevisionsUnitTest.java new file mode 100644 index 000000000..66745e801 --- /dev/null +++ b/spring-data-commons-core/src/test/java/org/springframework/data/history/RevisionsUnitTest.java @@ -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 first, second; + Revision firstRevision, secondRevision; + + @Before + public void setUp() { + + when(first.getRevisionNumber()).thenReturn(0); + when(second.getRevisionNumber()).thenReturn(10); + + firstRevision = new Revision(first, new Object()); + secondRevision = new Revision(second, new Object()); + } + + @Test + @SuppressWarnings("unchecked") + public void returnsCorrectLatestRevision() { + + Revisions revisions = new Revisions(Arrays.asList(firstRevision, secondRevision)); + assertThat(revisions.getLatestRevision(), is(secondRevision)); + } + + @Test + @SuppressWarnings("unchecked") + public void iteratesInCorrectOrder() { + + Revisions revisions = new Revisions(Arrays.asList(firstRevision, secondRevision)); + Iterator> 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 revisions = new Revisions(Arrays.asList(firstRevision, secondRevision)); + assertThat(revisions.reverse().getLatestRevision(), is(secondRevision)); + } + + @Test + @SuppressWarnings("unchecked") + public void iteratesReversedRevisionsInCorrectOrder() { + + Revisions revisions = new Revisions(Arrays.asList(firstRevision, secondRevision)); + Iterator> 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 revisions = new Revisions(Arrays.asList(secondRevision, firstRevision)); + Iterator> 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)); + } +}