Skip to content

Commit

Permalink
DATACMNS-1020 - Improvements to revision API.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
odrotbohm committed Mar 25, 2017
1 parent 11dc67d commit b5af2e9
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 56 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

Expand All @@ -32,55 +33,43 @@
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!");
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);
}

/*
* (non-Javadoc)
* @see org.springframework.data.repository.history.RevisionMetadata#getRevisionNumber()
*/
public Optional<N> getRevisionNumber() {
return Optional.ofNullable(revisionNumber);
return revisionNumber.get();
}

/*
* (non-Javadoc)
* @see org.springframework.data.history.RevisionMetadata#getRevisionDate()
*/
public Optional<LocalDateTime> getRevisionDate() {
return Optional.ofNullable(revisionDate);
return revisionDate.get();
}

/*
Expand All @@ -91,4 +80,14 @@ public Optional<LocalDateTime> getRevisionDate() {
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);
});
}
}
31 changes: 25 additions & 6 deletions src/main/java/org/springframework/data/history/Revision.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -64,6 +66,15 @@ public Optional<N> 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.
*
Expand All @@ -73,16 +84,22 @@ public Optional<LocalDateTime> 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<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);
}

/*
Expand All @@ -91,6 +108,8 @@ public int compareTo(Revision<N, ?> 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("<unknown>"), entity, metadata);
}
}
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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.
*
Expand Down
21 changes: 18 additions & 3 deletions src/main/java/org/springframework/data/history/Revisions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -30,7 +31,7 @@
* @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();

Expand Down Expand Up @@ -59,15 +60,29 @@ private Revisions(List<? extends Revision<N, T>> 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 <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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -33,7 +34,7 @@
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.
Expand All @@ -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;
}

Expand All @@ -52,16 +54,14 @@ public AnnotationDetectionFieldCallback(Class<? extends Annotation> 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);
}
}

Expand All @@ -70,8 +70,20 @@ public void doWith(Field field) throws IllegalArgumentException, IllegalAccessEx
*
* @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)));
}

/**
Expand All @@ -81,9 +93,10 @@ public Class<?> getType() {
* @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));
}
}
Loading

0 comments on commit b5af2e9

Please sign in to comment.