Skip to content
Open
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
22 changes: 22 additions & 0 deletions core/src/main/java/org/mapstruct/ValueMapping.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package org.mapstruct;

import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
Expand Down Expand Up @@ -121,4 +122,25 @@
*/
String target();

/**
* Only effective when {@code source } = {@link MappingConstants#ANY_UNMAPPED }
* or {@link MappingConstants#ANY_REMAINING }.
* <p>
* Specifies qualifier annotations to select a user-defined handler method,
* which will be invoked in the {@code default} branch of the generated {@code switch} statement
* for unmapped enum values.
*
* @return the qualifiers
* @see Qualifier
*/
Class<? extends Annotation>[] qualifiedBy() default {};

/**
* Similar to {@link #qualifiedBy()}, but used in combination with {@code @}{@link Named} in case no custom
* qualifier annotation is defined.
*
* @return the qualifiers
* @see Named
*/
String[] qualifiedByName() default {};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.internal.model;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import javax.lang.model.element.AnnotationMirror;

import org.mapstruct.ap.internal.model.common.Parameter;
import org.mapstruct.ap.internal.model.common.Type;
import org.mapstruct.ap.internal.model.source.Method;
import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods;
import org.mapstruct.ap.internal.model.source.SelectionParameters;
import org.mapstruct.ap.internal.model.source.SourceMethod;
import org.mapstruct.ap.internal.model.source.selector.MethodSelectors;
import org.mapstruct.ap.internal.model.source.selector.SelectedMethod;
import org.mapstruct.ap.internal.model.source.selector.SelectionContext;
import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.Message;

import static org.mapstruct.ap.internal.util.Collections.first;

/**
* Utility for resolving handler methods for unmapped enum branches in
* {@link org.mapstruct.ValueMapping}.
* <p>
* Specifically used for cases like:
* <pre>{@code
* @ValueMapping(
* source = MappingConstants.ANY_UNMAPPED,
* qualifiedByName = "unknownMapping"
* )}
* </pre>
* When {@code MappingConstants#ANY_UNMAPPED} is specified, this resolver finds a user-defined
* method (typically annotated with {@link org.mapstruct.Named}) to be invoked in the
* default branch of the generated switch statement.
*
* <p>
* The handler method's parameters will be filled by the source parameters of the mapping method
* where possible. If the handler method has a return value (i.e., its return type is not void),
* a return statement will be generated and no further statements will be executed in the default
* branch. If the handler method is void, it will be invoked for side effects (such as logging)
* and subsequent statements in the default branch will still be executed.
* <p>
*
* <p>
* This behavior is consistent with MapStruct's handling of {@link org.mapstruct.AfterMapping} and
* {@link org.mapstruct.BeforeMapping} lifecycle methods.
* <p>
* This allows users to insert custom logic (such as logging, exception handling, etc.)
* for unmapped enum values during mapping.
*/
public final class DefaultValueMappingMethodResolver {

private DefaultValueMappingMethodResolver() {
}

/**
* Finds a matching method for unmapped branch handling.
*
* @param method The mapping method.
* @param selectionParameters Selection parameters for method matching.
* @param ctx Builder context.
* @return The matched MethodReference, or null if none found.
*/
public static MethodReference getMatchingMethods(Method method, SelectionParameters selectionParameters,
AnnotationMirror positionHint, MappingBuilderContext ctx) {
if (selectionParameters.getQualifiers().isEmpty() && selectionParameters.getQualifyingNames().isEmpty()) {
return null;
}
List<SourceMethod> namedMethods = getAllAvailableMethods( method, ctx.getSourceModel() );
MethodSelectors selectors = new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getMessager() );
Type targetType = method.getReturnType();
List<SelectedMethod<SourceMethod>> matchingMethods = selectors.getMatchingMethods(
namedMethods,
SelectionContext.forDefaultValueMappingMethod(
method,
targetType,
selectionParameters,
ctx.getTypeFactory()
)
);
if ( matchingMethods.isEmpty() ) {
return null;
}

reportErrorWhenAmbiguous( method, matchingMethods, targetType, positionHint, ctx );

List<MethodReference> result = new ArrayList<>(matchingMethods.size());
for ( SelectedMethod<SourceMethod> candidate : matchingMethods ) {
Parameter providingParameter =
method.getContextProvidedMethods().getParameterForProvidedMethod( candidate.getMethod() );

MapperReference mapperReference = MapperReference.findMapperReference(
ctx.getMapperReferences(), candidate.getMethod() );

result.add( new MethodReference(
candidate.getMethod(),
mapperReference,
providingParameter,
candidate.getParameterBindings()
) );
}
return first( result );
}

private static <T extends Method> void reportErrorWhenAmbiguous(Method mappingMethod,
List<SelectedMethod<T>> candidates,
Type target,
AnnotationMirror positionHint,
MappingBuilderContext ctx) {
// raise an error if more than one mapping method is suitable
if ( candidates.size() <= 1 ) {
return;
}
FormattingMessager messager = ctx.getMessager();
messager.printMessage(
mappingMethod.getExecutable(),
positionHint,
Message.GENERAL_AMBIGUOUS_MAPPING_METHOD,
null,
target.describe(),
join( candidates, ctx )
);
}

private static <T extends Method> String join(List<SelectedMethod<T>> candidates, MappingBuilderContext ctx) {
int reportingLimitAmbiguous = ctx.getOptions().isVerbose() ? Integer.MAX_VALUE : 5;
String candidateStr = candidates.stream()
.limit( reportingLimitAmbiguous )
.map( m -> m.getMethod().describe() )
.collect( Collectors.joining( ", " ) );

if ( candidates.size() > reportingLimitAmbiguous ) {
candidateStr += String.format( "... and %s more", candidates.size() - reportingLimitAmbiguous );
}
return candidateStr;
}

/**
* Gets all available methods from context and source.
*
* @param method The mapping method.
* @param sourceModelMethods Source methods from the model.
* @return List of SourceMethod.
*/
private static List<SourceMethod> getAllAvailableMethods(
Method method,
List<SourceMethod> sourceModelMethods) {
ParameterProvidedMethods contextProvidedMethods = method.getContextProvidedMethods();
List<SourceMethod> allMethods = new ArrayList<>();
if ( !contextProvidedMethods.isEmpty() ) {
allMethods.addAll(
contextProvidedMethods.getAllProvidedMethodsInParameterOrder( method.getContextParameters() )
);
}
allMethods.addAll( sourceModelMethods );
return allMethods;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,31 @@ else if ( sourceType.isString() && targetType.isEnumType() ) {
annotations,
mappingEntries,
valueMappings.nullValueTarget,
valueMappings.defaultTargetValue,
getDefaultMappingEntry(),
determineUnexpectedValueMappingException(),
beforeMappingMethods,
afterMappingMethods
);
}

private MappingEntry getDefaultMappingEntry() {
MethodReference reference = null;
ValueMappingOptions defaultTargetOptions = valueMappings.defaultTarget;
if ( defaultTargetOptions != null ) {
reference = DefaultValueMappingMethodResolver.getMatchingMethods(
method,
defaultTargetOptions.getSelectionParameters(),
defaultTargetOptions.getMirror(),
ctx
);
}
return new MappingEntry(
null,
valueMappings.defaultTargetValue != null ? valueMappings.defaultTargetValue : THROW_EXCEPTION,
reference
);
}

private void initializeEnumTransformationStrategy() {
if ( !enumMapping.hasNameTransformationStrategy() ) {
enumTransformationInvoker = EnumTransformationStrategyInvoker.DEFAULT;
Expand Down Expand Up @@ -551,14 +569,14 @@ private ValueMappingMethod(Method method,
List<Annotation> annotations,
List<MappingEntry> enumMappings,
String nullTarget,
String defaultTarget,
MappingEntry defaultTarget,
Type unexpectedValueMappingException,
List<LifecycleCallbackMethodReference> beforeMappingMethods,
List<LifecycleCallbackMethodReference> afterMappingMethods) {
super( method, beforeMappingMethods, afterMappingMethods );
this.valueMappings = enumMappings;
this.nullTarget = new MappingEntry( null, nullTarget );
this.defaultTarget = new MappingEntry( null, defaultTarget != null ? defaultTarget : THROW_EXCEPTION);
this.defaultTarget = defaultTarget;
this.unexpectedValueMappingException = unexpectedValueMappingException;
this.overridden = method.overridesMethod();
this.annotations = annotations;
Expand Down Expand Up @@ -618,6 +636,7 @@ public static class MappingEntry {
private final String source;
private final String target;
private boolean targetAsException = false;
private MethodReference targetReference;

MappingEntry(String source, String target) {
this.source = source;
Expand All @@ -630,6 +649,12 @@ public static class MappingEntry {
else {
this.target = null;
}
this.targetReference = null;
}

MappingEntry(String source, String target, MethodReference targetReference) {
this( source, target );
this.targetReference = targetReference;
}

public boolean isTargetAsException() {
Expand All @@ -643,5 +668,9 @@ public String getSource() {
public String getTarget() {
return target;
}

public MethodReference getTargetReference() {
return targetReference;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.mapstruct.ap.internal.gem.ValueMappingsGem;
import org.mapstruct.ap.internal.util.FormattingMessager;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.TypeUtils;

import static org.mapstruct.ap.internal.gem.MappingConstantsGem.ANY_REMAINING;
import static org.mapstruct.ap.internal.gem.MappingConstantsGem.ANY_UNMAPPED;
Expand All @@ -29,16 +30,18 @@ public class ValueMappingOptions {

private final String source;
private final String target;
private final SelectionParameters selectionParameters;
private final AnnotationMirror mirror;
private final AnnotationValue sourceAnnotationValue;
private final AnnotationValue targetAnnotationValue;

public static void fromMappingsGem(ValueMappingsGem mappingsGem, ExecutableElement method,
FormattingMessager messager, Set<ValueMappingOptions> mappings) {
FormattingMessager messager, Set<ValueMappingOptions> mappings,
TypeUtils typeUtils) {

boolean anyFound = false;
for ( ValueMappingGem mappingGem : mappingsGem.value().get() ) {
ValueMappingOptions mapping = fromMappingGem( mappingGem );
ValueMappingOptions mapping = fromMappingGem( mappingGem, typeUtils );
if ( mapping != null ) {

if ( !mappings.contains( mapping ) ) {
Expand Down Expand Up @@ -70,10 +73,21 @@ public static void fromMappingsGem(ValueMappingsGem mappingsGem, ExecutableEleme
}
}

public static ValueMappingOptions fromMappingGem(ValueMappingGem mapping ) {

return new ValueMappingOptions( mapping.source().get(), mapping.target().get(), mapping.mirror(),
mapping.source().getAnnotationValue(), mapping.target().getAnnotationValue() );
public static ValueMappingOptions fromMappingGem(ValueMappingGem mapping, TypeUtils typeUtils) {
SelectionParameters selectionParameters = new SelectionParameters(
mapping.qualifiedBy().get(),
mapping.qualifiedByName().get(),
null,
typeUtils
);
return new ValueMappingOptions(
mapping.source().get(),
mapping.target().get(),
mapping.mirror(),
mapping.source().getAnnotationValue(),
mapping.target().getAnnotationValue(),
selectionParameters
);
}

private ValueMappingOptions(String source, String target, AnnotationMirror mirror,
Expand All @@ -83,6 +97,20 @@ private ValueMappingOptions(String source, String target, AnnotationMirror mirro
this.mirror = mirror;
this.sourceAnnotationValue = sourceAnnotationValue;
this.targetAnnotationValue = targetAnnotationValue;
this.selectionParameters = null;
}

private ValueMappingOptions(String source, String target,
AnnotationMirror mirror,
AnnotationValue sourceAnnotationValue,
AnnotationValue targetAnnotationValue,
SelectionParameters selectionParameters) {
this.source = source;
this.target = target;
this.mirror = mirror;
this.sourceAnnotationValue = sourceAnnotationValue;
this.targetAnnotationValue = targetAnnotationValue;
this.selectionParameters = selectionParameters;
}

/**
Expand Down Expand Up @@ -127,6 +155,10 @@ public ValueMappingOptions inverse() {
return result;
}

public SelectionParameters getSelectionParameters() {
return selectionParameters;
}

@Override
public int hashCode() {
int hash = 5;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,25 @@ public static SelectionContext forMappingMethods(Method mappingMethod, Type sour
);
}

public static SelectionContext forDefaultValueMappingMethod(Method mappingMethod, Type targetType,
SelectionParameters selectionParameters,
TypeFactory typeFactory) {
SelectionCriteria criteria = SelectionCriteria.forDefaultValueMappingMethod( selectionParameters );
return new SelectionContext(
null,
criteria,
mappingMethod,
targetType,
mappingMethod.getResultType(),
() -> getAvailableParameterBindingsFromMethod(
mappingMethod,
targetType,
criteria.getSourceRHS(),
typeFactory
)
);
}

public static SelectionContext forLifecycleMethods(Method mappingMethod, Type targetType,
SelectionParameters selectionParameters,
TypeFactory typeFactory) {
Expand Down
Loading