MapperIntroduction.java
/*
* Copyright 2017-2023 original 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 io.micronaut.runtime.beans;
import io.micronaut.aop.InterceptorBean;
import io.micronaut.aop.MethodInterceptor;
import io.micronaut.aop.MethodInvocationContext;
import io.micronaut.context.ApplicationContext;
import io.micronaut.context.annotation.BootstrapContextCompatible;
import io.micronaut.context.annotation.Mapper;
import io.micronaut.context.annotation.Mapper.ConflictStrategy;
import io.micronaut.context.annotation.Mapper.MergeStrategy;
import io.micronaut.context.expressions.ConfigurableExpressionEvaluationContext;
import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.core.annotation.Nullable;
import io.micronaut.core.beans.BeanIntrospection;
import io.micronaut.core.beans.BeanIntrospection.Builder;
import io.micronaut.core.beans.BeanProperty;
import io.micronaut.core.beans.exceptions.IntrospectionException;
import io.micronaut.core.convert.ArgumentConversionContext;
import io.micronaut.core.convert.ConversionContext;
import io.micronaut.core.convert.ConversionService;
import io.micronaut.core.convert.format.Format;
import io.micronaut.core.expressions.EvaluatedExpression;
import io.micronaut.core.expressions.ExpressionEvaluationContext;
import io.micronaut.core.type.Argument;
import io.micronaut.core.util.ObjectUtils;
import io.micronaut.core.util.StringUtils;
import io.micronaut.inject.ExecutableMethod;
import io.micronaut.inject.annotation.AnnotationMetadataHierarchy;
import io.micronaut.inject.annotation.EvaluatedAnnotationMetadata;
import io.micronaut.inject.annotation.MutableAnnotationMetadata;
import io.micronaut.inject.qualifiers.Qualifiers;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;
import java.util.function.Function;
/**
* Introduction advice for {@link Mapper}.
*/
@InterceptorBean(Mapper.class)
@Internal
@BootstrapContextCompatible
final class MapperIntroduction implements MethodInterceptor<Object, Object> {
private final ApplicationContext applicationContext;
private final ConversionService conversionService;
private final Map<ExecutableMethod<?, ?>, MapInvocation> cachedInvocations = new ConcurrentHashMap<>();
MapperIntroduction(ConversionService conversionService, ApplicationContext applicationContext) {
this.conversionService = conversionService;
this.applicationContext = applicationContext;
}
@Override
public int getOrder() {
return -100; // higher precedence
}
@Override
public Object intercept(MethodInvocationContext<Object, Object> context) {
if (context.hasDeclaredAnnotation(Mapper.class)) {
ExecutableMethod<Object, Object> key = context.getExecutableMethod();
MapInvocation invocation = cachedInvocations.get(key);
if (invocation == null) {
if (context.getArguments().length == 1) {
invocation = createMappingInvocation(context);
} else {
invocation = createMergingInvocation(context);
}
cachedInvocations.put(key, invocation);
}
return invocation.map(
context
);
} else {
return context.proceed();
}
}
private MapInvocation createMappingInvocation(MethodInvocationContext<Object, Object> context) {
Argument<Object> toType = context.getReturnType().asArgument();
BeanIntrospection<Object> toIntrospection = BeanIntrospection.getIntrospection(toType.getType());
// should never be empty, validated at compile time
Argument<Object> fromArgument = (Argument<Object>) context.getArguments()[0];
AnnotationMetadata annotationMetadata = context.getAnnotationMetadata();
Mapper.ConflictStrategy conflictStrategy = annotationMetadata
.enumValue(Mapper.class, "conflictStrategy", Mapper.ConflictStrategy.class)
.orElse(null);
return new DefaultMapInvocation(annotationMetadata, fromArgument, toIntrospection, 0, conflictStrategy);
}
private MapInvocation createMergingInvocation(MethodInvocationContext<Object, Object> context) {
Argument<Object> toType = context.getReturnType().asArgument();
BeanIntrospection<Object> toIntrospection = BeanIntrospection.getIntrospection(toType.getType());
AnnotationMetadata annotationMetadata = context.getAnnotationMetadata();
Mapper.ConflictStrategy conflictStrategy = annotationMetadata.enumValue(Mapper.class, "conflictStrategy", Mapper.ConflictStrategy.class)
.orElse(null);
MergeStrategy mergeStrategy = createMergeStrategy(annotationMetadata, applicationContext);
int nArguments = context.getArguments().length;
DefaultMapInvocation[] innerInvocations = new DefaultMapInvocation[nArguments];
for (int i = 0; i < nArguments; i++) {
Argument<Object> fromArgument = (Argument<Object>) context.getArguments()[i];
innerInvocations[i] = new DefaultMapInvocation(annotationMetadata, fromArgument, toIntrospection, i, conflictStrategy);
}
return callContext -> {
MergeMappingBuilder<Object> builder = new MergeMappingBuilder<>(toIntrospection.builder(), mergeStrategy);
for (int i = 0; i < innerInvocations.length; ++i) {
builder.setArgIndex(i);
innerInvocations[i].map(callContext, builder);
}
return builder.build();
};
}
private @Nullable List<Function<Object, BiConsumer<Object, MappingBuilder<Object>>>> buildRootMappers(
BeanIntrospection<Object> fromIntrospection,
Mapper.ConflictStrategy conflictStrategy,
List<AnnotationValue<Mapper.Mapping>> annotations,
boolean isMap) {
List<Function<Object, BiConsumer<Object, MappingBuilder<Object>>>> rootMappers = new ArrayList<>(5);
for (AnnotationValue<Mapper.Mapping> annotation : annotations) {
// a root mapping contains no object to bind to, so we assume we bind to the root
if (!annotation.contains(Mapper.Mapping.MEMBER_TO) && annotation.contains(Mapper.Mapping.MEMBER_FROM)) {
Map<CharSequence, Object> values = annotation.getValues();
Object from = values.get(Mapper.Mapping.MEMBER_FROM);
Object condition = values.get(Mapper.Mapping.MEMBER_CONDITION);
EvaluatedExpression evaluatedCondition = condition instanceof EvaluatedExpression ee ? ee : null;
if (from instanceof EvaluatedExpression evaluatedExpression) {
if (evaluatedCondition != null) {
rootMappers.add(expressionEvaluationContext ->
(object, builder) -> {
ExpressionEvaluationContext evaluationContext = (ExpressionEvaluationContext) expressionEvaluationContext;
if (ObjectUtils.coerceToBoolean(evaluatedCondition.evaluate(evaluationContext))) {
Object v = evaluatedExpression.evaluate(evaluationContext);
if (v != null) {
mapAllFromValue(conflictStrategy, builder, v);
}
}
}
);
} else {
rootMappers.add((expressionEvaluationContext ->
(object, builder) -> {
ExpressionEvaluationContext evaluationContext = (ExpressionEvaluationContext) expressionEvaluationContext;
Object v = evaluatedExpression.evaluate(evaluationContext);
if (v != null) {
mapAllFromValue(conflictStrategy, builder, v);
}
}
));
}
} else if (from != null) {
String propertyName = from.toString();
if (fromIntrospection != null) {
BeanProperty<Object, Object> fromProperty = fromIntrospection.getRequiredProperty(propertyName, Object.class);
rootMappers.add((expressionEvaluationContext -> (object, builder) -> {
Object result = fromProperty.get(object);
if (result != null) {
mapAllFromValue(conflictStrategy, builder, result);
}
}));
} else if (isMap) {
rootMappers.add((expressionEvaluationContext -> (object, builder) -> {
Object result = ((Map<String, Object>) object).get(propertyName);
if (result != null) {
mapAllFromValue(conflictStrategy, builder, result);
}
}));
}
}
}
}
if (rootMappers.isEmpty()) {
return null;
} else {
return Collections.unmodifiableList(rootMappers);
}
}
private Map<String, Function<Object, BiConsumer<Object, MappingBuilder<Object>>>> buildCustomMappers(
Argument<Object> methodArgument,
BeanIntrospection<Object> fromIntrospection,
BeanIntrospection<Object> toIntrospection,
Mapper.ConflictStrategy conflictStrategy,
List<AnnotationValue<Mapper.Mapping>> annotations,
boolean isMap
) {
Map<String, Function<Object, BiConsumer<Object, MappingBuilder<Object>>>> customMappers = new HashMap<>();
BeanIntrospection.Builder<Object> builderMeta = toIntrospection.builder();
@NonNull Argument<?>[] builderArguments = builderMeta.getBuilderArguments();
for (AnnotationValue<Mapper.Mapping> mapping : annotations) {
String to = mapping.stringValue(Mapper.Mapping.MEMBER_TO).orElse(null);
String format = mapping.stringValue(Mapper.Mapping.MEMBER_FORMAT).orElse(null);
if (StringUtils.isNotEmpty(to)) {
int i = builderMeta.indexOf(to);
if (i == -1) {
continue;
}
@SuppressWarnings("unchecked") Argument<Object> argument = (Argument<Object>) builderArguments[i];
ArgumentConversionContext<?> conversionContext = null;
if (format != null) {
conversionContext = ConversionContext.of(argument);
MutableAnnotationMetadata annotationMetadata = new MutableAnnotationMetadata();
annotationMetadata.addAnnotation(Format.class.getName(), Map.of(AnnotationMetadata.VALUE_MEMBER, format));
conversionContext = conversionContext.with(new AnnotationMetadataHierarchy(argument.getAnnotationMetadata(), annotationMetadata));
} else if (conflictStrategy == Mapper.ConflictStrategy.CONVERT || conflictStrategy == null) {
conversionContext = ConversionContext.of(argument);
}
Object defaultValue;
Map<CharSequence, Object> values = mapping.getValues();
if (mapping.contains(Mapper.Mapping.MEMBER_DEFAULT_VALUE)) {
defaultValue = mapping.stringValue(Mapper.Mapping.MEMBER_DEFAULT_VALUE)
.flatMap(v -> conversionService.convert(v, argument))
.orElseThrow(() -> new IllegalStateException("Invalid defaultValue [" + values.get(Mapper.Mapping.MEMBER_DEFAULT_VALUE) + "] specified to @Mapping annotation for type " + argument));
} else {
defaultValue = null;
}
Object from = values.get(Mapper.Mapping.MEMBER_FROM);
Object condition = values.get(Mapper.Mapping.MEMBER_CONDITION);
EvaluatedExpression evaluatedCondition = condition instanceof EvaluatedExpression ee ? ee : null;
ArgumentConversionContext<Object> finalConversionContext = (ArgumentConversionContext<Object>) conversionContext;
if (from instanceof EvaluatedExpression evaluatedExpression) {
if (evaluatedCondition != null) {
customMappers.put(to, (expressionEvaluationContext ->
(object, builder) -> {
ExpressionEvaluationContext evaluationContext = (ExpressionEvaluationContext) expressionEvaluationContext;
if (ObjectUtils.coerceToBoolean(evaluatedCondition.evaluate(evaluationContext))) {
Object v = evaluatedExpression.evaluate(evaluationContext);
handleValue(i, argument, defaultValue, finalConversionContext, builder, v, null, object);
} else if (defaultValue != null) {
builder.with(i, argument, defaultValue, to, object);
}
}
));
} else {
customMappers.put(to, (expressionEvaluationContext ->
(object, builder) -> {
ExpressionEvaluationContext evaluationContext = (ExpressionEvaluationContext) expressionEvaluationContext;
Object v = evaluatedExpression.evaluate(evaluationContext);
handleValue(i, argument, defaultValue, finalConversionContext, builder, v, null, object);
}
));
}
} else if (from != null) {
String propertyName = from.toString();
String methodArgumentName = null;
if (propertyName.contains(".")) {
int index = propertyName.indexOf('.');
methodArgumentName = propertyName.substring(0, index);
propertyName = propertyName.substring(index + 1);
}
String finalPropertyName = propertyName;
if (methodArgumentName == null || methodArgumentName.equals(methodArgument.getName())) {
if (fromIntrospection != null) {
BeanProperty<Object, Object> fromProperty = fromIntrospection.getRequiredProperty(propertyName, Object.class);
customMappers.put(to, (expressionEvaluationContext -> (object, builder) -> {
Object result = fromProperty.get(object);
handleValue(i, argument, defaultValue, finalConversionContext, builder, result, finalPropertyName, object);
}));
} else if (isMap) {
customMappers.put(to, (expressionEvaluationContext -> (object, builder) -> {
Object result = ((Map<String, Object>) object).get(finalPropertyName);
handleValue(i, argument, defaultValue, finalConversionContext, builder, result, finalPropertyName, object);
}));
}
}
}
}
}
return customMappers;
}
private MergeStrategy createMergeStrategy(AnnotationMetadata annotationMetadata, ApplicationContext applicationContext) {
return annotationMetadata.stringValue(Mapper.class, "mergeStrategy")
.map(name -> {
if (Mapper.MERGE_STRATEGY_NOT_NULL_OVERRIDE.equals(name)) {
return new NotNullOverrideMergeStrategy();
}
if (Mapper.MERGE_STRATEGY_ALWAYS_OVERRIDE.equals(name)) {
return new AlwaysOverrideMergeStrategy();
}
return applicationContext.getBean(Mapper.MergeStrategy.class, Qualifiers.byName(name));
})
.orElse(new NotNullOverrideMergeStrategy());
}
private void mapAllFromValue(Mapper.ConflictStrategy conflictStrategy, MappingBuilder<Object> builder, Object object) {
BeanIntrospection<Object> nestedFrom;
try {
//noinspection unchecked
nestedFrom = (BeanIntrospection<Object>) BeanIntrospection.getIntrospection(object.getClass());
} catch (IntrospectionException e) {
throw new IllegalArgumentException("Invalid @Mapping(from=..) declaration. The source property must declared @Introspected: " + e.getMessage(), e);
}
@NonNull Collection<BeanProperty<Object, Object>> propertyNames = nestedFrom.getBeanProperties();
for (BeanProperty<Object, Object> property : propertyNames) {
if (property.isWriteOnly()) {
continue;
}
int i = builder.indexOf(property.getName());
if (i > -1) {
@SuppressWarnings("unchecked")
Argument<Object> argument = (Argument<Object>) builder.getBuilderArguments()[i];
Object propertyValue = property.get(object);
if (argument.isInstance(propertyValue)) {
builder.with(i, argument, propertyValue, property.getName(), object);
} else if (conflictStrategy == Mapper.ConflictStrategy.CONVERT) {
builder.convert(i, ConversionContext.of(argument), propertyValue, conversionService, property.getName(), object);
} else {
throw new IllegalArgumentException("Cannot map invalid value [" + propertyValue + "] to type: " + argument);
}
}
}
}
private static MapStrategy buildMapStrategy(
Mapper.ConflictStrategy conflictStrategy,
Map<String, Function<Object, BiConsumer<Object, MappingBuilder<Object>>>> customMappers,
@Nullable List<Function<Object, BiConsumer<Object, MappingBuilder<Object>>>> rootMappers,
MethodInvocationContext<Object, Object> callContext
) {
MapStrategy mapStrategy = new MapStrategy(conflictStrategy);
AnnotationMetadata callAnnotationMetadata = callContext.getAnnotationMetadata();
if (callAnnotationMetadata instanceof EvaluatedAnnotationMetadata evaluatedAnnotationMetadata) {
ConfigurableExpressionEvaluationContext evaluationContext = evaluatedAnnotationMetadata.getEvaluationContext();
customMappers.forEach((name, mapperSupplier) -> mapStrategy.customMappers.put(name, mapperSupplier.apply(evaluationContext)));
if (rootMappers != null) {
for (Function<Object, BiConsumer<Object, MappingBuilder<Object>>> mapSupplier : rootMappers) {
mapStrategy.rootMappers.add(mapSupplier.apply(evaluationContext));
}
}
} else {
customMappers.forEach((name, mapperSupplier) -> mapStrategy.customMappers.put(name, mapperSupplier.apply(null)));
if (rootMappers != null) {
for (Function<Object, BiConsumer<Object, MappingBuilder<Object>>> mapSupplier : rootMappers) {
mapStrategy.rootMappers.add(mapSupplier.apply(null));
}
}
}
return mapStrategy;
}
private void handleValue(int index, Argument<Object> argument, Object defaultValue, ArgumentConversionContext<Object> conversionContext, MappingBuilder<Object> builder, Object value, String mappedPropertyName, Object owner) {
if (value == null) {
if (defaultValue != null) {
builder.with(index, argument, defaultValue, mappedPropertyName, owner);
} else {
builder.with(index, argument, null, mappedPropertyName, owner);
}
} else if (argument.isInstance(value)) {
builder.with(index, argument, value, mappedPropertyName, owner);
} else if (conversionContext != null) {
builder.convert(index, conversionContext, value, conversionService, mappedPropertyName, owner);
} else {
throw new IllegalArgumentException("Cannot map invalid value [" + value + "] to type: " + argument);
}
}
private <I, O> void processCustomMappers(I input, MapStrategy mapStrategy, MappingBuilder<O> builder) {
Map<String, BiConsumer<Object, MappingBuilder<Object>>> customMappers = mapStrategy.customMappers();
customMappers.forEach((name, func) -> {
int i = builder.indexOf(name);
if (i > -1) {
func.accept(input, (MappingBuilder<Object>) builder);
}
});
List<BiConsumer<Object, MappingBuilder<Object>>> rootMappers = mapStrategy.rootMappers();
for (BiConsumer<Object, MappingBuilder<Object>> rootMapper : rootMappers) {
rootMapper.accept(input, (MappingBuilder<Object>) builder);
}
}
@FunctionalInterface
private interface MapInvocation {
Object map(MethodInvocationContext<Object, Object> invocationContext);
}
private final class DefaultMapInvocation implements MapInvocation {
private final Map<String, Function<Object, BiConsumer<Object, MappingBuilder<Object>>>> customMapperSuppliers;
private final List<Function<Object, BiConsumer<Object, MappingBuilder<Object>>>> rootMapperSuppliers;
private final boolean isMap;
private final boolean needsCustom;
private final BeanIntrospection<Object> fromIntrospection;
private final BeanIntrospection<Object> toIntrospection;
private final int argIndex;
private final ConflictStrategy conflictStrategy;
public DefaultMapInvocation(
AnnotationMetadata annotationMetadata, Argument<Object> fromArgument,
BeanIntrospection<Object> toIntrospection, int argIndex, ConflictStrategy conflictStrategy
) {
Class<Object> fromClass = fromArgument.getType();
this.isMap = Map.class.isAssignableFrom(fromClass);
this.fromIntrospection = isMap ? null : BeanIntrospection.getIntrospection(fromClass);
this.toIntrospection = toIntrospection;
if (annotationMetadata.isPresent(Mapper.class, AnnotationMetadata.VALUE_MEMBER)) {
List<AnnotationValue<Mapper.Mapping>> annotations = annotationMetadata.getAnnotationValuesByType(Mapper.Mapping.class);
customMapperSuppliers = buildCustomMappers(fromArgument, fromIntrospection, toIntrospection, conflictStrategy, annotations, isMap);
rootMapperSuppliers = buildRootMappers(fromIntrospection, conflictStrategy, annotations, isMap);
} else {
customMapperSuppliers = null;
rootMapperSuppliers = null;
}
this.needsCustom = (customMapperSuppliers != null && !customMapperSuppliers.isEmpty())
|| (rootMapperSuppliers != null && !rootMapperSuppliers.isEmpty());
this.argIndex = argIndex;
this.conflictStrategy = conflictStrategy;
}
public void map(MethodInvocationContext<Object, Object> callContext, MappingBuilder<Object> builder) {
if (needsCustom) {
MapStrategy mapStrategy = buildMapStrategy(conflictStrategy, customMapperSuppliers, rootMapperSuppliers, callContext);
if (isMap) {
mapMap((Map<String, Object>) callContext.getParameterValues()[argIndex], mapStrategy, builder);
} else {
mapBean(callContext.getParameterValues()[argIndex], mapStrategy, fromIntrospection, builder);
}
} else if (isMap) {
mapMap((Map<String, Object>) callContext.getParameterValues()[argIndex],
MapStrategy.DEFAULT,
builder
);
} else {
mapBean(
callContext.getParameterValues()[argIndex],
MapStrategy.DEFAULT,
fromIntrospection,
builder
);
}
}
@Override
public Object map(MethodInvocationContext<Object, Object> invocationContext) {
MappingBuilder<Object> builder = new DefaultMappingBuilder<>(toIntrospection.builder());
map(invocationContext, builder);
return builder.build();
}
private <I, O> void mapBean(I input, MapStrategy mapStrategy, BeanIntrospection<I> inputIntrospection, MappingBuilder<O> builder) {
boolean isDefault = mapStrategy == MapStrategy.DEFAULT;
Mapper.ConflictStrategy conflictStrategy = mapStrategy.conflictStrategy();
@SuppressWarnings("unchecked") @NonNull Argument<Object>[] arguments = (Argument<Object>[]) builder.getBuilderArguments();
if (!isDefault) {
processCustomMappers(input, mapStrategy, builder);
}
for (BeanProperty<I, Object> beanProperty : inputIntrospection.getBeanProperties()) {
if (!beanProperty.isWriteOnly()) {
String propertyName = beanProperty.getName();
if (!isDefault && mapStrategy.customMappers().containsKey(propertyName)) {
continue;
}
int i = builder.indexOf(propertyName);
if (i > -1) {
Argument<Object> argument = arguments[i];
Object value = beanProperty.get(input);
if (value == null || argument.isInstance(value)) {
builder.with(i, argument, value, propertyName, input);
} else if (conflictStrategy == Mapper.ConflictStrategy.CONVERT) {
ArgumentConversionContext<Object> conversionContext = ConversionContext.of(argument);
builder.convert(i, conversionContext, value, conversionService, propertyName, input);
} else {
builder.with(i, argument, value, propertyName, input);
}
}
}
}
}
private <O> void mapMap(Map<String, Object> input, MapStrategy mapStrategy, MappingBuilder<O> builder) {
@NonNull Argument<Object>[] arguments = (Argument<Object>[]) builder.getBuilderArguments();
Mapper.ConflictStrategy conflictStrategy = mapStrategy.conflictStrategy();
boolean isDefault = mapStrategy == MapStrategy.DEFAULT;
if (!isDefault) {
processCustomMappers(input, mapStrategy, builder);
}
input.forEach((key, value) -> {
int i = builder.indexOf(key);
if (!isDefault && mapStrategy.customMappers().containsKey(key)) {
return;
}
if (i > -1) {
Argument<Object> argument = arguments[i];
if (conflictStrategy == Mapper.ConflictStrategy.CONVERT) {
builder.convert(i, ConversionContext.of(argument), value, conversionService, key, input);
} else {
builder.with(i, argument, value, key, input);
}
}
});
}
}
private record MapStrategy(
Mapper.ConflictStrategy conflictStrategy,
Map<String, BiConsumer<Object, MappingBuilder<Object>>> customMappers,
List<BiConsumer<Object, MappingBuilder<Object>>> rootMappers
) {
static final MapStrategy DEFAULT = new MapStrategy(Mapper.ConflictStrategy.CONVERT, Collections.emptyMap(), List.of());
private MapStrategy {
if (conflictStrategy == null) {
conflictStrategy = Mapper.ConflictStrategy.CONVERT;
}
if (customMappers == null) {
customMappers = new HashMap<>(10);
}
if (rootMappers == null) {
rootMappers = new ArrayList<>(3);
}
}
public MapStrategy(Mapper.ConflictStrategy conflictStrategy) {
this(conflictStrategy, new HashMap<>(10), new ArrayList<>(3));
}
}
private sealed interface MappingBuilder<B> permits DefaultMappingBuilder, MergeMappingBuilder {
/**
* @return the arguments to build from.
*/
@NonNull Argument<?>[] getBuilderArguments();
/**
* Get the argument index based on its name.
*
* @param name The argument name
* @return The index
*/
int indexOf(String name);
/**
* Set the argument given its index.
* The property is retrieved from the owner type.
*
* @param index The index of the argument
* @param argument The argument
* @param value The value to set it to
* @param mappedPropertyName The property name from the owner class
* @param owner The owner of the property
* @return this
* @param <A> The type of argument
*/
@NonNull <A> MappingBuilder<B> with(int index, Argument<A> argument, A value, String mappedPropertyName, Object owner);
/**
* Set the argument converting it at first.
*
* @param index The argument index
* @param of conversion context
* @param value The value to set to
* @param conversionService The conversion service
* @param mappedPropertyName The name of property from owner class
* @param owner The owner of the property
* @return this
* @param <A> The type of argument
*/
<A> MappingBuilder<B> convert(int index, ArgumentConversionContext<A> of, A value, ConversionService conversionService, String mappedPropertyName, Object owner);
/**
* Build the object.
*
* @param params The parameters of the build method
* @return The built object
*/
B build(Object... params);
}
private record DefaultMappingBuilder<B>(Builder<B> builder) implements MappingBuilder<B> {
@Override
public <A> MappingBuilder<B> with(int index, Argument<A> argument, A value, String mappedPropertyName, Object owner) {
builder.with(index, argument, value);
return this;
}
@Override
public @NonNull Argument<?>[] getBuilderArguments() {
return builder.getBuilderArguments();
}
@Override
public int indexOf(String name) {
return builder.indexOf(name);
}
@Override
public B build(Object... params) {
return builder.build(params);
}
@Override
public <A> MappingBuilder<B> convert(int index, ArgumentConversionContext<A> of, A value, ConversionService conversionService, String mappedPropertyName, Object owner) {
builder.convert(index, of, value, conversionService);
return this;
}
}
private static final class MergeMappingBuilder<B> implements MappingBuilder<B> {
private final BeanIntrospection.Builder<B> builder;
private final MergeStrategy mergeStrategy;
private final Argument<?>[] arguments;
private final Object[] params;
private int argIndex = 0;
public MergeMappingBuilder(
BeanIntrospection.Builder<B> builder,
MergeStrategy mergeStrategy
) {
this.builder = builder;
this.arguments = builder.getBuilderArguments();
this.params = new Object[arguments.length];
this.mergeStrategy = mergeStrategy;
}
public MergeMappingBuilder<B> setArgIndex(int argIndex) {
this.argIndex = argIndex;
return this;
}
@Override
public <A> MappingBuilder<B> with(int index, Argument<A> argument, A value, String mappedPropertyName, Object owner) {
if (argIndex == 0) {
params[index] = value;
} else {
params[index] = mergeStrategy.merge(params[index], value, owner, argument.getName(), mappedPropertyName);
}
return this;
}
@Override
public @NonNull Argument<?>[] getBuilderArguments() {
return arguments;
}
@Override
public int indexOf(String name) {
return builder.indexOf(name);
}
@Override
public B build(Object... builderParams) {
for (int i = 0; i < params.length; i++) {
if (params[i] == null) {
continue;
}
builder.with(i, (Argument<Object>) arguments[i], params[i]);
}
return builder.build(builderParams);
}
@Override
public <A> MappingBuilder<B> convert(int index, ArgumentConversionContext<A> conversionContext, A value, ConversionService conversionService, String mappedPropertyName, Object owner) {
Argument<A> argument = conversionContext.getArgument();
if (value != null) {
if (!argument.isInstance(value)) {
value = conversionService.convertRequired(value, conversionContext);
}
with(index, argument, value, mappedPropertyName, owner);
}
return this;
}
}
private static final class NotNullOverrideMergeStrategy implements MergeStrategy {
@Override
public @Nullable Object merge(Object currentValue, Object value, Object valueOwner, String propertyName, String mappedPropertyName) {
return value != null ? value : currentValue;
}
}
private static final class AlwaysOverrideMergeStrategy implements MergeStrategy {
@Override
public @Nullable Object merge(Object currentValue, Object value, Object valueOwner, String propertyName, String mappedPropertyName) {
return value;
}
}
}