DATACMNS-1020 - Improvements to revision API.

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.
This commit is contained in:
Oliver Gierke
2017-03-25 12:34:24 +01:00
parent 11dc67da2c
commit b5af2e9d96
8 changed files with 148 additions and 56 deletions

View File

@@ -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.time.LocalDateTime;
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 @@ import org.springframework.util.ReflectionUtils;
public class AnnotationRevisionMetadata<N extends Number & Comparable<N>> implements RevisionMetadata<N> {
private final Object entity;
private final N revisionNumber;
private final LocalDateTime revisionDate;
private final Lazy<Optional<N>> revisionNumber;
private final Lazy<Optional<LocalDateTime>> 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<? extends Annotation> revisionNumberAnnotation,
public AnnotationRevisionMetadata(Object entity, Class<? extends Annotation> revisionNumberAnnotation,
Class<? extends Annotation> revisionTimeStampAnnotation) {
Assert.notNull(entity, "Entity must not be null!");
Assert.notNull(revisionNumberAnnotation, "Revision number annotation must not be null!");
Assert.notNull(revisionTimeStampAnnotation, "Revision time stamp annotation 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;
}
if (revisionTimeStampAnnotation != null) {
AnnotationDetectionFieldCallback revisionCallback = new AnnotationDetectionFieldCallback(
revisionTimeStampAnnotation);
ReflectionUtils.doWithFields(entity.getClass(), revisionCallback);
this.revisionDate = revisionCallback.getValue(entity);
} else {
this.revisionDate = null;
}
this.revisionNumber = detectAnnotation(entity, revisionNumberAnnotation);
this.revisionDate = detectAnnotation(entity, revisionTimeStampAnnotation);
}
/*
@@ -72,7 +61,7 @@ public class AnnotationRevisionMetadata<N extends Number & Comparable<N>> implem
* @see org.springframework.data.repository.history.RevisionMetadata#getRevisionNumber()
*/
public Optional<N> getRevisionNumber() {
return Optional.ofNullable(revisionNumber);
return revisionNumber.get();
}
/*
@@ -80,7 +69,7 @@ public class AnnotationRevisionMetadata<N extends Number & Comparable<N>> implem
* @see org.springframework.data.history.RevisionMetadata#getRevisionDate()
*/
public Optional<LocalDateTime> getRevisionDate() {
return Optional.ofNullable(revisionDate);
return revisionDate.get();
}
/*
@@ -91,4 +80,14 @@ public class AnnotationRevisionMetadata<N extends Number & Comparable<N>> implem
public <T> T getDelegate() {
return (T) entity;
}
private static <T> Lazy<Optional<T>> detectAnnotation(Object entity, Class<? extends Annotation> annotationType) {
return Lazy.of(() -> {
AnnotationDetectionFieldCallback numberCallback = new AnnotationDetectionFieldCallback(annotationType);
ReflectionUtils.doWithFields(entity.getClass(), numberCallback);
return numberCallback.getValue(entity);
});
}
}

View File

@@ -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 final class Revision<N extends Number & Comparable<N>, T> implements Comp
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 final class Revision<N extends Number & Comparable<N>, T> implements Comp
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<N, ?> that) {
Optional<N> thisRevisionNumber = getRevisionNumber();
Optional<N> 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 final class Revision<N extends Number & Comparable<N>, T> implements Comp
*/
@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("<unknown>"), entity, metadata);
}
}

View File

@@ -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<N extends Number & Comparable<N>> {
/**
* Returns the revision number of the revision.
*
* @return
* @return will never be {@literal null}.
*/
Optional<N> 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<LocalDateTime> 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.
*

View File

@@ -19,8 +19,9 @@ import java.util.Collections;
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 @@ import org.springframework.util.Assert;
* @author Oliver Gierke
* @author Christoph Strobl
*/
public class Revisions<N extends Number & Comparable<N>, T> implements Iterable<Revision<N, T>> {
public class Revisions<N extends Number & Comparable<N>, T> implements Streamable<Revision<N, T>> {
private final Comparator<Revision<N, T>> NATURAL_ORDER = Comparator.naturalOrder();
@@ -59,15 +60,29 @@ public class Revisions<N extends Number & Comparable<N>, T> implements Iterable<
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 <N extends Number & Comparable<N>, T> Revisions<N, T> of(List<? extends Revision<N, T>> revisions) {
return new Revisions<>(revisions);
}
/**
* Creates a new empty {@link Revisions} instance.
*
* @return will never be {@literal null}.
*/
public static <N extends Number & Comparable<N>, T> Revisions<N, T> none() {
return new Revisions<>(Collections.emptyList());
}
/**
* Returns the latest revision of the revisions backing the wrapper independently of the order.
*

View File

@@ -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<T, ID extends Serializable, N extends Number
* @param id must not be {@literal null}.
* @return
*/
Revision<N, T> findLastChangeRevision(ID id);
Optional<Revision<N, T>> findLastChangeRevision(ID id);
/**
* Returns all {@link Revisions} of an entity with the given id.
@@ -70,5 +71,5 @@ public interface RevisionRepository<T, ID extends Serializable, N extends Number
* @return the entity with the given ID in the given revision number.
* @since 1.12
*/
Revision<N, T> findRevision(ID id, N revisionNumber);
Optional<Revision<N, T>> findRevision(ID id, N revisionNumber);
}

View File

@@ -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 @@ package org.springframework.data.util;
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 @@ import org.springframework.util.ReflectionUtils.FieldCallback;
public class AnnotationDetectionFieldCallback implements FieldCallback {
private final Class<? extends Annotation> annotationType;
private Field field;
private Optional<Field> 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<? extends Annotation> annotationType) {
Assert.notNull(annotationType, "AnnotationType must not be null!");
this.annotationType = annotationType;
}
@@ -52,16 +54,14 @@ public class AnnotationDetectionFieldCallback implements FieldCallback {
*/
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
if (this.field != null) {
if (this.field.isPresent()) {
return;
}
Annotation annotation = AnnotatedElementUtils.findMergedAnnotation(field, annotationType);
if (AnnotatedElementUtils.findMergedAnnotation(field, annotationType) != null) {
if (annotation != null) {
this.field = field;
ReflectionUtils.makeAccessible(this.field);
ReflectionUtils.makeAccessible(field);
this.field = Optional.of(field);
}
}
@@ -70,8 +70,20 @@ public class AnnotationDetectionFieldCallback implements FieldCallback {
*
* @return
*/
public Class<?> getType() {
return field == null ? null : field.getType();
public Optional<Class<?>> 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 AnnotationDetectionFieldCallback implements FieldCallback {
* @return
*/
@SuppressWarnings("unchecked")
public <T> T getValue(Object source) {
public <T> Optional<T> 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));
}
}

View File

@@ -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 interface StreamUtils {
? stream.onClose(() -> ((CloseableIterator<T>) iterator).close()) //
: stream;
}
/**
* Returns a {@link Collector} to create an unmodifiable {@link List}.
*
* @return will never be {@literal null}.
*/
public static <T> Collector<T, ?, List<T>> toUnmodifiableList() {
return collectingAndThen(toList(), Collections::unmodifiableList);
}
/**
* Returns a {@link Collector} to create an unmodifiable {@link Set}.
*
* @return will never be {@literal null}.
*/
public static <T> Collector<T, ?, Set<T>> toUnmodifiableSet() {
return collectingAndThen(toSet(), Collections::unmodifiableSet);
}
}