DATACMNS-1034 - Introduced API to easily register converters using Java 8 lambdas.

Introduced ConverterBuilder which exposes API to register Spring GenericConverters for the use with a store module's CustomConversions. This allows simple registration of such converters using Java 8 lambdas.

ConverterAware converters = ConverterBuilder.reading(String.class, Long.class,
  it -> Long.valueOf(it)).andWriting(it -> Object::toString);

The setup can also be done from the reading side which would just need the method invocations inverted. The resulting ConverterAware will expose the registered converters so that they can be easily handed to a CustomConversions instance. Partial creation of either reading or writing converters is possible, too with the returned instance then only exposing one of the two converters.

CustomConversions now considers ConverterAware and treats them appropriately for ConvertiblePair registration as well as during the registration in the ConversionService. Tweaked parameter types in CustomConversions to rather accept a Collection<?> for the converters (previously List<?>). Also, registerConverterIn(…) now takes a ConverterRegistry over a GenericConversionService. Polished Javadoc and non-null assertions.

Original pull request: #209.
This commit is contained in:
Oliver Gierke
2017-04-20 10:09:10 +02:00
parent 3fb7c870fb
commit a190c082f7
5 changed files with 479 additions and 25 deletions

View File

@@ -0,0 +1,157 @@
/*
* Copyright 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 java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
import org.springframework.util.Assert;
/**
* API to easily set up {@link GenericConverter} instances using Java 8 lambdas, mostly in bidirectional fashion for
* easy registration as custom type converters of the Spring Data mapping subsystem. The registration starts either with
* the definition of a reading or writing converter that can then be completed.
*
* @author Oliver Gierke
* @since 2.0
* @see #reading(Class, Class, Function)
* @see #writing(Class, Class, Function)
* @soundtrack John Mayer - Moving On and Getting Over (The Search for Everything)
*/
public interface ConverterBuilder {
/**
* Creates a new {@link ReadingConverterBuilder} to produce a converter to read values of the given source (the store
* type) into the given target (the domain type).
*
* @param source must not be {@literal null}.
* @param target must not be {@literal null}.
* @param function must not be {@literal null}.
* @return
*/
static <S, T> ReadingConverterBuilder<S, T> reading(Class<S> source, Class<T> target,
Function<? super S, ? extends T> function) {
Assert.notNull(source, "Source type must not be null!");
Assert.notNull(target, "Target type must not be null!");
Assert.notNull(function, "Conversion function must not be null!");
return new DefaultConverterBuilder<>(new ConvertiblePair(source, target), Optional.empty(), Optional.of(function));
}
/**
* Creates a new {@link WritingConverterBuilder} to produce a converter to write values of the given source (the
* domain type) into the given target (the store type).
*
* @param source must not be {@literal null}.
* @param target must not be {@literal null}.
* @param function must not be {@literal null}.
* @return
*/
static <S, T> WritingConverterBuilder<S, T> writing(Class<S> source, Class<T> target,
Function<? super S, ? extends T> function) {
Assert.notNull(source, "Source type must not be null!");
Assert.notNull(target, "Target type must not be null!");
Assert.notNull(function, "Conversion function must not be null!");
return new DefaultConverterBuilder<>(new ConvertiblePair(target, source), Optional.of(function), Optional.empty());
}
/**
* Returns all {@link GenericConverter} instances to be registered for the current {@link ConverterBuilder}.
*
* @return
*/
Set<GenericConverter> getConverters();
/**
* Exposes a writing converter.
*
* @author Oliver Gierke
* @since 2.0
*/
interface WritingConverterAware {
/**
* Returns the writing converter already created.
*
* @return
*/
GenericConverter getWritingConverter();
}
/**
* Exposes a reading converter.
*
* @author Oliver Gierke
* @since 2.0
*/
interface ReadingConverterAware {
/**
* Returns the reading converter already created.
*
* @return
*/
GenericConverter getReadingConverter();
}
/**
* Interface to represent an intermediate setup step of {@link ConverterAware} defining a reading converter first.
*
* @author Oliver Gierke
* @since 2.0
*/
interface ReadingConverterBuilder<T, S> extends ConverterBuilder, ReadingConverterAware {
/**
* Creates a new {@link ConverterAware} by registering the given {@link Function} to add a write converter.
*
* @param function must not be {@literal null}.
* @return
*/
ConverterAware andWriting(Function<? super S, ? extends T> function);
}
/**
* Interface to represent an intermediate setup step of {@link ConverterAware} defining a writing converter first.
*
* @author Oliver Gierke
* @since 2.0
*/
interface WritingConverterBuilder<S, T> extends ConverterBuilder, WritingConverterAware {
/**
* Creates a new {@link ConverterAware} by registering the given {@link Function} to add a write converter.
*
* @param function must not be {@literal null}.
* @return
*/
ConverterAware andReading(Function<? super T, ? extends S> function);
}
/**
* A {@link ConverterBuilder} aware of both a reading and writing converter.
*
* @author Oliver Gierke
* @since 2.0
*/
interface ConverterAware extends ConverterBuilder, ReadingConverterAware, WritingConverterAware {}
}

View File

@@ -37,9 +37,11 @@ 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.ConverterRegistry;
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.convert.ConverterBuilder.ConverterAware;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.Optionals;
import org.springframework.data.util.Streamable;
@@ -90,10 +92,12 @@ public class CustomConversions {
/**
* Creates a new {@link CustomConversions} instance registering the given converters.
*
* @param converters
* @param storeConversions must not be {@literal null}.
* @param converters must not be {@literal null}.
*/
public CustomConversions(StoreConversions storeConversions, List<?> converters) {
public CustomConversions(StoreConversions storeConversions, Collection<?> converters) {
Assert.notNull(storeConversions, "StoreConversions must not be null!");
Assert.notNull(converters, "List of converters must not be null!");
this.readingPairs = new LinkedHashSet<>();
@@ -149,33 +153,46 @@ public class CustomConversions {
*
* @param conversionService
*/
public void registerConvertersIn(GenericConversionService conversionService) {
public void registerConvertersIn(ConverterRegistry conversionService) {
Assert.notNull(conversionService, "ConversionService must not be null!");
converters.forEach(it -> {
converters.forEach(it -> registerConverterIn(it, conversionService));
}
boolean added = false;
/**
* Registers the given converter in the given {@link GenericConversionService}.
*
* @param candidate must not be {@literal null}.
* @param conversionService must not be {@literal null}.
*/
private void registerConverterIn(Object candidate, ConverterRegistry conversionService) {
if (it instanceof Converter) {
conversionService.addConverter(Converter.class.cast(it));
added = true;
}
boolean added = false;
if (it instanceof ConverterFactory) {
conversionService.addConverterFactory(ConverterFactory.class.cast(it));
added = true;
}
if (candidate instanceof Converter) {
conversionService.addConverter(Converter.class.cast(candidate));
added = true;
}
if (it instanceof GenericConverter) {
conversionService.addConverter(GenericConverter.class.cast(it));
added = true;
}
if (candidate instanceof ConverterFactory) {
conversionService.addConverterFactory(ConverterFactory.class.cast(candidate));
added = true;
}
if (!added) {
throw new IllegalArgumentException(String.format(NOT_A_CONVERTER, it));
}
});
if (candidate instanceof GenericConverter) {
conversionService.addConverter(GenericConverter.class.cast(candidate));
added = true;
}
if (candidate instanceof ConverterAware) {
ConverterAware.class.cast(candidate).getConverters().forEach(it -> registerConverterIn(it, conversionService));
added = true;
}
if (!added) {
throw new IllegalArgumentException(String.format(NOT_A_CONVERTER, candidate));
}
}
/**
@@ -460,10 +477,15 @@ public class CustomConversions {
boolean isWriting = type.isAnnotationPresent(WritingConverter.class);
boolean isReading = type.isAnnotationPresent(ReadingConverter.class);
if (converter instanceof GenericConverter) {
if (converter instanceof ConverterAware) {
GenericConverter genericConverter = (GenericConverter) converter;
return Streamable.of(genericConverter.getConvertibleTypes()).map(it -> register(it, isReading, isWriting));
return Streamable.of(() -> ConverterAware.class.cast(converter).getConverters().stream()//
.flatMap(it -> getRegistrationsFor(it).stream()));
} else if (converter instanceof GenericConverter) {
return Streamable.of(GenericConverter.class.cast(converter).getConvertibleTypes())//
.map(it -> register(it, isReading, isWriting));
} else if (converter instanceof ConverterFactory) {
@@ -474,7 +496,7 @@ public class CustomConversions {
return getRegistrationFor(converter, Converter.class, isReading, isWriting);
} else {
throw new IllegalArgumentException("Unsupported converter type!");
throw new IllegalArgumentException(String.format("Unsupported converter type %s!", converter));
}
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 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.EqualsAndHashCode;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Wither;
import java.util.Collections;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
import org.springframework.data.convert.ConverterBuilder.ConverterAware;
import org.springframework.data.convert.ConverterBuilder.ReadingConverterBuilder;
import org.springframework.data.convert.ConverterBuilder.WritingConverterBuilder;
import org.springframework.data.util.Optionals;
/**
* Builder to easily set up (bi-directional) {@link Converter} instances for Spring Data type mapping using Lambdas. Use
* factory methods on {@link ConverterBuilder} to create instances of this class.
*
* @author Oliver Gierke
* @since 2.0
* @see ConverterBuilder#writing(Class, Class, Function)
* @see ConverterBuilder#reading(Class, Class, Function)
* @soundtrack John Mayer - Still Feel Like Your Man (The Search for Everything)
*/
@Wither(AccessLevel.PACKAGE)
@RequiredArgsConstructor(access = AccessLevel.PACKAGE)
class DefaultConverterBuilder<S, T>
implements ConverterAware, ReadingConverterBuilder<T, S>, WritingConverterBuilder<S, T> {
private final @NonNull ConvertiblePair convertiblePair;
private final @NonNull Optional<Function<? super S, ? extends T>> writing;
private final @NonNull Optional<Function<? super T, ? extends S>> reading;
/*
* (non-Javadoc)
* @see org.springframework.data.convert.WritingConverterBuilder#andReading(java.util.function.Function)
*/
@Override
public ConverterAware andReading(Function<? super T, ? extends S> function) {
return withReading(Optional.of(function));
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.ReadingConverterBuilder#andWriting(java.util.function.Function)
*/
@Override
public ConverterAware andWriting(Function<? super S, ? extends T> function) {
return withWriting(Optional.of(function));
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.ReadingConverterBuilder#getRequiredReadingConverter()
*/
@Override
public GenericConverter getReadingConverter() {
return getOptionalReadingConverter()
.orElseThrow(() -> new IllegalStateException("No reading converter specified!"));
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.WritingConverterBuilder#getRequiredWritingConverter()
*/
@Override
public GenericConverter getWritingConverter() {
return getOptionalWritingConverter()
.orElseThrow(() -> new IllegalStateException("No writing converter specified!"));
}
/*
* (non-Javadoc)
* @see org.springframework.data.convert.ConverterBuilder#getConverters()
*/
@Override
public Set<GenericConverter> getConverters() {
return Optionals//
.toStream(getOptionalReadingConverter(), getOptionalWritingConverter())//
.collect(Collectors.toSet());
}
private Optional<GenericConverter> getOptionalReadingConverter() {
return reading.map(it -> new ConfigurableGenericConverter.Reading<>(convertiblePair, it));
}
private Optional<GenericConverter> getOptionalWritingConverter() {
return writing.map(it -> new ConfigurableGenericConverter.Writing<>(invertedPair(), it));
}
private ConvertiblePair invertedPair() {
return new ConvertiblePair(convertiblePair.getTargetType(), convertiblePair.getSourceType());
}
@RequiredArgsConstructor
@EqualsAndHashCode
private static class ConfigurableGenericConverter<S, T> implements GenericConverter {
private final ConvertiblePair convertiblePair;
private final Function<? super S, ? extends T> function;
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.GenericConverter#convert(java.lang.Object, org.springframework.core.convert.TypeDescriptor, org.springframework.core.convert.TypeDescriptor)
*/
@Override
@SuppressWarnings("unchecked")
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return function.apply((S) source);
}
/*
* (non-Javadoc)
* @see org.springframework.core.convert.converter.GenericConverter#getConvertibleTypes()
*/
@Override
public Set<ConvertiblePair> getConvertibleTypes() {
return Collections.singleton(convertiblePair);
}
@WritingConverter
private static class Writing<S, T> extends ConfigurableGenericConverter<S, T> {
Writing(ConvertiblePair convertiblePair, Function<? super S, ? extends T> function) {
super(convertiblePair, function);
}
}
@ReadingConverter
private static class Reading<S, T> extends ConfigurableGenericConverter<S, T> {
Reading(ConvertiblePair convertiblePair, Function<? super S, ? extends T> function) {
super(convertiblePair, function);
}
}
}
}

View File

@@ -0,0 +1,89 @@
/*
* Copyright 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.*;
import org.junit.Test;
import org.mockito.internal.util.Supplier;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
import org.springframework.data.convert.ConverterBuilder.ConverterAware;
import org.springframework.data.convert.ConverterBuilder.ReadingConverterBuilder;
import org.springframework.data.convert.ConverterBuilder.WritingConverterBuilder;
/**
* Unit tests for {@link DefaultConverterBuilder}.
*
* @author Oliver Gierke
* @since 2.0
* @soundtrack John Mayer - In the Blood (The Search for Everything)
*/
public class ConverterBuilderUnitTests {
@Test // DATACMNS-1034
public void setsUpBidirectionalConvertersFromReading() {
ConverterAware builder = ConverterBuilder.reading(String.class, Long.class, it -> Long.valueOf(it))
.andWriting(Object::toString);
assertConverter(builder.getReadingConverter(), "1", 1L);
assertConverter(builder.getWritingConverter(), 1L, "1");
}
@Test // DATACMNS-1034
public void setsUpBidirectionalConvertersFromWriting() {
ConverterAware builder = ConverterBuilder.writing(Long.class, String.class, Object::toString)
.andReading(it -> Long.valueOf(it));
assertConverter(builder.getReadingConverter(), "1", 1L);
assertConverter(builder.getWritingConverter(), 1L, "1");
}
@Test // DATACMNS-1034
public void setsUpReadingConverter() {
ReadingConverterBuilder<String, Long> builder = ConverterBuilder.reading(String.class, Long.class,
string -> Long.valueOf(string));
assertConverter(builder.getReadingConverter(), "1", 1L);
assertOnlyConverter(builder, builder::getReadingConverter);
}
@Test // DATACMNS-1034
public void setsUpWritingConverter() {
WritingConverterBuilder<Long, String> builder = ConverterBuilder.writing(Long.class, String.class,
Object::toString);
assertConverter(builder.getWritingConverter(), 1L, "1");
assertOnlyConverter(builder, builder::getWritingConverter);
}
private static void assertConverter(GenericConverter converter, Object source, Object target) {
assertThat(converter.getConvertibleTypes())
.containsExactly(new ConvertiblePair(source.getClass(), target.getClass()));
assertThat(converter.convert(source, TypeDescriptor.forObject(source), TypeDescriptor.forObject(target)))
.isEqualTo(target);
}
private static void assertOnlyConverter(ConverterBuilder builder, Supplier<GenericConverter> supplier) {
assertThat(builder.getConverters()).containsExactly(supplier.get());
}
}

View File

@@ -32,8 +32,10 @@ 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.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.convert.support.GenericConversionService;
import org.springframework.data.convert.ConverterBuilder.ConverterAware;
import org.springframework.data.convert.CustomConversions.StoreConversions;
import org.threeten.bp.LocalDateTime;
@@ -158,6 +160,24 @@ public class CustomConversionsUnitTests {
assertThat(customConversions.getCustomWriteTarget(String.class, SimpleDateFormat.class)).isPresent();
}
@Test // DATACMNS-1034
public void registersConverterFromConverterAware() {
ConverterAware converters = ConverterBuilder.reading(Left.class, Right.class, left -> new Right())
.andWriting(right -> new Left());
CustomConversions conversions = new CustomConversions(StoreConversions.NONE, Collections.singletonList(converters));
assertThat(conversions.hasCustomWriteTarget(Right.class)).isTrue();
assertThat(conversions.hasCustomReadTarget(Left.class, Right.class)).isTrue();
ConfigurableConversionService conversionService = new GenericConversionService();
conversions.registerConvertersIn(conversionService);
assertThat(conversionService.canConvert(Left.class, Right.class)).isTrue();
assertThat(conversionService.canConvert(Right.class, Left.class)).isTrue();
}
private static Class<?> createProxyTypeFor(Class<?> type) {
ProxyFactory factory = new ProxyFactory();
@@ -274,4 +294,8 @@ public class CustomConversionsUnitTests {
}
}
}
static class Left {}
static class Right {}
}