Skip to content
Merged
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 core/src/main/java/org/mapstruct/Qualifier.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
* <li>{@link IterableMapping#qualifiedBy() }</li>
* <li>{@link MapMapping#keyQualifiedBy() }</li>
* <li>{@link MapMapping#valueQualifiedBy() }</li>
* <li>{@link SubclassMapping#qualifiedBy() }</li>
* </ul>
* <p><strong>Example:</strong></p>
* <pre><code class='java'>
Expand Down
26 changes: 26 additions & 0 deletions core/src/main/java/org/mapstruct/SubclassMapping.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 @@ -81,4 +82,29 @@
* @return the target subclass to map the source to.
*/
Class<?> target();

/**
* A qualifier can be specified to aid the selection process of a suitable mapper. This is useful in case multiple
* mapping methods (hand written or generated) qualify and thus would result in an 'Ambiguous mapping methods found'
* error. A qualifier is a custom annotation and can be placed on a hand written mapper class or a method.
*
* @return the qualifiers
* @see Qualifier
*/
Class<? extends Annotation>[] qualifiedBy() default {};

/**
* String-based form of qualifiers; When looking for a suitable mapping method for a given property, MapStruct will
* only consider those methods carrying directly or indirectly (i.e. on the class-level) a {@link Named} annotation
* for each of the specified qualifier names.
* <p>
* Note that annotation-based qualifiers are generally preferable as they allow more easily to find references and
* are safe for refactorings, but name-based qualifiers can be a less verbose alternative when requiring a large
* number of qualifiers as no custom annotation types are needed.
*
* @return One or more qualifier name(s)
* @see #qualifiedBy()
* @see Named
*/
String[] qualifiedByName() default {};
}
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ In the case that the `Fruit` is an abstract class or an interface, you would get
To allow mappings for abstract classes or interfaces you need to set the `subclassExhaustiveStrategy` to `RUNTIME_EXCEPTION`, you can do this at the `@MapperConfig`, `@Mapper` or `@BeanMapping` annotations. If you then pass a `GrapeDto` an `IllegalArgumentException` will be thrown because it is unknown how to map a `GrapeDto`.
Adding the missing (`@SubclassMapping`) for it will fix that.

<<selection-based-on-qualifiers>> can be used to further control which methods may be chosen to map a specific subclass. For that, you will need to use one of `SubclassMapping#qualifiedByName` or `SubclassMapping#qualifiedBy`.

[TIP]
====
If the mapping method for the subclasses does not exist it will be created and any other annotations on the fruit mapping method will be inherited by the newly generated mappings.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -399,13 +399,10 @@ private SubclassMapping createSubclassMapping(SubclassMappingOptions subclassMap
"SubclassMapping for " + sourceType.getFullyQualifiedName() );
SelectionCriteria criteria =
SelectionCriteria
.forSubclassMappingMethods(
new SelectionParameters(
Collections.emptyList(),
Collections.emptyList(),
subclassMappingOptions.getTarget(),
ctx.getTypeUtils() ).withSourceRHS( rightHandSide ),
subclassMappingOptions.getMappingControl( ctx.getElementUtils() ) );
.forSubclassMappingMethods(
subclassMappingOptions.getSelectionParameters().withSourceRHS( rightHandSide ),
subclassMappingOptions.getMappingControl( ctx.getElementUtils() )
);
Assignment assignment = ctx
.getMappingResolver()
.getTargetAssignment(
Expand All @@ -424,10 +421,13 @@ private SubclassMapping createSubclassMapping(SubclassMappingOptions subclassMap
String sourceArgument = null;
for ( Parameter parameter : method.getSourceParameters() ) {
if ( ctx
.getTypeUtils()
.isAssignable( sourceType.getTypeMirror(), parameter.getType().getTypeMirror() ) ) {
.getTypeUtils()
.isAssignable( sourceType.getTypeMirror(), parameter.getType().getTypeMirror() ) ) {
sourceArgument = parameter.getName();
assignment.setSourceLocalVarName( "(" + sourceType.createReferenceName() + ") " + sourceArgument );
if ( assignment != null ) {
assignment.setSourceLocalVarName(
"(" + sourceType.createReferenceName() + ") " + sourceArgument );
}
}
}
return new SubclassMapping( sourceType, sourceArgument, targetType, assignment );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,15 @@ public class SubclassMappingOptions extends DelegatingOptions {
private final TypeMirror source;
private final TypeMirror target;
private final TypeUtils typeUtils;
private final SelectionParameters selectionParameters;

public SubclassMappingOptions(TypeMirror source, TypeMirror target, TypeUtils typeUtils, DelegatingOptions next) {
public SubclassMappingOptions(TypeMirror source, TypeMirror target, TypeUtils typeUtils, DelegatingOptions next,
SelectionParameters selectionParameters) {
super( next );
this.source = source;
this.target = target;
this.typeUtils = typeUtils;
this.selectionParameters = selectionParameters;
}

@Override
Expand Down Expand Up @@ -117,6 +120,10 @@ public TypeMirror getTarget() {
return target;
}

public SelectionParameters getSelectionParameters() {
return selectionParameters;
}

public static void addInstances(SubclassMappingsGem gem, ExecutableElement method,
BeanMappingOptions beanMappingOptions, FormattingMessager messager,
TypeUtils typeUtils, Set<SubclassMappingOptions> mappings,
Expand Down Expand Up @@ -154,27 +161,37 @@ public static void addInstance(SubclassMappingGem subclassMapping, ExecutableEle

TypeMirror sourceSubclass = subclassMapping.source().getValue();
TypeMirror targetSubclass = subclassMapping.target().getValue();
SelectionParameters selectionParameters = new SelectionParameters(
subclassMapping.qualifiedBy().get(),
subclassMapping.qualifiedByName().get(),
targetSubclass,
typeUtils
);

mappings
.add(
new SubclassMappingOptions(
sourceSubclass,
targetSubclass,
typeUtils,
beanMappingOptions ) );
beanMappingOptions,
selectionParameters
) );
}

public static List<SubclassMappingOptions> copyForInverseInheritance(Set<SubclassMappingOptions> subclassMappings,
BeanMappingOptions beanMappingOptions) {
BeanMappingOptions beanMappingOptions) {
// we are not interested in keeping it unique at this point.
List<SubclassMappingOptions> mappings = new ArrayList<>();
for ( SubclassMappingOptions subclassMapping : subclassMappings ) {
mappings.add(
new SubclassMappingOptions(
subclassMapping.target,
subclassMapping.source,
subclassMapping.typeUtils,
beanMappingOptions ) );
new SubclassMappingOptions(
subclassMapping.target,
subclassMapping.source,
subclassMapping.typeUtils,
beanMappingOptions,
subclassMapping.selectionParameters
) );
}
return mappings;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.test.subclassmapping.qualifier;

public class Composer {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* 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.test.subclassmapping.qualifier;

public class ComposerDto {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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.test.subclassmapping.qualifier;

import org.mapstruct.Mapper;
import org.mapstruct.SubclassMapping;

@Mapper(uses = { RossiniMapper.class, VivaldiMapper.class })
public interface ErroneousSubclassQualifiedByMapper {
@SubclassMapping(source = Rossini.class, target = RossiniDto.class, qualifiedBy = NonExistent.class)
@SubclassMapping(source = Vivaldi.class, target = VivaldiDto.class)
ComposerDto toDto(Composer composer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* 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.test.subclassmapping.qualifier;

import org.mapstruct.Mapper;
import org.mapstruct.SubclassMapping;

@Mapper(uses = { RossiniMapper.class, VivaldiMapper.class })
public interface ErroneousSubclassQualifiedByNameMapper {
@SubclassMapping(source = Rossini.class, target = RossiniDto.class, qualifiedByName = "non-existent")
@SubclassMapping(source = Vivaldi.class, target = VivaldiDto.class)
ComposerDto toDto(Composer composer);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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.test.subclassmapping.qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface Light {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* 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.test.subclassmapping.qualifier;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.mapstruct.Qualifier;

@Qualifier
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface NonExistent {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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.test.subclassmapping.qualifier;

import java.util.List;

public class Rossini extends Composer {
private List<String> crescendo;

public List<String> getCrescendo() {
return crescendo;
}

public void setCrescendo(List<String> crescendo) {
this.crescendo = crescendo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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.test.subclassmapping.qualifier;

import java.util.List;

public class RossiniDto extends ComposerDto {
private List<String> crescendo;

public List<String> getCrescendo() {
return crescendo;
}

public void setCrescendo(List<String> crescendo) {
this.crescendo = crescendo;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.test.subclassmapping.qualifier;

import org.mapstruct.BeanMapping;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Named;

@Mapper
public interface RossiniMapper {
RossiniDto toDto(Rossini rossini);

@Light
@Mapping(target = "crescendo", ignore = true)
RossiniDto toDtoLight(Rossini source);

@Named("light")
@Mapping(target = "crescendo", ignore = true)
RossiniDto toDtoLightNamed(Rossini source);

@Unused
@BeanMapping(ignoreByDefault = true)
RossiniDto toDtoUnused(Rossini source);

@Named("unused")
@BeanMapping(ignoreByDefault = true)
RossiniDto toDtoUnusedNamed(Rossini source);

Rossini fromDto(RossiniDto rossini);

@Light
@Mapping(target = "crescendo", ignore = true)
Rossini fromDtoLight(RossiniDto source);

@Named("light")
@Mapping(target = "crescendo", ignore = true)
Rossini fromDtoLightNamed(RossiniDto source);

@Unused
@BeanMapping(ignoreByDefault = true)
Rossini fromDtoUnused(RossiniDto source);

@Named("unused")
@BeanMapping(ignoreByDefault = true)
Rossini fromDtoUnusedNamed(RossiniDto source);
}
Loading