Skip to content

DATACMNS-1034 - Introduced API to easily register bidirectional, type-based converters. #209

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.0.0.BUILD-SNAPSHOT</version>
<version>2.0.0.DATACMNS-1034-SNAPSHOT</version>

<name>Spring Data Core</name>

Expand Down
157 changes: 157 additions & 0 deletions src/main/java/org/springframework/data/convert/ConverterBuilder.java
Original file line number Diff line number Diff line change
@@ -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 {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be <S, T>?

/**
* 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 {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@
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;
Expand Down Expand Up @@ -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<>();
Expand Down Expand Up @@ -149,33 +153,46 @@ public boolean isSimpleType(Class<?> type) {
*
* @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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found it easier to read (understand) multiple return's instead of setting the added state:

if (candidate instanceof Converter) {
	conversionService.addConverter(Converter.class.cast(candidate));
	return;
}

so throw new IllegalArgumentException(…) would be the last and unconditional statement in the method body. Does this make sense?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At a quick glance, yes. Not at a second. The approach used allows the instance to implement multiple interfaces and thus obtains all ConvertibleType instances that implementing multiple ones would expose. An early return would stop that process after the first step.

Not sure this is still needed as I am unsure how likely this is in the first place. I just kept it around to preserve behavioural consistency to the old CustomConversions. We could think about removing that functionality, though.


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));
}
}

/**
Expand Down Expand Up @@ -460,10 +477,15 @@ public Streamable<ConverterRegistration> getRegistrationsFor(Object converter) {
boolean isWriting = type.isAnnotationPresent(WritingConverter.class);
boolean isReading = type.isAnnotationPresent(ReadingConverter.class);

if (converter instanceof GenericConverter) {
if (converter instanceof ConverterAware) {

return Streamable.of(() -> ConverterAware.class.cast(converter).getConverters().stream()//
.flatMap(it -> getRegistrationsFor(it).stream()));

} else if (converter instanceof GenericConverter) {

GenericConverter genericConverter = (GenericConverter) converter;
return Streamable.of(genericConverter.getConvertibleTypes()).map(it -> register(it, isReading, isWriting));
return Streamable.of(GenericConverter.class.cast(converter).getConvertibleTypes())//
.map(it -> register(it, isReading, isWriting));

} else if (converter instanceof ConverterFactory) {

Expand All @@ -474,7 +496,7 @@ public Streamable<ConverterRegistration> getRegistrationsFor(Object converter) {
return getRegistrationFor(converter, Converter.class, isReading, isWriting);

} else {
throw new IllegalArgumentException("Unsupported converter type!");
throw new IllegalArgumentException(String.format("Unsupported converter type %s!", converter));
}
}

Expand Down
Loading