From b5af2e9d9685f176d0a5ef0154baeb93b698a556 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Sat, 25 Mar 2017 12:34:24 +0100 Subject: [PATCH] DATACMNS-1020 - Improvements to revision API. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RevisionRepository now returns Optional for methods that could previously return null. Revision exposes additional methods to lookup required revision numbers and dates. Revisions now implements Streamable and exposes a ….none() factory method to create an empty instance. AnnotationBasedRevisionMetadata now uses Lazy to lookup the fields with revision annotations. AnnotationDetectionFieldCallback now also uses Optional in places it previously returned null. StreamUtils now exposes factory methods for Collector instances producing unmodifiable List and Set instances. Related ticket: DATACMNS-867. --- .../history/AnnotationRevisionMetadata.java | 49 +++++++++---------- .../data/history/Revision.java | 31 +++++++++--- .../data/history/RevisionMetadata.java | 28 +++++++++-- .../data/history/Revisions.java | 21 ++++++-- .../history/RevisionRepository.java | 5 +- .../AnnotationDetectionFieldCallback.java | 37 +++++++++----- .../data/util/StreamUtils.java | 24 +++++++++ ...tationDetectionFieldCallbackUnitTests.java | 9 ++-- 8 files changed, 148 insertions(+), 56 deletions(-) diff --git a/src/main/java/org/springframework/data/history/AnnotationRevisionMetadata.java b/src/main/java/org/springframework/data/history/AnnotationRevisionMetadata.java index b8830ebe8c..a7871240ad 100755 --- a/src/main/java/org/springframework/data/history/AnnotationRevisionMetadata.java +++ b/src/main/java/org/springframework/data/history/AnnotationRevisionMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2017 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. @@ -20,6 +20,7 @@ import java.util.Optional; import org.springframework.data.util.AnnotationDetectionFieldCallback; +import org.springframework.data.util.Lazy; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -32,39 +33,27 @@ public class AnnotationRevisionMetadata> implements RevisionMetadata { private final Object entity; - private final N revisionNumber; - private final LocalDateTime revisionDate; + private final Lazy> revisionNumber; + private final Lazy> revisionDate; /** * Creates a new {@link AnnotationRevisionMetadata} inspecting 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 + * @param revisionNumberAnnotation must not be {@literal null}. + * @param revisionTimeStampAnnotation must not be {@literal null}. */ - public AnnotationRevisionMetadata(final Object entity, Class revisionNumberAnnotation, + public AnnotationRevisionMetadata(Object entity, Class revisionNumberAnnotation, Class revisionTimeStampAnnotation) { Assert.notNull(entity, "Entity must not be null!"); - 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; - } + Assert.notNull(revisionNumberAnnotation, "Revision number annotation must not be null!"); + Assert.notNull(revisionTimeStampAnnotation, "Revision time stamp annotation must not be null!"); - if (revisionTimeStampAnnotation != null) { - AnnotationDetectionFieldCallback revisionCallback = new AnnotationDetectionFieldCallback( - revisionTimeStampAnnotation); - ReflectionUtils.doWithFields(entity.getClass(), revisionCallback); - this.revisionDate = revisionCallback.getValue(entity); - } else { - this.revisionDate = null; - } + this.entity = entity; + this.revisionNumber = detectAnnotation(entity, revisionNumberAnnotation); + this.revisionDate = detectAnnotation(entity, revisionTimeStampAnnotation); } /* @@ -72,7 +61,7 @@ public AnnotationRevisionMetadata(final Object entity, Class getRevisionNumber() { - return Optional.ofNullable(revisionNumber); + return revisionNumber.get(); } /* @@ -80,7 +69,7 @@ public Optional getRevisionNumber() { * @see org.springframework.data.history.RevisionMetadata#getRevisionDate() */ public Optional getRevisionDate() { - return Optional.ofNullable(revisionDate); + return revisionDate.get(); } /* @@ -91,4 +80,14 @@ public Optional getRevisionDate() { public T getDelegate() { return (T) entity; } + + private static Lazy> detectAnnotation(Object entity, Class annotationType) { + + return Lazy.of(() -> { + + AnnotationDetectionFieldCallback numberCallback = new AnnotationDetectionFieldCallback(annotationType); + ReflectionUtils.doWithFields(entity.getClass(), numberCallback); + return numberCallback.getValue(entity); + }); + } } diff --git a/src/main/java/org/springframework/data/history/Revision.java b/src/main/java/org/springframework/data/history/Revision.java index 607e9824e0..da515e41ea 100755 --- a/src/main/java/org/springframework/data/history/Revision.java +++ b/src/main/java/org/springframework/data/history/Revision.java @@ -15,6 +15,8 @@ */ package org.springframework.data.history; +import static org.springframework.data.util.Optionals.*; + import lombok.AccessLevel; import lombok.NonNull; import lombok.RequiredArgsConstructor; @@ -64,6 +66,15 @@ public Optional getRevisionNumber() { return metadata.getRevisionNumber(); } + /** + * Returns the revision number of the revision, immediately failing on absence. + * + * @return the revision number. + */ + public N getRequiredRevisionNumber() { + return metadata.getRequiredRevisionNumber(); + } + /** * Returns the revision date of the revision. * @@ -73,16 +84,22 @@ public Optional getRevisionDate() { return metadata.getRevisionDate(); } + /** + * Returns the revision date of the revision, immediately failing on absence. + * + * @return the revision date. + */ + public LocalDateTime getRequiredRevisionDate() { + return metadata.getRequiredRevisionDate(); + } + /* * (non-Javadoc) * @see java.lang.Comparable#compareTo(java.lang.Object) */ public int compareTo(Revision that) { - - Optional thisRevisionNumber = getRevisionNumber(); - Optional thatRevisionNumber = that.getRevisionNumber(); - - return thisRevisionNumber.map(left -> thatRevisionNumber.map(left::compareTo).orElse(1)).orElse(-1); + return mapIfAllPresent(getRevisionNumber(), that.getRevisionNumber(), // + (left, right) -> left.compareTo(right)).orElse(-1); } /* @@ -91,6 +108,8 @@ public int compareTo(Revision that) { */ @Override public String toString() { - return String.format("Revision %s of entity %s - Revision metadata %s", getRevisionNumber(), entity, metadata); + + return String.format("Revision %s of entity %s - Revision metadata %s", + getRevisionNumber().map(Object::toString).orElse(""), entity, metadata); } } diff --git a/src/main/java/org/springframework/data/history/RevisionMetadata.java b/src/main/java/org/springframework/data/history/RevisionMetadata.java index cd32c10837..72cebaa785 100755 --- a/src/main/java/org/springframework/data/history/RevisionMetadata.java +++ b/src/main/java/org/springframework/data/history/RevisionMetadata.java @@ -1,5 +1,5 @@ /* - * Copyright 2012 the original author or authors. + * Copyright 2012-2017 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. @@ -29,17 +29,39 @@ public interface RevisionMetadata> { /** * Returns the revision number of the revision. * - * @return + * @return will never be {@literal null}. */ Optional getRevisionNumber(); + /** + * Returns the revision number of the revision, immediately failing on absence. + * + * @return will never be {@literal null}. + * @throws IllegalStateException if no revision number is available. + */ + default N getRequiredRevisionNumber() { + return getRevisionNumber() + .orElseThrow(() -> new IllegalStateException(String.format("No revision number found on %s!", getDelegate()))); + } + /** * Returns the date of the revision. * - * @return + * @return will never be {@literal null}. */ Optional getRevisionDate(); + /** + * Returns the revision date of the revision, immediately failing on absence. + * + * @return will never be {@literal null}. + * @throw IllegalStateException if no revision date is available. + */ + default LocalDateTime getRequiredRevisionDate() { + return getRevisionDate() + .orElseThrow(() -> new IllegalStateException(String.format("No revision date found on %s!", getDelegate()))); + } + /** * Returns the underlying revision metadata which might provider more detailed implementation specific information. * diff --git a/src/main/java/org/springframework/data/history/Revisions.java b/src/main/java/org/springframework/data/history/Revisions.java index daad89bc76..e2925b4b18 100644 --- a/src/main/java/org/springframework/data/history/Revisions.java +++ b/src/main/java/org/springframework/data/history/Revisions.java @@ -19,8 +19,9 @@ import java.util.Comparator; import java.util.Iterator; import java.util.List; -import java.util.stream.Collectors; +import org.springframework.data.util.StreamUtils; +import org.springframework.data.util.Streamable; import org.springframework.util.Assert; /** @@ -30,7 +31,7 @@ * @author Oliver Gierke * @author Christoph Strobl */ -public class Revisions, T> implements Iterable> { +public class Revisions, T> implements Streamable> { private final Comparator> NATURAL_ORDER = Comparator.naturalOrder(); @@ -59,15 +60,29 @@ private Revisions(List> revisions, boolean latestLast) this.revisions = revisions.stream()// .sorted(latestLast ? NATURAL_ORDER : NATURAL_ORDER.reversed())// - .collect(Collectors.toList()); + .collect(StreamUtils.toUnmodifiableList()); this.latestLast = latestLast; } + /** + * Creates a new {@link Revisions} instance for the given {@link Revision}s. + * + * @return will never be {@literal null}. + */ public static , T> Revisions of(List> revisions) { return new Revisions<>(revisions); } + /** + * Creates a new empty {@link Revisions} instance. + * + * @return will never be {@literal null}. + */ + public static , T> Revisions none() { + return new Revisions<>(Collections.emptyList()); + } + /** * Returns the latest revision of the revisions backing the wrapper independently of the order. * diff --git a/src/main/java/org/springframework/data/repository/history/RevisionRepository.java b/src/main/java/org/springframework/data/repository/history/RevisionRepository.java index b9aa241e36..94b0bc376a 100755 --- a/src/main/java/org/springframework/data/repository/history/RevisionRepository.java +++ b/src/main/java/org/springframework/data/repository/history/RevisionRepository.java @@ -16,6 +16,7 @@ package org.springframework.data.repository.history; import java.io.Serializable; +import java.util.Optional; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -41,7 +42,7 @@ public interface RevisionRepository findLastChangeRevision(ID id); + Optional> findLastChangeRevision(ID id); /** * Returns all {@link Revisions} of an entity with the given id. @@ -70,5 +71,5 @@ public interface RevisionRepository findRevision(ID id, N revisionNumber); + Optional> findRevision(ID id, N revisionNumber); } diff --git a/src/main/java/org/springframework/data/util/AnnotationDetectionFieldCallback.java b/src/main/java/org/springframework/data/util/AnnotationDetectionFieldCallback.java index 84ba764496..de09f21071 100755 --- a/src/main/java/org/springframework/data/util/AnnotationDetectionFieldCallback.java +++ b/src/main/java/org/springframework/data/util/AnnotationDetectionFieldCallback.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. @@ -17,6 +17,7 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; +import java.util.Optional; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.util.Assert; @@ -33,7 +34,7 @@ public class AnnotationDetectionFieldCallback implements FieldCallback { private final Class annotationType; - private Field field; + private Optional field = Optional.empty(); /** * Creates a new {@link AnnotationDetectionFieldCallback} scanning for an annotation of the given type. @@ -43,6 +44,7 @@ public class AnnotationDetectionFieldCallback implements FieldCallback { public AnnotationDetectionFieldCallback(Class annotationType) { Assert.notNull(annotationType, "AnnotationType must not be null!"); + this.annotationType = annotationType; } @@ -52,16 +54,14 @@ public AnnotationDetectionFieldCallback(Class annotationTy */ public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException { - if (this.field != null) { + if (this.field.isPresent()) { return; } - Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(field, annotationType); - - if (annotation != null) { + if (AnnotatedElementUtils.findMergedAnnotation(field, annotationType) != null) { - this.field = field; - ReflectionUtils.makeAccessible(this.field); + ReflectionUtils.makeAccessible(field); + this.field = Optional.of(field); } } @@ -70,8 +70,20 @@ public void doWith(Field field) throws IllegalArgumentException, IllegalAccessEx * * @return */ - public Class getType() { - return field == null ? null : field.getType(); + public Optional> getType() { + return field.map(Field::getType); + } + + /** + * Returns the type of the field or throws an {@link IllegalArgumentException} if no field could be found. + * + * @return + * @throws IllegalStateException + */ + public Class getRequiredType() { + + return getType().orElseThrow(() -> new IllegalStateException( + String.format("Unable to obtain type! Didn't find field with annotation %s!", annotationType))); } /** @@ -81,9 +93,10 @@ public Class getType() { * @return */ @SuppressWarnings("unchecked") - public T getValue(Object source) { + public Optional getValue(Object source) { Assert.notNull(source, "Source object must not be null!"); - return field == null ? null : (T) ReflectionUtils.getField(field, source); + + return field.map(it -> (T) ReflectionUtils.getField(it, source)); } } diff --git a/src/main/java/org/springframework/data/util/StreamUtils.java b/src/main/java/org/springframework/data/util/StreamUtils.java index 1cb8fb4b00..ea84b25540 100644 --- a/src/main/java/org/springframework/data/util/StreamUtils.java +++ b/src/main/java/org/springframework/data/util/StreamUtils.java @@ -15,9 +15,15 @@ */ package org.springframework.data.util; +import static java.util.stream.Collectors.*; + +import java.util.Collections; import java.util.Iterator; +import java.util.List; +import java.util.Set; import java.util.Spliterator; import java.util.Spliterators; +import java.util.stream.Collector; import java.util.stream.Stream; import java.util.stream.StreamSupport; @@ -53,4 +59,22 @@ public static Stream createStreamFromIterator(Iterator iterator) { ? stream.onClose(() -> ((CloseableIterator) iterator).close()) // : stream; } + + /** + * Returns a {@link Collector} to create an unmodifiable {@link List}. + * + * @return will never be {@literal null}. + */ + public static Collector> toUnmodifiableList() { + return collectingAndThen(toList(), Collections::unmodifiableList); + } + + /** + * Returns a {@link Collector} to create an unmodifiable {@link Set}. + * + * @return will never be {@literal null}. + */ + public static Collector> toUnmodifiableSet() { + return collectingAndThen(toSet(), Collections::unmodifiableSet); + } } diff --git a/src/test/java/org/springframework/data/util/AnnotationDetectionFieldCallbackUnitTests.java b/src/test/java/org/springframework/data/util/AnnotationDetectionFieldCallbackUnitTests.java index 53e52d0d08..f6432ba529 100755 --- a/src/test/java/org/springframework/data/util/AnnotationDetectionFieldCallbackUnitTests.java +++ b/src/test/java/org/springframework/data/util/AnnotationDetectionFieldCallbackUnitTests.java @@ -36,14 +36,13 @@ public void rejectsNullAnnotationType() { } @Test // DATACMNS-616 - @SuppressWarnings("rawtypes") public void looksUpValueFromPrivateField() { AnnotationDetectionFieldCallback callback = new AnnotationDetectionFieldCallback(Autowired.class); ReflectionUtils.doWithFields(Sample.class, callback); - assertThat(callback.getType()).isEqualTo(String.class); - assertThat(callback. getValue(new Sample("foo"))).isEqualTo("foo"); + assertThat(callback.getType()).hasValue(String.class); + assertThat(callback.getValue(new Sample("foo"))).hasValue("foo"); } @Test // DATACMNS-616 @@ -52,8 +51,8 @@ public void returnsNullForObjectNotContainingAFieldWithTheConfiguredAnnotation() AnnotationDetectionFieldCallback callback = new AnnotationDetectionFieldCallback(Autowired.class); ReflectionUtils.doWithFields(Empty.class, callback); - assertThat(callback.getType()).isNull(); - assertThat(callback. getValue(new Empty())).isNull(); + assertThat(callback.getType()).isNotPresent(); + assertThat(callback.getValue(new Empty())).isNotPresent(); } @Value