diff --git a/pom.xml b/pom.xml index 1db9b70ebb..087370b2ce 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 2.0.0.BUILD-SNAPSHOT + 2.0.0.DATACMNS-1035-SNAPSHOT Spring Data Core diff --git a/readme.md b/readme.md index 6ce69386d3..3a7ae02c97 100644 --- a/readme.md +++ b/readme.md @@ -31,7 +31,7 @@ This README as well as the [reference documentation](http://docs.spring.io/sprin The main project [website](http://projects.spring.io/spring-data/) contains links to basic project information such as source code, JavaDocs, Issue tracking, etc. -For more detailed questions, please refer to [spring-data on stackoverflow](http://stackoverflow.com/questions/tagged/spring-data). If you are new to Spring as well as to Spring Data, look for information about [Spring projects](https://spring.io/projects). +For more detailed questions, please refer to [spring-data on stackoverflow](http://stackoverflow.com/questions/tagged/spring-data). If you are new to Spring as well as to Spring Data, look for information about [Spring projects](https://spring.io/projects). ## Contributing to Spring Data Commons diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java new file mode 100644 index 0000000000..d8d1cc7507 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/CustomConversions.java @@ -0,0 +1,500 @@ +/* + * Copyright 2011-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. + * 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.convert; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.core.GenericTypeResolver; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.mapping.model.SimpleTypeHolder; +import org.springframework.data.util.Optionals; +import org.springframework.data.util.Streamable; +import org.springframework.util.Assert; + +/** + * Value object to capture custom conversion. That is essentially a {@link List} of converters and some additional logic + * around them. The converters build up two sets of types which store-specific basic types can be converted into and + * from. These types will be considered simple ones (which means they neither need deeper inspection nor nested + * conversion. Thus the {@link CustomConversions} also act as factory for {@link SimpleTypeHolder} . + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.0 + */ +@Slf4j +public class CustomConversions { + + private static final String READ_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as reading converter although it doesn't convert from a store-supported type! You might wanna check you annotation setup at the converter implementation."; + private static final String WRITE_CONVERTER_NOT_SIMPLE = "Registering converter from %s to %s as writing converter although it doesn't convert to a store-supported type! You might wanna check you annotation setup at the converter implementation."; + private static final String NOT_A_CONVERTER = "Converter %s is neither a Spring Converter, GenericConverter or ConverterFactory!"; + private static final List DEFAULT_CONVERTERS; + + static { + + List defaults = new ArrayList<>(); + + defaults.addAll(JodaTimeConverters.getConvertersToRegister()); + defaults.addAll(Jsr310Converters.getConvertersToRegister()); + defaults.addAll(ThreeTenBackPortConverters.getConvertersToRegister()); + + DEFAULT_CONVERTERS = Collections.unmodifiableList(defaults); + } + + private final Set readingPairs; + private final Set writingPairs; + private final Set> customSimpleTypes; + private final SimpleTypeHolder simpleTypeHolder; + + private final List converters; + + private final Map>> customReadTargetTypes; + private final Map>> customWriteTargetTypes; + private final Map, Optional>> rawWriteTargetTypes; + + /** + * Creates a new {@link CustomConversions} instance registering the given converters. + * + * @param converters + */ + public CustomConversions(StoreConversions storeConversions, List converters) { + + Assert.notNull(converters, "List of converters must not be null!"); + + this.readingPairs = new LinkedHashSet<>(); + this.writingPairs = new LinkedHashSet<>(); + this.customSimpleTypes = new HashSet<>(); + this.customReadTargetTypes = new ConcurrentHashMap<>(); + this.customWriteTargetTypes = new ConcurrentHashMap<>(); + this.rawWriteTargetTypes = new ConcurrentHashMap<>(); + + List toRegister = new ArrayList(); + + // Add user provided converters to make sure they can override the defaults + toRegister.addAll(converters); + toRegister.addAll(storeConversions.getStoreConverters()); + toRegister.addAll(DEFAULT_CONVERTERS); + + toRegister.stream()// + .flatMap(it -> storeConversions.getRegistrationsFor(it).stream())// + .forEach(this::register); + + Collections.reverse(toRegister); + + this.converters = Collections.unmodifiableList(toRegister); + this.simpleTypeHolder = new SimpleTypeHolder(customSimpleTypes, storeConversions.getStoreTypeHolder()); + } + + /** + * Returns the underlying {@link SimpleTypeHolder}. + * + * @return + */ + public SimpleTypeHolder getSimpleTypeHolder() { + return simpleTypeHolder; + } + + /** + * Returns whether the given type is considered to be simple. That means it's either a general simple type or we have + * a writing {@link Converter} registered for a particular type. + * + * @see SimpleTypeHolder#isSimpleType(Class) + * @param type + * @return + */ + public boolean isSimpleType(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + return simpleTypeHolder.isSimpleType(type); + } + + /** + * Populates the given {@link GenericConversionService} with the converters registered. + * + * @param conversionService + */ + public void registerConvertersIn(GenericConversionService conversionService) { + + Assert.notNull(conversionService, "ConversionService must not be null!"); + + converters.forEach(it -> { + + boolean added = false; + + if (it instanceof Converter) { + conversionService.addConverter(Converter.class.cast(it)); + added = true; + } + + if (it instanceof ConverterFactory) { + conversionService.addConverterFactory(ConverterFactory.class.cast(it)); + added = true; + } + + if (it instanceof GenericConverter) { + conversionService.addConverter(GenericConverter.class.cast(it)); + added = true; + } + + if (!added) { + throw new IllegalArgumentException(String.format(NOT_A_CONVERTER, it)); + } + }); + } + + /** + * Registers the given {@link ConvertiblePair} as reading or writing pair depending on the type sides being basic + * Mongo types. + * + * @param pair + */ + private void register(ConverterRegistration converterRegistration) { + + Assert.notNull(converterRegistration, "Converter registration must not be null!"); + + ConvertiblePair pair = converterRegistration.getConvertiblePair(); + + if (converterRegistration.isReading()) { + + readingPairs.add(pair); + + if (LOG.isWarnEnabled() && !converterRegistration.isSimpleSourceType()) { + LOG.warn(String.format(READ_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType())); + } + } + + if (converterRegistration.isWriting()) { + + writingPairs.add(pair); + customSimpleTypes.add(pair.getSourceType()); + + if (LOG.isWarnEnabled() && !converterRegistration.isSimpleTargetType()) { + LOG.warn(String.format(WRITE_CONVERTER_NOT_SIMPLE, pair.getSourceType(), pair.getTargetType())); + } + } + } + + /** + * Returns the target type to convert to in case we have a custom conversion registered to convert the given source + * type into a Mongo native one. + * + * @param sourceType must not be {@literal null} + * @return + */ + public Optional> getCustomWriteTarget(Class sourceType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + + return rawWriteTargetTypes.computeIfAbsent(sourceType, + it -> getCustomTarget(sourceType, Optional.empty(), writingPairs)); + } + + /** + * Returns the target type we can readTargetWriteLocl an inject of the given source type to. The returned type might + * be a subclass of the given expected type though. If {@code expectedTargetType} is {@literal null} we will simply + * return the first target type matching or {@literal null} if no conversion can be found. + * + * @param sourceType must not be {@literal null} + * @param requestedTargetType must not be {@literal null}. + * @return + */ + public Optional> getCustomWriteTarget(Class sourceType, Class requestedTargetType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + Assert.notNull(requestedTargetType, "Target type must not be null!"); + + return customWriteTargetTypes.computeIfAbsent(new ConvertiblePair(sourceType, requestedTargetType), + it -> getCustomTarget(sourceType, Optional.of(requestedTargetType), writingPairs)); + } + + /** + * Returns whether we have a custom conversion registered to readTargetWriteLocl into a Mongo native type. The + * returned type might be a subclass of the given expected type though. + * + * @param sourceType must not be {@literal null} + * @return + */ + public boolean hasCustomWriteTarget(Class sourceType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + + return getCustomWriteTarget(sourceType).isPresent(); + } + + /** + * Returns whether we have a custom conversion registered to readTargetWriteLocl an object of the given source type + * into an object of the given Mongo native target type. + * + * @param sourceType must not be {@literal null}. + * @param targetType must not be {@literal null}. + * @return + */ + public boolean hasCustomWriteTarget(Class sourceType, Class targetType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + Assert.notNull(targetType, "Target type must not be null!"); + + return getCustomWriteTarget(sourceType, targetType).isPresent(); + } + + /** + * Returns whether we have a custom conversion registered to readTargetReadLock the given source into the given target + * type. + * + * @param sourceType must not be {@literal null} + * @param targetType must not be {@literal null} + * @return + */ + public boolean hasCustomReadTarget(Class sourceType, Class targetType) { + + Assert.notNull(sourceType, "Source type must not be null!"); + Assert.notNull(targetType, "Target type must not be null!"); + + return getCustomReadTarget(sourceType, targetType).isPresent(); + } + + /** + * Returns the actual target type for the given {@code sourceType} and {@code requestedTargetType}. Note that the + * returned {@link Class} could be an assignable type to the given {@code requestedTargetType}. + * + * @param sourceType must not be {@literal null}. + * @param targetType must not be {@literal null}. + * @return + */ + private Optional> getCustomReadTarget(Class sourceType, Class targetType) { + + return customReadTargetTypes.computeIfAbsent(new ConvertiblePair(sourceType, targetType), + it -> getCustomTarget(sourceType, Optional.of(targetType), readingPairs)); + } + + /** + * Inspects the given {@link ConvertiblePair}s for ones that have a source compatible type as source. Additionally + * checks assignability of the target type if one is given. + * + * @param sourceType must not be {@literal null}. + * @param targetType can be {@literal null}. + * @param pairs must not be {@literal null}. + * @return + */ + private static Optional> getCustomTarget(Class sourceType, Optional> targetType, + Collection pairs) { + + Assert.notNull(sourceType, "Source Class must not be null!"); + Assert.notNull(pairs, "Collection of ConvertiblePair must not be null!"); + + return Optionals.firstNonEmpty(// + () -> targetType.filter(it -> pairs.contains(new ConvertiblePair(sourceType, it))), // + () -> pairs.stream()// + .filter(it -> hasAssignableSourceType(it, sourceType)) // + .> map(ConvertiblePair::getTargetType)// + .filter(it -> requestTargetTypeIsAssignable(targetType, it))// + .findFirst()); + } + + private static boolean hasAssignableSourceType(ConvertiblePair pair, Class sourceType) { + return pair.getSourceType().isAssignableFrom(sourceType); + } + + private static boolean requestTargetTypeIsAssignable(Optional> requestedTargetType, Class targetType) { + + return !requestedTargetType.isPresent() // + ? true // + : requestedTargetType.map(it -> targetType.isAssignableFrom(it)).orElse(false); + } + + /** + * Conversion registration information. + * + * @author Oliver Gierke + * @author Mark Paluch + */ + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + private static class ConverterRegistration { + + private final @NonNull ConvertiblePair convertiblePair; + private final @NonNull StoreConversions storeConversions; + private final boolean reading; + private final boolean writing; + + /** + * Returns whether the converter shall be used for writing. + * + * @return + */ + public boolean isWriting() { + return writing == true || (!reading && isSimpleTargetType()); + } + + /** + * Returns whether the converter shall be used for reading. + * + * @return + */ + public boolean isReading() { + return reading == true || (!writing && isSimpleSourceType()); + } + + /** + * Returns the actual conversion pair. + * + * @return + */ + public ConvertiblePair getConvertiblePair() { + return convertiblePair; + } + + /** + * Returns whether the source type is a Mongo simple one. + * + * @return + */ + public boolean isSimpleSourceType() { + return storeConversions.isStoreSimpleType(convertiblePair.getSourceType()); + } + + /** + * Returns whether the target type is a Mongo simple one. + * + * @return + */ + public boolean isSimpleTargetType() { + return storeConversions.isStoreSimpleType(convertiblePair.getTargetType()); + } + } + + /** + * Value type to capture store-specific extensions to the {@link CustomConversions}. Allows to forward store specific + * default conversions and a set of types that are supposed to be considered simple. + * + * @author Oliver Gierke + */ + @Value + @Getter(AccessLevel.PACKAGE) + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public static class StoreConversions { + + public static final StoreConversions NONE = StoreConversions.of(SimpleTypeHolder.DEFAULT, Collections.emptyList()); + + SimpleTypeHolder storeTypeHolder; + Collection storeConverters; + + /** + * Creates a new {@link StoreConversions} for the given store-specific {@link SimpleTypeHolder} and the given + * converters. + * + * @param storeTypeHolder must not be {@literal null}. + * @param converters must not be {@literal null}. + * @return + */ + public static StoreConversions of(SimpleTypeHolder storeTypeHolder, Object... converters) { + + Assert.notNull(storeTypeHolder, "SimpleTypeHolder must not be null!"); + Assert.notNull(converters, "Converters must not be null!"); + + return new StoreConversions(storeTypeHolder, Arrays.asList(converters)); + } + + /** + * Creates a new {@link StoreConversions} for the given store-specific {@link SimpleTypeHolder} and the given + * converters. + * + * @param storeTypeHolder must not be {@literal null}. + * @param converters must not be {@literal null}. + * @return + */ + public static StoreConversions of(SimpleTypeHolder storeTypeHolder, Collection converters) { + + Assert.notNull(storeTypeHolder, "SimpleTypeHolder must not be null!"); + Assert.notNull(converters, "Converters must not be null!"); + + return new StoreConversions(storeTypeHolder, converters); + } + + /** + * Returns {@link ConverterRegistration}s for the given converter. + * + * @param converter must not be {@literal null}. + * @return + */ + public Streamable getRegistrationsFor(Object converter) { + + Assert.notNull(converter, "Converter must not be null!"); + + Class type = converter.getClass(); + boolean isWriting = type.isAnnotationPresent(WritingConverter.class); + boolean isReading = type.isAnnotationPresent(ReadingConverter.class); + + if (converter instanceof GenericConverter) { + + GenericConverter genericConverter = (GenericConverter) converter; + return Streamable.of(genericConverter.getConvertibleTypes()).map(it -> register(it, isReading, isWriting)); + + } else if (converter instanceof ConverterFactory) { + + return getRegistrationFor(converter, ConverterFactory.class, isReading, isWriting); + + } else if (converter instanceof Converter) { + + return getRegistrationFor(converter, Converter.class, isReading, isWriting); + + } else { + throw new IllegalArgumentException("Unsupported converter type!"); + } + } + + private Streamable getRegistrationFor(Object converter, Class type, boolean isReading, + boolean isWriting) { + + Class[] arguments = GenericTypeResolver.resolveTypeArguments(converter.getClass(), type); + return Streamable.of(register(arguments[0], arguments[1], isReading, isWriting)); + } + + private ConverterRegistration register(Class source, Class target, boolean isReading, boolean isWriting) { + return register(new ConvertiblePair(source, target), isReading, isWriting); + } + + private ConverterRegistration register(ConvertiblePair pair, boolean isReading, boolean isWriting) { + return new ConverterRegistration(pair, this, isReading, isWriting); + } + + private boolean isStoreSimpleType(Class type) { + return storeTypeHolder.isSimpleType(type); + } + } +} diff --git a/src/main/java/org/springframework/data/domain/Chunk.java b/src/main/java/org/springframework/data/domain/Chunk.java index 00267e349a..5a0c0aaa7d 100644 --- a/src/main/java/org/springframework/data/domain/Chunk.java +++ b/src/main/java/org/springframework/data/domain/Chunk.java @@ -15,6 +15,8 @@ */ package org.springframework.data.domain; +import lombok.Getter; + import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; @@ -38,7 +40,7 @@ abstract class Chunk implements Slice, Serializable { private static final long serialVersionUID = 867755909294344406L; private final List content = new ArrayList<>(); - private final Pageable pageable; + private final @Getter Pageable pageable; /** * Creates a new {@link Chunk} with the given content and the given governing {@link Pageable}. diff --git a/src/main/java/org/springframework/data/domain/Slice.java b/src/main/java/org/springframework/data/domain/Slice.java index 8ad73582e4..e642a535de 100644 --- a/src/main/java/org/springframework/data/domain/Slice.java +++ b/src/main/java/org/springframework/data/domain/Slice.java @@ -100,6 +100,16 @@ public interface Slice extends Streamable { */ boolean hasPrevious(); + /** + * Returns the {@link Pageable} that's been used to request the current {@link Slice}. + * + * @return + * @since 2.0 + */ + default Pageable getPageable() { + return PageRequest.of(getNumber(), getSize(), getSort()); + } + /** * Returns the {@link Pageable} to request the next {@link Slice}. Can be {@literal null} in case the current * {@link Slice} is already the last one. Clients should check {@link #hasNext()} before calling this method to make diff --git a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java index a962e86ec0..749b80af5b 100644 --- a/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java +++ b/src/main/java/org/springframework/data/mapping/context/AbstractMappingContext.java @@ -89,7 +89,7 @@ public abstract class AbstractMappingContext> initialEntitySet = new HashSet<>(); private boolean strict = false; - private SimpleTypeHolder simpleTypeHolder = new SimpleTypeHolder(); + private SimpleTypeHolder simpleTypeHolder = SimpleTypeHolder.DEFAULT; private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private final Lock read = lock.readLock(); diff --git a/src/main/java/org/springframework/data/mapping/model/SimpleTypeHolder.java b/src/main/java/org/springframework/data/mapping/model/SimpleTypeHolder.java index 3437d28d5d..5a27b00248 100644 --- a/src/main/java/org/springframework/data/mapping/model/SimpleTypeHolder.java +++ b/src/main/java/org/springframework/data/mapping/model/SimpleTypeHolder.java @@ -32,39 +32,43 @@ */ public class SimpleTypeHolder { - private static final Set> DEFAULTS = new HashSet<>(); - - static { - DEFAULTS.add(boolean.class); - DEFAULTS.add(boolean[].class); - DEFAULTS.add(long.class); - DEFAULTS.add(long[].class); - DEFAULTS.add(short.class); - DEFAULTS.add(short[].class); - DEFAULTS.add(int.class); - DEFAULTS.add(int[].class); - DEFAULTS.add(byte.class); - DEFAULTS.add(byte[].class); - DEFAULTS.add(float.class); - DEFAULTS.add(float[].class); - DEFAULTS.add(double.class); - DEFAULTS.add(double[].class); - DEFAULTS.add(char.class); - DEFAULTS.add(char[].class); - DEFAULTS.add(Boolean.class); - DEFAULTS.add(Long.class); - DEFAULTS.add(Short.class); - DEFAULTS.add(Integer.class); - DEFAULTS.add(Byte.class); - DEFAULTS.add(Float.class); - DEFAULTS.add(Double.class); - DEFAULTS.add(Character.class); - DEFAULTS.add(String.class); - DEFAULTS.add(Date.class); - DEFAULTS.add(Locale.class); - DEFAULTS.add(Class.class); - DEFAULTS.add(Enum.class); - } + private static final Set> DEFAULTS = new HashSet>() { + + private static final long serialVersionUID = -1738594126505221500L; + + { + add(boolean.class); + add(boolean[].class); + add(long.class); + add(long[].class); + add(short.class); + add(short[].class); + add(int.class); + add(int[].class); + add(byte.class); + add(byte[].class); + add(float.class); + add(float[].class); + add(double.class); + add(double[].class); + add(char.class); + add(char[].class); + add(Boolean.class); + add(Long.class); + add(Short.class); + add(Integer.class); + add(Byte.class); + add(Float.class); + add(Double.class); + add(Character.class); + add(String.class); + add(Date.class); + add(Locale.class); + add(Class.class); + add(Enum.class); + } + }; + public static final SimpleTypeHolder DEFAULT = new SimpleTypeHolder(); private final Set> simpleTypes; @@ -73,8 +77,7 @@ public class SimpleTypeHolder { * * @see #SimpleTypeHolder(Set, boolean) */ - @SuppressWarnings("unchecked") - public SimpleTypeHolder() { + protected SimpleTypeHolder() { this(Collections.emptySet(), true); } @@ -128,13 +131,8 @@ public boolean isSimpleType(Class type) { return true; } - for (Class clazz : simpleTypes) { - if (clazz.isAssignableFrom(type)) { - simpleTypes.add(type); - return true; - } - } - - return false; + return simpleTypes.stream()// + .filter(it -> it.isAssignableFrom(type))// + .peek(it -> simpleTypes.add(type)).findFirst().isPresent(); } } diff --git a/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java b/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java index 25c4642fe7..3ac0b833ba 100644 --- a/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java +++ b/src/main/java/org/springframework/data/web/PagedResourcesAssembler.java @@ -20,6 +20,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import org.springframework.core.MethodParameter; import org.springframework.data.domain.Page; @@ -49,7 +50,7 @@ public class PagedResourcesAssembler implements ResourceAssembler, PagedResources>> { private final HateoasPageableHandlerMethodArgumentResolver pageableResolver; - private final UriComponents baseUri; + private final Optional baseUri; private final EmbeddedWrappers wrappers = new EmbeddedWrappers(false); private boolean forceFirstAndLastRels = false; @@ -65,7 +66,7 @@ public class PagedResourcesAssembler implements ResourceAssembler, Pa public PagedResourcesAssembler(HateoasPageableHandlerMethodArgumentResolver resolver, UriComponents baseUri) { this.pageableResolver = resolver == null ? new HateoasPageableHandlerMethodArgumentResolver() : resolver; - this.baseUri = baseUri; + this.baseUri = Optional.ofNullable(baseUri); } /** @@ -87,7 +88,7 @@ public void setForceFirstAndLastRels(boolean forceFirstAndLastRels) { */ @Override public PagedResources> toResource(Page entity) { - return toResource(entity, new SimplePagedResourceAssembler<>()); + return toResource(entity, it -> new Resource<>(it)); } /** @@ -100,7 +101,7 @@ public PagedResources> toResource(Page entity) { * @return */ public PagedResources> toResource(Page page, Link selfLink) { - return toResource(page, new SimplePagedResourceAssembler<>(), selfLink); + return toResource(page, it -> new Resource<>(it), selfLink); } /** @@ -112,7 +113,7 @@ public PagedResources> toResource(Page page, Link selfLink) { * @return */ public PagedResources toResource(Page page, ResourceAssembler assembler) { - return createResource(page, assembler, null); + return createResource(page, assembler, Optional.empty()); } /** @@ -129,7 +130,20 @@ public PagedResources toResource(Page page, Re Link link) { Assert.notNull(link, "Link must not be null!"); - return createResource(page, assembler, link); + + return createResource(page, assembler, Optional.of(link)); + } + + /** + * Creates a {@link PagedResources} with an empt collection {@link EmbeddedWrapper} for the given domain type. + * + * @param page must not be {@literal null}, content must be empty. + * @param type must not be {@literal null}. + * @return + * @since 2.0 + */ + public PagedResources toEmptyResource(Page page, Class type) { + return toEmptyResource(page, type, Optional.empty()); } /** @@ -137,15 +151,20 @@ public PagedResources toResource(Page page, Re * * @param page must not be {@literal null}, content must be empty. * @param type must not be {@literal null}. - * @param link can be {@literal null}. + * @param link must not be {@literal null}. * @return * @since 1.11 */ public PagedResources toEmptyResource(Page page, Class type, Link link) { + return toEmptyResource(page, type, Optional.of(link)); + } + + private PagedResources toEmptyResource(Page page, Class type, Optional link) { Assert.notNull(page, "Page must must not be null!"); Assert.isTrue(!page.hasContent(), "Page must not have any content!"); Assert.notNull(type, "Type must not be null!"); + Assert.notNull(link, "Link must not be null!"); PageMetadata metadata = asPageMetadata(page); @@ -155,21 +174,6 @@ public PagedResources toEmptyResource(Page page, Class type, Link link) return addPaginationLinks(new PagedResources<>(embedded, metadata), page, link); } - /** - * Adds the pagination parameters for all parameters not already present in the given {@link Link}. - * - * @param link must not be {@literal null}. - * @return - * @deprecated this method will be removed in 1.11 as no Spring Data module actually calls it. Other clients calling - * it should stop doing so as {@link Link}s used for pagination shouldn't be templated in the first place. - */ - @Deprecated - public Link appendPaginationParameterTemplates(Link link) { - - Assert.notNull(link, "Link must not be null!"); - return createLink(new UriTemplate(link.getHref()), null, link.getRel()); - } - /** * Creates the {@link PagedResources} to be equipped with pagination links downstream. * @@ -189,7 +193,7 @@ protected PagedResources createPagedResource(L } private PagedResources createResource(Page page, - ResourceAssembler assembler, Link link) { + ResourceAssembler assembler, Optional link) { Assert.notNull(page, "Page must not be null!"); Assert.notNull(assembler, "ResourceAssembler must not be null!"); @@ -205,7 +209,7 @@ private PagedResources createResource(Page return addPaginationLinks(resource, page, link); } - private PagedResources addPaginationLinks(PagedResources resources, Page page, Link link) { + private PagedResources addPaginationLinks(PagedResources resources, Page page, Optional link) { UriTemplate base = getUriTemplate(link); @@ -219,7 +223,10 @@ private PagedResources addPaginationLinks(PagedResources resources, Pa resources.add(createLink(base, page.previousPageable(), Link.REL_PREVIOUS)); } - resources.add(createLink(base, null, Link.REL_SELF)); + Link selfLink = link.map(it -> it.withSelfRel())// + .orElseGet(() -> createLink(base, page.getPageable(), Link.REL_SELF)); + + resources.add(selfLink); if (page.hasNext()) { resources.add(createLink(base, page.nextPageable(), Link.REL_NEXT)); @@ -241,12 +248,8 @@ private PagedResources addPaginationLinks(PagedResources resources, Pa * * @return */ - private UriTemplate getUriTemplate(Link baseLink) { - - String href = baseLink != null ? baseLink.getHref() - : baseUri == null ? ServletUriComponentsBuilder.fromCurrentRequest().build().toString() : baseUri.toString(); - - return new UriTemplate(href); + private UriTemplate getUriTemplate(Optional baseLink) { + return new UriTemplate(baseLink.map(Link::getHref).orElseGet(this::baseUriOrCurrentRequest)); } /** @@ -289,11 +292,11 @@ private static PageMetadata asPageMetadata(Page page) { return new PageMetadata(page.getSize(), page.getNumber(), page.getTotalElements(), page.getTotalPages()); } - private static class SimplePagedResourceAssembler implements ResourceAssembler> { + private String baseUriOrCurrentRequest() { + return baseUri.map(Object::toString).orElseGet(PagedResourcesAssembler::currentRequest); + } - @Override - public Resource toResource(T entity) { - return new Resource<>(entity); - } + private static String currentRequest() { + return ServletUriComponentsBuilder.fromCurrentRequest().build().toString(); } } diff --git a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java new file mode 100644 index 0000000000..371824cb06 --- /dev/null +++ b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java @@ -0,0 +1,277 @@ +/* + * Copyright 2011-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. + * 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.convert; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThat; + +import java.text.DateFormat; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.Locale; + +import org.joda.time.DateTime; +import org.junit.Test; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.data.convert.CustomConversions.StoreConversions; +import org.threeten.bp.LocalDateTime; + +/** + * Unit tests for {@link MongoCustomConversions}. + * + * @author Oliver Gierke + * @author Christoph Strobl + * @since 2.0 + */ +public class CustomConversionsUnitTests { + + @Test // DATACMNS-1035 + public void findsBasicReadAndWriteConversions() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(FormatToStringConverter.INSTANCE, StringToFormatConverter.INSTANCE)); + + assertThat(conversions.getCustomWriteTarget(Format.class)).hasValue(String.class); + assertThat(conversions.getCustomWriteTarget(String.class)).isNotPresent(); + + assertThat(conversions.hasCustomReadTarget(String.class, Format.class)).isTrue(); + assertThat(conversions.hasCustomReadTarget(String.class, Locale.class)).isFalse(); + } + + @Test // DATACMNS-1035 + public void considersSubtypesCorrectly() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(NumberToStringConverter.INSTANCE, StringToNumberConverter.INSTANCE)); + + assertThat(conversions.getCustomWriteTarget(Long.class)).hasValue(String.class); + assertThat(conversions.hasCustomReadTarget(String.class, Long.class)).isTrue(); + } + + @Test // DATACMNS-1035 + public void populatesConversionServiceCorrectly() { + + GenericConversionService conversionService = new DefaultConversionService(); + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(StringToFormatConverter.INSTANCE)); + conversions.registerConvertersIn(conversionService); + + assertThat(conversionService.canConvert(String.class, Format.class), is(true)); + } + + @Test // DATAMONGO-259, DATACMNS-1035 + public void doesNotConsiderTypeSimpleIfOnlyReadConverterIsRegistered() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(StringToFormatConverter.INSTANCE)); + assertThat(conversions.isSimpleType(Format.class), is(false)); + } + + @Test // DATAMONGO-298, DATACMNS-1035 + public void discoversConvertersForSubtypesOfMongoTypes() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(StringToIntegerConverter.INSTANCE)); + assertThat(conversions.hasCustomReadTarget(String.class, Integer.class), is(true)); + assertThat(conversions.hasCustomWriteTarget(String.class, Integer.class), is(true)); + } + + @Test // DATAMONGO-795, DATACMNS-1035 + public void favorsCustomConverterForIndeterminedTargetType() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(DateTimeToStringConverter.INSTANCE)); + assertThat(conversions.getCustomWriteTarget(DateTime.class)).hasValue(String.class); + } + + @Test // DATAMONGO-881, DATACMNS-1035 + public void customConverterOverridesDefault() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(CustomDateTimeConverter.INSTANCE)); + GenericConversionService conversionService = new DefaultConversionService(); + conversions.registerConvertersIn(conversionService); + + assertThat(conversionService.convert(new DateTime(), Date.class)).isEqualTo(new Date(0)); + } + + @Test // DATAMONGO-1001, DATACMNS-1035 + public void shouldSelectPropertCustomWriteTargetForCglibProxiedType() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(FormatToStringConverter.INSTANCE)); + assertThat(conversions.getCustomWriteTarget(createProxyTypeFor(Format.class))).hasValue(String.class); + } + + @Test // DATAMONGO-1001, DATACMNS-1035 + public void shouldSelectPropertCustomReadTargetForCglibProxiedType() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, + Arrays.asList(CustomObjectToStringConverter.INSTANCE)); + assertThat(conversions.hasCustomReadTarget(createProxyTypeFor(Object.class), String.class)).isTrue(); + } + + @Test // DATAMONGO-1131, DATACMNS-1035 + public void registersConvertersForJsr310() { + + CustomConversions customConversions = new CustomConversions(StoreConversions.NONE, Collections.emptyList()); + + assertThat(customConversions.hasCustomWriteTarget(java.time.LocalDateTime.class)).isTrue(); + } + + @Test // DATAMONGO-1131, DATACMNS-1035 + public void registersConvertersForThreeTenBackPort() { + + CustomConversions customConversions = new CustomConversions(StoreConversions.NONE, Collections.emptyList()); + + assertThat(customConversions.hasCustomWriteTarget(LocalDateTime.class)).isTrue(); + } + + @Test // DATAMONGO-1302, DATACMNS-1035 + public void registersConverterFactoryCorrectly() { + + CustomConversions customConversions = new CustomConversions(StoreConversions.NONE, + Collections.singletonList(new FormatConverterFactory())); + + assertThat(customConversions.getCustomWriteTarget(String.class, SimpleDateFormat.class)).isPresent(); + } + + private static Class createProxyTypeFor(Class type) { + + ProxyFactory factory = new ProxyFactory(); + factory.setProxyTargetClass(true); + factory.setTargetClass(type); + + return factory.getProxy().getClass(); + } + + enum FormatToStringConverter implements Converter { + + INSTANCE; + + public String convert(Format source) { + return source.toString(); + } + } + + enum StringToFormatConverter implements Converter { + + INSTANCE; + + public Format convert(String source) { + return DateFormat.getInstance(); + } + } + + enum NumberToStringConverter implements Converter { + + INSTANCE; + + public String convert(Number source) { + return source.toString(); + } + } + + enum StringToNumberConverter implements Converter { + + INSTANCE; + + public Number convert(String source) { + return 0L; + } + } + + enum StringToIntegerConverter implements Converter { + + INSTANCE; + + public Integer convert(String source) { + return 0; + } + } + + enum DateTimeToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(DateTime source) { + return ""; + } + } + + enum CustomDateTimeConverter implements Converter { + + INSTANCE; + + @Override + public Date convert(DateTime source) { + return new Date(0); + } + } + + enum CustomObjectToStringConverter implements Converter { + + INSTANCE; + + @Override + public String convert(Object source) { + return source != null ? source.toString() : null; + } + + } + + @WritingConverter + static class FormatConverterFactory implements ConverterFactory { + + @Override + public Converter getConverter(Class targetType) { + return new StringToFormat(targetType); + } + + private static final class StringToFormat implements Converter { + + private final Class targetType; + + public StringToFormat(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(String source) { + + if (source.length() == 0) { + return null; + } + + try { + return targetType.newInstance(); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + } + } +} diff --git a/src/test/java/org/springframework/data/mapping/SimpleTypeHolderUnitTests.java b/src/test/java/org/springframework/data/mapping/SimpleTypeHolderUnitTests.java index 0c0cdaefa9..90d1ed770c 100755 --- a/src/test/java/org/springframework/data/mapping/SimpleTypeHolderUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/SimpleTypeHolderUnitTests.java @@ -44,14 +44,14 @@ public void rejectsNullOriginal() { @Test(expected = IllegalArgumentException.class) // DATACMNS-31 public void rejectsNullTypeForIsSimpleTypeCall() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; holder.isSimpleType(null); } @Test public void addsDefaultTypes() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(String.class)).isTrue(); } @@ -87,28 +87,28 @@ public void createsHolderFromAnotherOneCorrectly() { @Test public void considersObjectToBeSimpleType() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(Object.class)).isTrue(); } @Test public void considersSimpleEnumAsSimple() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(SimpleEnum.FOO.getClass())).isTrue(); } @Test public void considersComplexEnumAsSimple() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(ComplexEnum.FOO.getClass())).isTrue(); } @Test // DATACMNS-1006 public void considersJavaLangTypesSimple() { - SimpleTypeHolder holder = new SimpleTypeHolder(); + SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; assertThat(holder.isSimpleType(Type.class)).isTrue(); } diff --git a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java index 504eadb344..57014caa33 100755 --- a/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/context/AbstractMappingContextUnitTests.java @@ -48,7 +48,7 @@ */ public class AbstractMappingContextUnitTests { - final SimpleTypeHolder holder = new SimpleTypeHolder(); + final SimpleTypeHolder holder = SimpleTypeHolder.DEFAULT; SampleMappingContext context; @Before @@ -153,7 +153,9 @@ public void returnsEntityForComponentType() { PersistentEntity entity = mappingContext .getRequiredPersistentEntity(Sample.class); - assertThat(entity.getPersistentProperty("persons")).hasValueSatisfying(it -> assertThat(mappingContext.getPersistentEntity(it)).hasValueSatisfying(inner -> assertThat(inner.getType()).isEqualTo(Person.class))); + assertThat(entity.getPersistentProperty("persons")) + .hasValueSatisfying(it -> assertThat(mappingContext.getPersistentEntity(it)) + .hasValueSatisfying(inner -> assertThat(inner.getType()).isEqualTo(Person.class))); } @Test // DATACMNS-380 diff --git a/src/test/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolverUnitTests.java b/src/test/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolverUnitTests.java index b443d22a6d..65d26c4899 100755 --- a/src/test/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolverUnitTests.java +++ b/src/test/java/org/springframework/data/web/PagedResourcesAssemblerArgumentResolverUnitTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import java.lang.reflect.Method; +import java.util.Optional; import org.junit.Before; import org.junit.Rule; @@ -130,9 +131,12 @@ public java.lang.Class getDeclaringClass() { Object result = resolver.resolveArgument(methodParameter, null, null, null); assertThat(result).isInstanceOf(PagedResourcesAssembler.class); - UriComponents uriComponents = (UriComponents) ReflectionTestUtils.getField(result, "baseUri"); - assertThat(uriComponents.getPath()).isEqualTo("/foo/mapping"); + Optional uriComponents = (Optional) ReflectionTestUtils.getField(result, "baseUri"); + + assertThat(uriComponents).hasValueSatisfying(it -> { + assertThat(it.getPath()).isEqualTo("/foo/mapping"); + }); } private void assertSelectsParameter(Method method, int expectedIndex) throws Exception { diff --git a/src/test/java/org/springframework/data/web/PagedResourcesAssemblerUnitTests.java b/src/test/java/org/springframework/data/web/PagedResourcesAssemblerUnitTests.java index 9274ce45bf..b91a1349c4 100755 --- a/src/test/java/org/springframework/data/web/PagedResourcesAssemblerUnitTests.java +++ b/src/test/java/org/springframework/data/web/PagedResourcesAssemblerUnitTests.java @@ -18,7 +18,6 @@ import static org.assertj.core.api.Assertions.*; import java.net.URI; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -111,7 +110,7 @@ public void usesCustomLinkProvided() { PagedResources> resources = assembler.toResource(createPage(1), link); assertThat(resources.getLink(Link.REL_PREVIOUS).getHref()).startsWith(link.getHref()); - assertThat(resources.getLink(Link.REL_SELF)).isNotNull(); + assertThat(resources.getLink(Link.REL_SELF)).isEqualTo(link.withSelfRel()); assertThat(resources.getLink(Link.REL_NEXT).getHref()).startsWith(link.getHref()); } @@ -131,8 +130,7 @@ public void createsACanonicalLinkWithoutTemplateParameters() { PagedResources> resources = assembler.toResource(createPage(1)); - Link selfLink = resources.getLink(Link.REL_SELF); - assertThat(selfLink.getHref()).endsWith("localhost"); + assertThat(resources.getLink(Link.REL_SELF).getHref()).doesNotContain("{").doesNotContain("}"); } @Test // DATACMNS-418 @@ -170,7 +168,7 @@ public void generatedLinksShouldNotBeTemplated() { PagedResources> resources = assembler.toResource(createPage(1)); - assertThat(resources.getLink(Link.REL_SELF).getHref()).endsWith("localhost"); + assertThat(resources.getLink(Link.REL_SELF).getHref()).doesNotContain("{").doesNotContain("}"); assertThat(resources.getLink(Link.REL_NEXT).getHref()).endsWith("?page=2&size=1"); assertThat(resources.getLink(Link.REL_PREVIOUS).getHref()).endsWith("?page=0&size=1"); } @@ -178,7 +176,7 @@ public void generatedLinksShouldNotBeTemplated() { @Test // DATACMNS-699 public void generatesEmptyPagedResourceWithEmbeddedWrapper() { - PagedResources result = assembler.toEmptyResource(EMPTY_PAGE, Person.class, null); + PagedResources result = assembler.toEmptyResource(EMPTY_PAGE, Person.class); Collection content = result.getContent(); assertThat(content).hasSize(1); @@ -190,12 +188,12 @@ public void generatesEmptyPagedResourceWithEmbeddedWrapper() { @Test(expected = IllegalArgumentException.class) // DATACMNS-699 public void emptyPageCreatorRejectsPageWithContent() { - assembler.toEmptyResource(createPage(1), Person.class, null); + assembler.toEmptyResource(createPage(1), Person.class); } @Test(expected = IllegalArgumentException.class) // DATACMNS-699 public void emptyPageCreatorRejectsNullType() { - assembler.toEmptyResource(EMPTY_PAGE, null, null); + assembler.toEmptyResource(EMPTY_PAGE, null); } @Test // DATACMNS-701 @@ -246,6 +244,14 @@ public void usesCustomPagedResources() { assertThat(assembler.toResource(EMPTY_PAGE)).isInstanceOf(CustomPagedResources.class); } + @Test // DATACMNS-1042 + public void selfLinkContainsCoordinatesForCurrentPage() { + + PagedResources> resource = assembler.toResource(createPage(0)); + + assertThat(resource.getLink(Link.REL_SELF).getHref()).endsWith("?page=0&size=1"); + } + private static Page createPage(int index) { Pageable request = PageRequest.of(index, 1);