Skip to content

Commit d4cb3bd

Browse files
tangyang9464tangyang
authored andcommitted
feat: Added qualified to @ValueMapping to support inserting user-defined methods in the switch-default branch
Signed-off-by: TangYang <tangyang9464@163.com>
1 parent fce73ae commit d4cb3bd

File tree

11 files changed

+444
-13
lines changed

11 files changed

+444
-13
lines changed

core/src/main/java/org/mapstruct/ValueMapping.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package org.mapstruct;
77

8+
import java.lang.annotation.Annotation;
89
import java.lang.annotation.ElementType;
910
import java.lang.annotation.Repeatable;
1011
import java.lang.annotation.Retention;
@@ -121,4 +122,25 @@
121122
*/
122123
String target();
123124

125+
/**
126+
* Only effective when {@code source } = {@link MappingConstants#ANY_UNMAPPED }
127+
* or {@link MappingConstants#ANY_REMAINING }.
128+
* <p>
129+
* Specifies qualifier annotations to select a user-defined handler method,
130+
* which will be invoked in the {@code default} branch of the generated {@code switch} statement
131+
* for unmapped enum values.
132+
*
133+
* @return the qualifiers
134+
* @see Qualifier
135+
*/
136+
Class<? extends Annotation>[] qualifiedBy() default {};
137+
138+
/**
139+
* Similar to {@link #qualifiedBy()}, but used in combination with {@code @}{@link Named} in case no custom
140+
* qualifier annotation is defined.
141+
*
142+
* @return the qualifiers
143+
* @see Named
144+
*/
145+
String[] qualifiedByName() default {};
124146
}
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
/*
2+
* Copyright MapStruct Authors.
3+
*
4+
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
5+
*/
6+
package org.mapstruct.ap.internal.model;
7+
8+
import java.util.ArrayList;
9+
import java.util.List;
10+
import java.util.stream.Collectors;
11+
import javax.lang.model.element.AnnotationMirror;
12+
13+
import org.mapstruct.ap.internal.model.common.Parameter;
14+
import org.mapstruct.ap.internal.model.common.Type;
15+
import org.mapstruct.ap.internal.model.source.Method;
16+
import org.mapstruct.ap.internal.model.source.ParameterProvidedMethods;
17+
import org.mapstruct.ap.internal.model.source.SelectionParameters;
18+
import org.mapstruct.ap.internal.model.source.SourceMethod;
19+
import org.mapstruct.ap.internal.model.source.selector.MethodSelectors;
20+
import org.mapstruct.ap.internal.model.source.selector.SelectedMethod;
21+
import org.mapstruct.ap.internal.model.source.selector.SelectionContext;
22+
import org.mapstruct.ap.internal.util.FormattingMessager;
23+
import org.mapstruct.ap.internal.util.Message;
24+
25+
import static org.mapstruct.ap.internal.util.Collections.first;
26+
27+
/**
28+
* Utility for resolving handler methods for unmapped enum branches in
29+
* {@link org.mapstruct.ValueMapping}.
30+
* <p>
31+
* Specifically used for cases like:
32+
* <pre>{@code
33+
* @ValueMapping(
34+
* source = MappingConstants.ANY_UNMAPPED,
35+
* qualifiedByName = "unknownMapping"
36+
* )}
37+
* </pre>
38+
* When {@code MappingConstants#ANY_UNMAPPED} is specified, this resolver finds a user-defined
39+
* method (typically annotated with {@link org.mapstruct.Named}) to be invoked in the
40+
* default branch of the generated switch statement.
41+
*
42+
* <p>
43+
* The handler method's parameters will be filled by the source parameters of the mapping method
44+
* where possible. If the handler method has a return value (i.e., its return type is not void),
45+
* a return statement will be generated and no further statements will be executed in the default
46+
* branch. If the handler method is void, it will be invoked for side effects (such as logging)
47+
* and subsequent statements in the default branch will still be executed.
48+
* <p>
49+
*
50+
* <p>
51+
* This behavior is consistent with MapStruct's handling of {@link org.mapstruct.AfterMapping} and
52+
* {@link org.mapstruct.BeforeMapping} lifecycle methods.
53+
* <p>
54+
* This allows users to insert custom logic (such as logging, exception handling, etc.)
55+
* for unmapped enum values during mapping.
56+
*/
57+
public final class DefaultValueMappingMethodResolver {
58+
59+
private DefaultValueMappingMethodResolver() {
60+
}
61+
62+
/**
63+
* Finds a matching method for unmapped branch handling.
64+
*
65+
* @param method The mapping method.
66+
* @param selectionParameters Selection parameters for method matching.
67+
* @param ctx Builder context.
68+
* @return The matched MethodReference, or null if none found.
69+
*/
70+
public static MethodReference getMatchingMethods(Method method, SelectionParameters selectionParameters,
71+
AnnotationMirror positionHint, MappingBuilderContext ctx) {
72+
if (selectionParameters.getQualifiers().isEmpty() && selectionParameters.getQualifyingNames().isEmpty()) {
73+
return null;
74+
}
75+
List<SourceMethod> namedMethods = getAllAvailableMethods( method, ctx.getSourceModel() );
76+
MethodSelectors selectors = new MethodSelectors( ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getMessager() );
77+
Type targetType = method.getReturnType();
78+
List<SelectedMethod<SourceMethod>> matchingMethods = selectors.getMatchingMethods(
79+
namedMethods,
80+
SelectionContext.forDefaultValueMappingMethod(
81+
method,
82+
targetType,
83+
selectionParameters,
84+
ctx.getTypeFactory()
85+
)
86+
);
87+
if ( matchingMethods.isEmpty() ) {
88+
return null;
89+
}
90+
91+
reportErrorWhenAmbiguous( method, matchingMethods, targetType, positionHint, ctx );
92+
93+
List<MethodReference> result = new ArrayList<>(matchingMethods.size());
94+
for ( SelectedMethod<SourceMethod> candidate : matchingMethods ) {
95+
Parameter providingParameter =
96+
method.getContextProvidedMethods().getParameterForProvidedMethod( candidate.getMethod() );
97+
98+
MapperReference mapperReference = MapperReference.findMapperReference(
99+
ctx.getMapperReferences(), candidate.getMethod() );
100+
101+
result.add( new MethodReference(
102+
candidate.getMethod(),
103+
mapperReference,
104+
providingParameter,
105+
candidate.getParameterBindings()
106+
) );
107+
}
108+
return first( result );
109+
}
110+
111+
private static <T extends Method> void reportErrorWhenAmbiguous(Method mappingMethod,
112+
List<SelectedMethod<T>> candidates,
113+
Type target,
114+
AnnotationMirror positionHint,
115+
MappingBuilderContext ctx) {
116+
// raise an error if more than one mapping method is suitable
117+
if ( candidates.size() <= 1 ) {
118+
return;
119+
}
120+
FormattingMessager messager = ctx.getMessager();
121+
messager.printMessage(
122+
mappingMethod.getExecutable(),
123+
positionHint,
124+
Message.GENERAL_AMBIGUOUS_MAPPING_METHOD,
125+
null,
126+
target.describe(),
127+
join( candidates, ctx )
128+
);
129+
}
130+
131+
private static <T extends Method> String join(List<SelectedMethod<T>> candidates, MappingBuilderContext ctx) {
132+
int reportingLimitAmbiguous = ctx.getOptions().isVerbose() ? Integer.MAX_VALUE : 5;
133+
String candidateStr = candidates.stream()
134+
.limit( reportingLimitAmbiguous )
135+
.map( m -> m.getMethod().describe() )
136+
.collect( Collectors.joining( ", " ) );
137+
138+
if ( candidates.size() > reportingLimitAmbiguous ) {
139+
candidateStr += String.format( "... and %s more", candidates.size() - reportingLimitAmbiguous );
140+
}
141+
return candidateStr;
142+
}
143+
144+
/**
145+
* Gets all available methods from context and source.
146+
*
147+
* @param method The mapping method.
148+
* @param sourceModelMethods Source methods from the model.
149+
* @return List of SourceMethod.
150+
*/
151+
private static List<SourceMethod> getAllAvailableMethods(
152+
Method method,
153+
List<SourceMethod> sourceModelMethods) {
154+
ParameterProvidedMethods contextProvidedMethods = method.getContextProvidedMethods();
155+
List<SourceMethod> allMethods = new ArrayList<>();
156+
if ( !contextProvidedMethods.isEmpty() ) {
157+
allMethods.addAll(
158+
contextProvidedMethods.getAllProvidedMethodsInParameterOrder( method.getContextParameters() )
159+
);
160+
}
161+
allMethods.addAll( sourceModelMethods );
162+
return allMethods;
163+
}
164+
}

processor/src/main/java/org/mapstruct/ap/internal/model/ValueMappingMethod.java

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -137,13 +137,31 @@ else if ( sourceType.isString() && targetType.isEnumType() ) {
137137
annotations,
138138
mappingEntries,
139139
valueMappings.nullValueTarget,
140-
valueMappings.defaultTargetValue,
140+
getDefaultMappingEntry(),
141141
determineUnexpectedValueMappingException(),
142142
beforeMappingMethods,
143143
afterMappingMethods
144144
);
145145
}
146146

147+
private MappingEntry getDefaultMappingEntry() {
148+
MethodReference reference = null;
149+
ValueMappingOptions defaultTargetOptions = valueMappings.defaultTarget;
150+
if ( defaultTargetOptions != null ) {
151+
reference = DefaultValueMappingMethodResolver.getMatchingMethods(
152+
method,
153+
defaultTargetOptions.getSelectionParameters(),
154+
defaultTargetOptions.getMirror(),
155+
ctx
156+
);
157+
}
158+
return new MappingEntry(
159+
null,
160+
valueMappings.defaultTargetValue != null ? valueMappings.defaultTargetValue : THROW_EXCEPTION,
161+
reference
162+
);
163+
}
164+
147165
private void initializeEnumTransformationStrategy() {
148166
if ( !enumMapping.hasNameTransformationStrategy() ) {
149167
enumTransformationInvoker = EnumTransformationStrategyInvoker.DEFAULT;
@@ -551,14 +569,14 @@ private ValueMappingMethod(Method method,
551569
List<Annotation> annotations,
552570
List<MappingEntry> enumMappings,
553571
String nullTarget,
554-
String defaultTarget,
572+
MappingEntry defaultTarget,
555573
Type unexpectedValueMappingException,
556574
List<LifecycleCallbackMethodReference> beforeMappingMethods,
557575
List<LifecycleCallbackMethodReference> afterMappingMethods) {
558576
super( method, beforeMappingMethods, afterMappingMethods );
559577
this.valueMappings = enumMappings;
560578
this.nullTarget = new MappingEntry( null, nullTarget );
561-
this.defaultTarget = new MappingEntry( null, defaultTarget != null ? defaultTarget : THROW_EXCEPTION);
579+
this.defaultTarget = defaultTarget;
562580
this.unexpectedValueMappingException = unexpectedValueMappingException;
563581
this.overridden = method.overridesMethod();
564582
this.annotations = annotations;
@@ -618,6 +636,7 @@ public static class MappingEntry {
618636
private final String source;
619637
private final String target;
620638
private boolean targetAsException = false;
639+
private MethodReference targetReference;
621640

622641
MappingEntry(String source, String target) {
623642
this.source = source;
@@ -630,6 +649,12 @@ public static class MappingEntry {
630649
else {
631650
this.target = null;
632651
}
652+
this.targetReference = null;
653+
}
654+
655+
MappingEntry(String source, String target, MethodReference targetReference) {
656+
this( source, target );
657+
this.targetReference = targetReference;
633658
}
634659

635660
public boolean isTargetAsException() {
@@ -643,5 +668,9 @@ public String getSource() {
643668
public String getTarget() {
644669
return target;
645670
}
671+
672+
public MethodReference getTargetReference() {
673+
return targetReference;
674+
}
646675
}
647676
}

processor/src/main/java/org/mapstruct/ap/internal/model/source/ValueMappingOptions.java

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import org.mapstruct.ap.internal.gem.ValueMappingsGem;
1616
import org.mapstruct.ap.internal.util.FormattingMessager;
1717
import org.mapstruct.ap.internal.util.Message;
18+
import org.mapstruct.ap.internal.util.TypeUtils;
1819

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

3031
private final String source;
3132
private final String target;
33+
private final SelectionParameters selectionParameters;
3234
private final AnnotationMirror mirror;
3335
private final AnnotationValue sourceAnnotationValue;
3436
private final AnnotationValue targetAnnotationValue;
3537

3638
public static void fromMappingsGem(ValueMappingsGem mappingsGem, ExecutableElement method,
37-
FormattingMessager messager, Set<ValueMappingOptions> mappings) {
39+
FormattingMessager messager, Set<ValueMappingOptions> mappings,
40+
TypeUtils typeUtils) {
3841

3942
boolean anyFound = false;
4043
for ( ValueMappingGem mappingGem : mappingsGem.value().get() ) {
41-
ValueMappingOptions mapping = fromMappingGem( mappingGem );
44+
ValueMappingOptions mapping = fromMappingGem( mappingGem, typeUtils );
4245
if ( mapping != null ) {
4346

4447
if ( !mappings.contains( mapping ) ) {
@@ -70,10 +73,21 @@ public static void fromMappingsGem(ValueMappingsGem mappingsGem, ExecutableEleme
7073
}
7174
}
7275

73-
public static ValueMappingOptions fromMappingGem(ValueMappingGem mapping ) {
74-
75-
return new ValueMappingOptions( mapping.source().get(), mapping.target().get(), mapping.mirror(),
76-
mapping.source().getAnnotationValue(), mapping.target().getAnnotationValue() );
76+
public static ValueMappingOptions fromMappingGem(ValueMappingGem mapping, TypeUtils typeUtils) {
77+
SelectionParameters selectionParameters = new SelectionParameters(
78+
mapping.qualifiedBy().get(),
79+
mapping.qualifiedByName().get(),
80+
null,
81+
typeUtils
82+
);
83+
return new ValueMappingOptions(
84+
mapping.source().get(),
85+
mapping.target().get(),
86+
mapping.mirror(),
87+
mapping.source().getAnnotationValue(),
88+
mapping.target().getAnnotationValue(),
89+
selectionParameters
90+
);
7791
}
7892

7993
private ValueMappingOptions(String source, String target, AnnotationMirror mirror,
@@ -83,6 +97,20 @@ private ValueMappingOptions(String source, String target, AnnotationMirror mirro
8397
this.mirror = mirror;
8498
this.sourceAnnotationValue = sourceAnnotationValue;
8599
this.targetAnnotationValue = targetAnnotationValue;
100+
this.selectionParameters = null;
101+
}
102+
103+
private ValueMappingOptions(String source, String target,
104+
AnnotationMirror mirror,
105+
AnnotationValue sourceAnnotationValue,
106+
AnnotationValue targetAnnotationValue,
107+
SelectionParameters selectionParameters) {
108+
this.source = source;
109+
this.target = target;
110+
this.mirror = mirror;
111+
this.sourceAnnotationValue = sourceAnnotationValue;
112+
this.targetAnnotationValue = targetAnnotationValue;
113+
this.selectionParameters = selectionParameters;
86114
}
87115

88116
/**
@@ -127,6 +155,10 @@ public ValueMappingOptions inverse() {
127155
return result;
128156
}
129157

158+
public SelectionParameters getSelectionParameters() {
159+
return selectionParameters;
160+
}
161+
130162
@Override
131163
public int hashCode() {
132164
int hash = 5;

processor/src/main/java/org/mapstruct/ap/internal/model/source/selector/SelectionContext.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,25 @@ public static SelectionContext forMappingMethods(Method mappingMethod, Type sour
105105
);
106106
}
107107

108+
public static SelectionContext forDefaultValueMappingMethod(Method mappingMethod, Type targetType,
109+
SelectionParameters selectionParameters,
110+
TypeFactory typeFactory) {
111+
SelectionCriteria criteria = SelectionCriteria.forDefaultValueMappingMethod( selectionParameters );
112+
return new SelectionContext(
113+
null,
114+
criteria,
115+
mappingMethod,
116+
targetType,
117+
mappingMethod.getResultType(),
118+
() -> getAvailableParameterBindingsFromMethod(
119+
mappingMethod,
120+
targetType,
121+
criteria.getSourceRHS(),
122+
typeFactory
123+
)
124+
);
125+
}
126+
108127
public static SelectionContext forLifecycleMethods(Method mappingMethod, Type targetType,
109128
SelectionParameters selectionParameters,
110129
TypeFactory typeFactory) {

0 commit comments

Comments
 (0)