Skip to content
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
1 change: 1 addition & 0 deletions copyright.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Cornelius Dirmeier - https://github.com/cornzy
David Feinblum - https://github.com/dvfeinblum
Darren Rambaud - https://github.com/xyzst
Dekel Pilli - https://github.com/dekelpilli
Desislav Petrov - https://github.com/desislav-petrov
Dilip Krishnan - https://github.com/dilipkrish
Dmytro Polovinkin - https://github.com/navpil
Eric Martineau - https://github.com/ericmartineau
Expand Down
27 changes: 25 additions & 2 deletions core/src/main/java/org/mapstruct/Mapping.java
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,31 @@
*/
String defaultExpression() default "";

/**
* A condition {@link String} based on which the specified target property is to be set.
* <p>
* An example mapping would look like this:
* <pre><code>
* &#64;Mapping(
* target = "someProp",
* condition = "conditionMethodName"
* )
* </code></pre>
* <p>
* will cause the following target property assignment to be generated:
* <p>
* {@code
* if (conditionMethodName(s)) {
* targetBean.setSomeProp( new TimeAndFormat( s.getTime(), s.getFormat() ) )}
* }
* <p>
* The condition method needs to be defined as follows:
* {@code boolean someName(SourceType source); }
*
* @return The name of the condition method to be evaluated when trting to do the given target mapping
*/
String condition() default "";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The example that you are using from the defaultExpression is not something valid for condition.

I would suggest the following:

Example:

@Mapping(
    target = "name",
    condition = "isNotBlank"
)

generates:

if ( isNotBlank( source.getName() ) ) {
    target.setName( source.getName() );
}

And the condition method should be defined as:

boolean isNotBlank(String value);

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I'm not sure that this makes it really flexible:
target="name", condition="isNotBlack" always assumes that the condition will be based on the source's corresponding field but this might not be the case.

It would be much more flexible if the condition method is of the form:
boolean someName(SourceObject type)

Let me know what do you think?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I can follow both ideas. I like having the flexibility @Desislav-Petrov proposes with getting the whole source object to do some checks on this one.
But keeping it strict to the value to which the check is assigned as @filiphr proposes makes it easier to reuse the condition methods. Sticking to the isNotBlank example: This could be used for all String source values, getting the whole source type will make it more complicated to reuse it for other source values.

One idea to support both: Default to @filiphr proposals and if there is an (not annotated) argument for the method this will be the source value.
And also support if an argument was annotated with @MappingSource the source object will be used (do we already have an annotation like this? tbh I dont remember, just can find @MappingTarget that maybe could also be supported?!)


/**
* Whether the property specified via {@link #target()} should be ignored by the generated mapping method or not.
* This can be useful when certain attributes should not be propagated from source or target or when properties in
Expand Down Expand Up @@ -409,6 +434,4 @@ NullValuePropertyMappingStrategy nullValuePropertyMappingStrategy()
*/
Class<? extends Annotation> mappingControl() default MappingControl.class;



}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
import static org.mapstruct.ap.internal.util.Message.GENERAL_CONSTRUCTOR_PROPERTIES_NOT_MATCHING_PARAMETERS;
import static org.mapstruct.ap.internal.util.Message.PROPERTYMAPPING_CANNOT_DETERMINE_SOURCE_PARAMETER_FROM_TARGET;
import static org.mapstruct.ap.internal.util.Message.PROPERTYMAPPING_CANNOT_DETERMINE_SOURCE_PROPERTY_FROM_TARGET;
import static org.mapstruct.ap.internal.util.Message.PROPERTYMAPPING_CANNOT_RESOLVE_CONDITION_METHOD;

/**
* A {@link MappingMethod} implemented by a {@link Mapper} class which maps one bean type to another, optionally
Expand Down Expand Up @@ -1147,6 +1148,7 @@ else if ( mapping.getJavaExpression() != null ) {
.defaultJavaExpression( mapping.getDefaultJavaExpression() )
.mirror( mapping.getMirror() )
.options( mapping )
.condition( mapping.getCondition() )
.build();
handledTargets.add( targetPropertyName );
unprocessedSourceParameters.remove( sourceRef.getParameter() );
Expand Down Expand Up @@ -1181,6 +1183,27 @@ else if ( mapping.getJavaExpression() != null ) {
}
}
}

//check if there's a condition and if the method exists
if ( mapping.getCondition() != null ) {

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Tried to do this check in a few different places but this one seems to be the most appropriate one. Could somebody please confirm? If so, I'll add a check for the conditional method param type.

List<SelectedMethod<SourceMethod>> matchingMethods =
ConditionMethodResolver.getConditionalMappingMethods(
method, mapping.getSelectionParameters(), mapping.getCondition(), ctx );

if ( matchingMethods.size() != 1 ) {
ctx.getMessager()
.printMessage(
method.getExecutable(),
mapping.getMirror(),
mapping.getTargetAnnotationValue(),
PROPERTYMAPPING_CANNOT_RESOLVE_CONDITION_METHOD,
mapping.getCondition(),
targetPropertyName
);
errorOccured = true;
}
}

// remaining are the mappings without a 'source' so, 'only' a date format or qualifiers
if ( propertyMapping != null ) {
propertyMappings.add( propertyMapping );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 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.SelectionCriteria;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static java.util.stream.Collectors.toList;

/**
* Factory for creating conditional mapping resolvers
*
* @author Desislav Petrov
*/
public final class ConditionMethodResolver {

private ConditionMethodResolver() {
}

public static List<SelectedMethod<SourceMethod>> getConditionalMappingMethods(
Method method,
SelectionParameters selectionParameters,
String conditionalMethodName,
MappingBuilderContext ctx) {

MethodSelectors selectors = new MethodSelectors(
ctx.getTypeUtils(), ctx.getElementUtils(), ctx.getTypeFactory(), ctx.getMessager() );

return filterByName( selectors.getMatchingMethods(
method,
getAllAvailableMethods( method, ctx.getSourceModel() ),
Arrays.asList( method.getSourceParameters().get( 0 ).getType() ),
ctx.getTypeFactory().getType( Boolean.class ),
SelectionCriteria.
forMappingMethods( selectionParameters, null, null, false ) ), conditionalMethodName );
}

private static List<SelectedMethod<SourceMethod>> filterByName(
List<SelectedMethod<SourceMethod>> source,
String conditionalMethodName) {

if ( source == null || source.isEmpty() ) {
return source;
}
else {
String[] split = conditionalMethodName.split( "\\." );
return source.stream().filter( e -> e.getMethod().getName().equals( split[split.length - 1] ) )
.collect( toList() );
}
}

private static List<SourceMethod> getAllAvailableMethods( Method method, List<SourceMethod> sourceModelMethods ) {
ParameterProvidedMethods contextProvidedMethods = method.getContextProvidedMethods();
if ( contextProvidedMethods.isEmpty() ) {
return sourceModelMethods;
}

List<SourceMethod> methodsProvidedByParams = contextProvidedMethods
.getAllProvidedMethodsInParameterOrder( method.getContextParameters() );

List<SourceMethod> availableMethods =
new ArrayList<>( methodsProvidedByParams.size() + sourceModelMethods.size() );

availableMethods.addAll( methodsProvidedByParams );
availableMethods.addAll( sourceModelMethods );

return availableMethods;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ public class PropertyMapping extends ModelElement {
private final Set<String> dependsOn;
private final Assignment defaultValueAssignment;
private final boolean constructorMapping;
private final String condition;

@SuppressWarnings("unchecked")
private static class MappingBuilderBase<T extends MappingBuilderBase<T>> extends AbstractBaseBuilder<T> {
Expand Down Expand Up @@ -148,11 +149,17 @@ public static class PropertyMappingBuilder extends MappingBuilderBase<PropertyMa
private boolean forgedNamedBased = true;
private NullValueCheckStrategyGem nvcs;
private NullValuePropertyMappingStrategyGem nvpms;
private String condition;

PropertyMappingBuilder() {
super( PropertyMappingBuilder.class );
}

public PropertyMappingBuilder condition(String condition) {
this.condition = condition;
return this;
}

public PropertyMappingBuilder sourceReference(SourceReference sourceReference) {
this.sourceReference = sourceReference;
return this;
Expand Down Expand Up @@ -283,7 +290,8 @@ else if ( targetType.isArrayType() && sourceType.isArrayType() && assignment.get
assignment,
dependsOn,
getDefaultValueAssignment( assignment ),
targetWriteAccessorType == AccessorType.PARAMETER
targetWriteAccessorType == AccessorType.PARAMETER,
condition
);
}

Expand Down Expand Up @@ -925,7 +933,8 @@ else if ( errorMessageDetails == null ) {
assignment,
dependsOn,
null,
targetWriteAccessorType == AccessorType.PARAMETER
targetWriteAccessorType == AccessorType.PARAMETER,
null
);
}

Expand Down Expand Up @@ -991,7 +1000,8 @@ public PropertyMapping build() {
assignment,
dependsOn,
null,
targetWriteAccessorType == AccessorType.PARAMETER
targetWriteAccessorType == AccessorType.PARAMETER,
null
);
}

Expand All @@ -1001,17 +1011,17 @@ public PropertyMapping build() {
private PropertyMapping(String name, String targetWriteAccessorName,
ValueProvider targetReadAccessorProvider,
Type targetType, Assignment propertyAssignment,
Set<String> dependsOn, Assignment defaultValueAssignment, boolean constructorMapping) {
Set<String> dependsOn, Assignment defaultValueAssignment, boolean constructorMapping, String condition) {
this( name, null, targetWriteAccessorName, targetReadAccessorProvider,
targetType, propertyAssignment, dependsOn, defaultValueAssignment,
constructorMapping
constructorMapping, condition
);
}

private PropertyMapping(String name, String sourceBeanName, String targetWriteAccessorName,
ValueProvider targetReadAccessorProvider, Type targetType,
Assignment assignment,
Set<String> dependsOn, Assignment defaultValueAssignment, boolean constructorMapping) {
Set<String> dependsOn, Assignment defaultValueAssignment, boolean constructorMapping, String condition) {
this.name = name;
this.sourceBeanName = sourceBeanName;
this.targetWriteAccessorName = targetWriteAccessorName;
Expand All @@ -1022,6 +1032,7 @@ private PropertyMapping(String name, String sourceBeanName, String targetWriteAc
this.dependsOn = dependsOn != null ? dependsOn : Collections.<String>emptySet();
this.defaultValueAssignment = defaultValueAssignment;
this.constructorMapping = constructorMapping;
this.condition = condition;
}

/**
Expand All @@ -1031,6 +1042,10 @@ public String getName() {
return name;
}

public String getCondition() {
return condition;
}

public String getSourceBeanName() {
return sourceBeanName;
}
Expand Down Expand Up @@ -1114,7 +1129,8 @@ public String toString() {
+ "\n targetType=" + targetType + ","
+ "\n propertyAssignment=" + assignment + ","
+ "\n defaultValueAssignment=" + defaultValueAssignment + ","
+ "\n dependsOn=" + dependsOn
+ "\n dependsOn=" + dependsOn + ","
+ "\n condition=" + condition
+ "\n}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class MappingOptions extends DelegatingOptions {
private final String javaExpression;
private final String defaultJavaExpression;
private final String targetName;
private final String condition;
private final String defaultValue;
private final FormattingParameters formattingParameters;
private final SelectionParameters selectionParameters;
Expand Down Expand Up @@ -117,10 +118,10 @@ public static void addInstance(MappingGem mapping, ExecutableElement method,
String dateFormat = mapping.dateFormat().getValue();
String numberFormat = mapping.numberFormat().getValue();
String defaultValue = mapping.defaultValue().getValue();
String condition = mapping.condition().getValue();

Set<String> dependsOn = mapping.dependsOn().hasValue() ?
new LinkedHashSet( mapping.dependsOn().getValue() ) :
Collections.emptySet();
new LinkedHashSet<>(mapping.dependsOn().getValue()) : Collections.emptySet();

FormattingParameters formattingParam = new FormattingParameters(
dateFormat,
Expand Down Expand Up @@ -152,8 +153,8 @@ public static void addInstance(MappingGem mapping, ExecutableElement method,
dependsOn,
mapping,
null,
beanMappingOptions
);
beanMappingOptions,
condition);

if ( mappings.contains( options ) ) {
messager.printMessage( method, Message.PROPERTYMAPPING_DUPLICATE_TARGETS, mapping.target().get() );
Expand All @@ -180,8 +181,8 @@ public static MappingOptions forIgnore(String targetName) {
Collections.emptySet(),
null,
null,
null
);
null,
null);
}

private static boolean isConsistent(MappingGem gem, ExecutableElement method,
Expand Down Expand Up @@ -268,8 +269,8 @@ private MappingOptions(String targetName,
Set<String> dependsOn,
MappingGem mapping,
InheritContext inheritContext,
DelegatingOptions next
) {
DelegatingOptions next,
String condition) {
super( next );
this.targetName = targetName;
this.element = element;
Expand All @@ -286,6 +287,7 @@ private MappingOptions(String targetName,
this.dependsOn = dependsOn;
this.mapping = mapping;
this.inheritContext = inheritContext;
this.condition = condition;
}

private static String getExpression(MappingGem mapping, ExecutableElement element,
Expand Down Expand Up @@ -348,6 +350,10 @@ public String getSourceName() {
return sourceName;
}

public String getCondition() {
return condition;
}

public AnnotationValue getSourceAnnotationValue() {
return sourceAnnotationValue;
}
Expand Down Expand Up @@ -459,8 +465,8 @@ public MappingOptions copyForInverseInheritance(SourceMethod templateMethod,
Collections.emptySet(),
mapping,
new InheritContext( true, false, templateMethod ),
beanMappingOptions
);
beanMappingOptions,
condition);
return mappingOptions;

}
Expand Down Expand Up @@ -488,8 +494,8 @@ public MappingOptions copyForForwardInheritance(SourceMethod templateMethod,
dependsOn,
mapping,
new InheritContext( false, true, templateMethod ),
beanMappingOptions
);
beanMappingOptions,
condition);
return mappingOptions;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,14 +392,14 @@ else if ( method.isRemovedEnumMapping() ) {
else if ( method.isStreamMapping() ) {
this.messager.note( 1, Message.STREAMMAPPING_CREATE_NOTE, method );
StreamMappingMethod streamMappingMethod = createWithElementMappingMethod(
method,
mappingOptions,
new StreamMappingMethod.Builder()
method,
mappingOptions,
new StreamMappingMethod.Builder()
);

// If we do StreamMapping that means that internally there is a way to generate the result type
hasFactoryMethod =
streamMappingMethod.getFactoryMethod() != null || method.getResultType().isStreamType();
streamMappingMethod.getFactoryMethod() != null || method.getResultType().isStreamType();
mappingMethods.add( streamMappingMethod );
}
else {
Expand Down
Loading