Skip to content

Add support for property-specific converters #2566

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 9 commits into from
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.7.0-SNAPSHOT</version>
<version>2.7.0-GH-1484-SNAPSHOT</version>

<name>Spring Data Core</name>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@
package org.springframework.data.convert;

import java.lang.annotation.Annotation;
import java.util.*;
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 java.util.function.Function;
import java.util.function.Predicate;
Expand All @@ -33,6 +42,7 @@
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.PersistentProperty;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.Predicates;
import org.springframework.data.util.Streamable;
Expand Down Expand Up @@ -98,6 +108,8 @@ public class CustomConversions {
private final Function<ConvertiblePair, Class<?>> getRawWriteTarget = convertiblePair -> getCustomTarget(
convertiblePair.getSourceType(), null, writingPairs);

private @Nullable PropertyValueConversions propertyValueConversions;

/**
* @param converterConfiguration the {@link ConverterConfiguration} to apply.
* @since 2.3
Expand All @@ -120,6 +132,7 @@ public CustomConversions(ConverterConfiguration converterConfiguration) {
this.converters = Collections.unmodifiableList(registeredConverters);
this.simpleTypeHolder = new SimpleTypeHolder(customSimpleTypes,
converterConfiguration.getStoreConversions().getStoreTypeHolder());
this.propertyValueConversions = converterConfiguration.getPropertyValueConversions();
}

/**
Expand Down Expand Up @@ -172,6 +185,36 @@ public void registerConvertersIn(ConverterRegistry conversionService) {
VavrCollectionConverters.getConvertersToRegister().forEach(it -> registerConverterIn(it, conversionService));
}

/**
* Delegate check if a {@link PropertyValueConverter} for the given {@literal property} is present via
* {@link PropertyValueConversions}.
*
* @param property must not be {@literal null}.
* @return {@literal true} if a specific {@link PropertyValueConverter} is available.
* @see PropertyValueConversions#hasValueConverter(PersistentProperty)
* @since 2.7
*/
public boolean hasPropertyValueConverter(PersistentProperty<?> property) {
return propertyValueConversions != null ? propertyValueConversions.hasValueConverter(property) : false;
}

/**
* Delegate to obtain the {@link PropertyValueConverter} for the given {@literal property} from
* {@link PropertyValueConversions}.
*
* @param property must not be {@literal null}. param <A> domain specific type
* @param <B> store native type
* @param <C> conversion context type
* @return the suitable {@link PropertyValueConverter} or {@literal null} if none available.
* @see PropertyValueConversions#getValueConverter(PersistentProperty)
* @since 2.7
*/
@Nullable
public <A, B, C extends PersistentProperty<C>, D extends ValueConversionContext<C>> PropertyValueConverter<A, B, D> getPropertyValueConverter(
Copy link
Member

Choose a reason for hiding this comment

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

Let's make the getPropertyValueConverter() method non-nullable as we have hasPropertyValueConverter(…) methods to check for the presence of converters.

C property) {
return propertyValueConversions != null ? propertyValueConversions.getValueConverter(property) : null;
}

/**
* Get all converters and add origin information
*
Expand Down Expand Up @@ -293,8 +336,8 @@ private boolean isSupportedConverter(ConverterRegistrationIntent registrationInt
registrationIntent.getSourceType(), registrationIntent.getTargetType(),
registrationIntent.isReading() ? "reading" : "writing"));
} else {
logger.debug(String.format(SKIP_CONVERTER, registrationIntent.getSourceType(), registrationIntent.getTargetType(),
registrationIntent.isReading() ? "reading" : "writing",
logger.debug(String.format(SKIP_CONVERTER, registrationIntent.getSourceType(),
registrationIntent.getTargetType(), registrationIntent.isReading() ? "reading" : "writing",
registrationIntent.isReading() ? registrationIntent.getSourceType() : registrationIntent.getTargetType()));
}
}
Expand Down Expand Up @@ -877,6 +920,7 @@ protected static class ConverterConfiguration {
private final StoreConversions storeConversions;
private final List<?> userConverters;
private final Predicate<ConvertiblePair> converterRegistrationFilter;
private final PropertyValueConversions propertyValueConversions;

/**
* Create a new ConverterConfiguration holding the given {@link StoreConversions} and user defined converters.
Expand All @@ -902,9 +946,30 @@ public ConverterConfiguration(StoreConversions storeConversions, List<?> userCon
public ConverterConfiguration(StoreConversions storeConversions, List<?> userConverters,
Predicate<ConvertiblePair> converterRegistrationFilter) {

this(storeConversions, userConverters, converterRegistrationFilter, new SimplePropertyValueConversions());
}

/**
* Create a new ConverterConfiguration holding the given {@link StoreConversions} and user defined converters as
* well as a {@link Collection} of {@link ConvertiblePair} for which to skip the registration of default converters.
* <br />
* This allows store implementations to modify default converter registration based on specific needs and
* configurations. User defined converters will are never subject of filtering.
*
* @param storeConversions must not be {@literal null}.
* @param userConverters must not be {@literal null} use {@link Collections#emptyList()} instead.
* @param converterRegistrationFilter must not be {@literal null}.
* @param propertyValueConversions can be {@literal null}.
* @since 2.7
*/
public ConverterConfiguration(StoreConversions storeConversions, List<?> userConverters,
Predicate<ConvertiblePair> converterRegistrationFilter,
@Nullable PropertyValueConversions propertyValueConversions) {
Copy link
Member

Choose a reason for hiding this comment

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

Having a null-object for PropertyValueConversions would allow to avoid nullable constructor args.


this.storeConversions = storeConversions;
this.userConverters = new ArrayList<>(userConverters);
this.converterRegistrationFilter = converterRegistrationFilter;
this.propertyValueConversions = propertyValueConversions;
}

/**
Expand All @@ -927,5 +992,14 @@ List<?> getUserConverters() {
boolean shouldRegister(ConvertiblePair candidate) {
return this.converterRegistrationFilter.test(candidate);
}

/**
* @return the configured {@link PropertyValueConversions} if set, {@literal null} otherwise.
* @since 2.7
*/
@Nullable
public PropertyValueConversions getPropertyValueConversions() {
return this.propertyValueConversions;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright 2022 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
*
* https://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.function.Consumer;

import org.springframework.data.mapping.PersistentProperty;
import org.springframework.lang.Nullable;

/**
* {@link PropertyValueConversions} provides access to {@link PropertyValueConverter converters} that may only be
* applied to a specific property. Other than {@link org.springframework.core.convert.converter.Converter converters}
* registered in {@link CustomConversions}, the property based variants accept and allow returning {@literal null}
* values and provide access to a store specific {@link ValueConversionContext conversion context}.
*
* @author Christoph Strobl
* @since 2.7
* @currentBook The Desert Prince - Peter V. Brett
*/
public interface PropertyValueConversions {

/**
* Check if a {@link PropertyValueConverter} is present for the given {@literal property}.
*
* @param property must not be {@literal null}.
* @return {@literal true} if a specific {@link PropertyValueConverter} is available.
*/
default boolean hasValueConverter(PersistentProperty<?> property) {
return getValueConverter((PersistentProperty) property) != null;
}

/**
* Get the {@link PropertyValueConverter} for the given {@literal property} if present.
*
* @param property must not be {@literal null}. param <A> domain specific type
* @param <B> store native type
* @param <C> conversion context type
* @return the suitable {@link PropertyValueConverter} or {@literal null} if none available.
*/
@Nullable
<A, B, C extends PersistentProperty<C>, D extends ValueConversionContext<C>> PropertyValueConverter<A, B, D> getValueConverter(
Copy link
Member

Choose a reason for hiding this comment

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

Let's make the getPropertyValueConverter() method non-nullable as we have hasPropertyValueConverter(…) methods to check for the presence of converters.

C property);

/**
* Helper that allows to create {@link PropertyValueConversions} instance with the configured
* {@link PropertyValueConverter converters} provided via the given callback.
*/
static <P extends PersistentProperty<P>> PropertyValueConversions simple(
Copy link
Member

Choose a reason for hiding this comment

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

How about naming this method create? simple implies there could be also a complex variant.

Consumer<PropertyValueConverterRegistrar<P>> config) {

SimplePropertyValueConversions conversions = new SimplePropertyValueConversions();
PropertyValueConverterRegistrar registrar = new PropertyValueConverterRegistrar();
config.accept(registrar);
conversions.setValueConverterRegistry(registrar.buildRegistry());
return conversions;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2021 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
*
* https://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.function.BiFunction;

import org.springframework.data.mapping.PersistentProperty;
import org.springframework.lang.Nullable;

/**
* {@link PropertyValueConverter} provides a symmetric way of converting certain properties from domain to
* store-specific values.
* <p>
* A {@link PropertyValueConverter} is, other than a {@link ReadingConverter} or {@link WritingConverter}, only applied
* to special annotated fields which allows a fine-grained conversion of certain values within a specific context.
*
* @author Christoph Strobl
* @param <A> domain specific type.
* @param <B> store native type.
* @param <C> the store specific {@link ValueConversionContext conversion context}.
* @since 2.7
*/
public interface PropertyValueConverter<A, B, C extends ValueConversionContext<? extends PersistentProperty<?>>> {

/**
* Convert the given store specific value into it's domain value representation. Typically, a {@literal read}
* operation.
*
* @param value can be {@literal null}.
* @param context never {@literal null}.
* @return the converted value. Can be {@literal null}.
*/
@Nullable
A read(@Nullable B value, C context);
Copy link
Member

Choose a reason for hiding this comment

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

Didn't we want to have default methods or similar to avoid nullability for the value to read/to write?


/**
* Convert the given domain specific value into it's native store representation. Typically, a {@literal write}
* operation.
*
* @param value can be {@literal null}.
* @param context never {@literal null}.
* @return the converted value. Can be {@literal null}.
*/
@Nullable
B write(@Nullable A value, C context);

/**
* NoOp {@link PropertyValueConverter} implementation.
*
* @author Christoph Strobl
*/
@SuppressWarnings({ "rawtypes", "null" })
enum ObjectToObjectPropertyValueConverter implements PropertyValueConverter {

INSTANCE;

@Nullable
@Override
public Object read(@Nullable Object value, ValueConversionContext context) {
return value;
}

@Nullable
@Override
public Object write(@Nullable Object value, ValueConversionContext context) {
return value;
}
}

/**
* A {@link PropertyValueConverter} that delegates conversion to the given {@link BiFunction}s.
*
* @author Oliver Drotbohm
*/
class FunctionPropertyValueConverter<A, B, P extends PersistentProperty<P>>
implements PropertyValueConverter<A, B, ValueConversionContext<P>> {

private final BiFunction<A, ValueConversionContext<P>, B> writer;
private final BiFunction<B, ValueConversionContext<P>, A> reader;

public FunctionPropertyValueConverter(BiFunction<A, ValueConversionContext<P>, B> writer,
BiFunction<B, ValueConversionContext<P>, A> reader) {

this.writer = writer;
this.reader = reader;
}

@Nullable
@Override
public B write(@Nullable A value, ValueConversionContext<P> context) {
return writer.apply(value, context);
}

@Nullable
@Override
public A read(@Nullable B value, ValueConversionContext<P> context) {
return reader.apply(value, context);
}
}
}
Loading