diff --git a/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc b/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc index ba7e4d902..f545113af 100644 --- a/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc +++ b/documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc @@ -1974,3 +1974,132 @@ The order in which the selected methods are applied is roughly determined by the ==== `@BeforeMapping` and `@AfterMapping` are considered experimental as of the 1.0.0.CR1 release. Details in the selection of before/after mapping methods that are applicable for a mapping method or the order in which they are called might still be changed. ==== +[[using-spi]] +== Using the MapStruct SPI +=== Custom Accessor Naming Strategy + +MapStruct offers the possibility to override the `AccessorNamingStrategy` via the Service Provide Interface (SPI). A nice example is the use of the fluent API on the source object `GolfPlayer` and `GolfPlayerDto` below. + +.Source object GolfPlayer with fluent API. +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public class GolfPlayer { + + private double handicap; + private String name; + + public double handicap() { + return handicap; + } + + public GolfPlayer withHandicap(double handicap) { + this.handicap = handicap; + return this; + } + + public String name() { + return name; + } + + public GolfPlayer withName(String name) { + this.name = name; + return this; + } +} +---- +==== + +.Source object GolfPlayerDto with fluent API. +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +public class GolfPlayerDto { + + private double handicap; + private String name; + + public double handicap() { + return handicap; + } + + public GolfPlayerDto withHandicap(double handicap) { + this.handicap = handicap; + return this; + } + + public String name() { + return name; + } + + public GolfPlayerDto withName(String name) { + this.name = name; + return this + } +} +---- +==== + +We want `GolfPlayer` to be mapped to a target object `GolfPlayerDto` similar like we 'always' do this: + +.Source object with fluent API. +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +@Mapper +public interface GolfPlayerMapper { + + GolfPlayerMapper INSTANCE = Mappers.getMapper( GolfPlayerMapper.class ); + + GolfPlayerDto toDto(GolfPlayer player); + + GolfPlayer toPlayer(GolfPlayerDto player); + +} +---- +==== + +This can be achieved with implementing the SPI `org.mapstruct.ap.spi.AccessorNamingStrategy` as in the following example. Here's an implemented `org.mapstruct.ap.spi.AccessorNamingStrategy`: + +.CustomAccessorNamingStrategy +==== +[source, java, linenums] +[subs="verbatim,attributes"] +---- +/** + * A custom {@link AccessorNamingStrategy} recognizing getters in the form of {@code property()} and setters in the + * form of {@code withProperty(value)}. + */ +public class CustomAccessorNamingStrategy extends DefaultAccessorNamingStrategy { + + @Override + public boolean isGetterMethod(ExecutableElement method) { + String methodName = method.getSimpleName().toString(); + return !methodName.startsWith( "with" ) && method.getReturnType().getKind() != TypeKind.VOID; + } + + @Override + public boolean isSetterMethod(ExecutableElement method) { + String methodName = method.getSimpleName().toString(); + return methodName.startsWith( "with" ) && methodName.length() > 4; + } + + @Override + public String getPropertyName(ExecutableElement getterOrSetterMethod) { + String methodName = getterOrSetterMethod.getSimpleName().toString(); + return Introspector.decapitalize( methodName.startsWith( "with" ) ? methodName.substring( 4 ) : methodName ); + } +} +---- +==== +The `CustomAccessorNamingStrategy` makes use of the `DefaultAccessorNamingStrategy` (also available in mapstruct-processor) and relies on that class to leave most of the default behaviour unchanged. + +To use a custom SPI implementation, it must be located in a seperate .jar file together with the file `META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy` with the fully qualified name of your custom implementation as content (e.g. `org.mapstruct.example.CustomAccessorNamingStrategy`). This .jar file needs to be added to the annotation processor classpath (i.e. add it next to the place where you added the mapstruct-processor jar). + +[TIP] +Fore more details: There's the above example is present in our our examples repository (https://github.com/mapstruct/mapstruct-examples). + + diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Executables.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Executables.java index ed0ac11f1..3067242d6 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/util/Executables.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Executables.java @@ -34,6 +34,8 @@ import javax.lang.model.type.DeclaredType; import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Elements; +import javax.lang.model.util.SimpleElementVisitor6; +import javax.lang.model.util.SimpleTypeVisitor6; import org.mapstruct.ap.internal.prism.AfterMappingPrism; import org.mapstruct.ap.internal.prism.BeforeMappingPrism; @@ -75,9 +77,11 @@ public class Executables { } public static boolean isPresenceCheckMethod(ExecutableElement method) { - return isPublic( method ) && - method.getParameters().isEmpty() && - ACCESSOR_NAMING_STRATEGY.getMethodType( method ) == MethodType.PRESENCE_CHECKER; + return isPublic( method ) + && method.getParameters().isEmpty() + && ( method.getReturnType().getKind() == TypeKind.BOOLEAN || + "java.lang.Boolean".equals( getQualifiedName( method.getReturnType() ) ) ) + && ACCESSOR_NAMING_STRATEGY.getMethodType( method ) == MethodType.PRESENCE_CHECKER; } public static boolean isSetterMethod(ExecutableElement method) { @@ -284,4 +288,33 @@ public class Executables { public static boolean isBeforeMappingMethod(ExecutableElement executableElement) { return BeforeMappingPrism.getInstanceOn( executableElement ) != null; } + + private static String getQualifiedName(TypeMirror type) { + DeclaredType declaredType = type.accept( + new SimpleTypeVisitor6() { + @Override + public DeclaredType visitDeclared(DeclaredType t, Void p) { + return t; + } + }, + null + ); + + if ( declaredType == null ) { + return null; + } + + TypeElement typeElement = declaredType.asElement().accept( + new SimpleElementVisitor6() { + @Override + public TypeElement visitType(TypeElement e, Void p) { + return e; + } + }, + null + ); + + return typeElement != null ? typeElement.getQualifiedName().toString() : null; + } + } diff --git a/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java b/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java index 1420a1c57..cf8c5e460 100644 --- a/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java +++ b/processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java @@ -32,7 +32,7 @@ import javax.lang.model.util.SimpleTypeVisitor6; /** * The default JavaBeans-compliant implementation of the {@link AccessorNamingStrategy} service provider interface. * - * @author Christian Schuster + * @author Christian Schuster, Sjaak Derken */ public class DefaultAccessorNamingStrategy implements AccessorNamingStrategy { @@ -55,7 +55,19 @@ public class DefaultAccessorNamingStrategy implements AccessorNamingStrategy { } } - private boolean isGetterMethod(ExecutableElement method) { + /** + * Returns {@code true} when the {@link ExecutableElement} is a getter method. A method is a getter when it starts + * with 'get' and the return type is any type other than {@code void}, OR the getter starts with 'is' and the type + * returned is a primitive or the wrapper for {@code boolean}. NOTE: the latter does strictly not comply to the bean + * convention. The remainder of the name is supposed to reflect the property name. + *

+ * The calling MapStruct code guarantees that the given method has no arguments. + * + * @param method to be analyzed + * + * @return {@code true} when the method is a getter. + */ + public boolean isGetterMethod(ExecutableElement method) { String methodName = method.getSimpleName().toString(); boolean isNonBooleanGetterName = methodName.startsWith( "get" ) && methodName.length() > 3 && @@ -68,36 +80,109 @@ public class DefaultAccessorNamingStrategy implements AccessorNamingStrategy { return isNonBooleanGetterName || ( isBooleanGetterName && returnTypeIsBoolean ); } + + /** + * Returns {@code true} when the {@link ExecutableElement} is a setter method. A setter starts with 'set'. The + * remainder of the name is supposed to reflect the property name. + *

+ * The calling MapStruct code guarantees that there's only one argument. + * + * @param method to be analyzed + * @return {@code true} when the method is a setter. + */ public boolean isSetterMethod(ExecutableElement method) { String methodName = method.getSimpleName().toString(); return methodName.startsWith( "set" ) && methodName.length() > 3; } + /** + * Returns {@code true} when the {@link ExecutableElement} is an adder method. An adder method starts with 'add'. + * The remainder of the name is supposed to reflect the singular property name (as opposed to plural) of + * its corresponding property. For example: property "children", but "addChild". See also + * {@link #getElementName(ExecutableElement) }. + *

+ * The calling MapStruct code guarantees there's only one argument. + *

+ * + * @param method to be analyzed + * + * @return {@code true} when the method is an adder method. + */ public boolean isAdderMethod(ExecutableElement method) { String methodName = method.getSimpleName().toString(); return methodName.startsWith( "add" ) && methodName.length() > 3; } + /** + * Returns {@code true} when the {@link ExecutableElement} is a presence check method that checks if the + * corresponding property is present (e.g. not null, not nil, ..). A presence check method method starts with + * 'has'. The remainder of the name is supposed to reflect the property name. + *

+ * The calling MapStruct code guarantees there's no argument and that the return type is boolean or a + * {@link Boolean} + * + * @param method to be analyzed + * @return {@code true} when the method is a presence check method. + */ + public boolean isPresenceCheckMethod(ExecutableElement method) { + String methodName = method.getSimpleName().toString(); + return methodName.startsWith( "has" ) && methodName.length() > 3; + } + /** + * Analyzes the method (getter or setter) and derives the property name. + * See {@link #isGetterMethod(ExecutableElement)} {@link #isSetterMethod(ExecutableElement)}. The first three + * ('get' / 'set' scenario) characters are removed from the simple name, or the first 2 characters ('is' scenario). + * From the remainder the first character is made into small case (to counter camel casing) and the result forms + * the property name. + * + * @param getterOrSetterMethod getter or setter method. + * + * @return the property name. + */ @Override public String getPropertyName(ExecutableElement getterOrSetterMethod) { String methodName = getterOrSetterMethod.getSimpleName().toString(); return Introspector.decapitalize( methodName.substring( methodName.startsWith( "is" ) ? 2 : 3 ) ); } + /** + * Adder methods are used to add elements to collections on a target bean. A typical use case is JPA. The + * convention is that the element name will be equal to the remainder of the add method. Example: 'addElement' + * element name will be 'element'. + * + * @param adderMethod getter or setter method. + * + * @return the property name. + */ @Override public String getElementName(ExecutableElement adderMethod) { String methodName = adderMethod.getSimpleName().toString(); return Introspector.decapitalize( methodName.substring( 3 ) ); } + /** + * Returns the getter name of a collection given the property name. This will start with 'get' and the + * first character of the remainder will be placed in upper case. + * + * @param property the property + * + * @return getter name for collections. + */ @Override public String getCollectionGetterName(String property) { return "get" + property.substring( 0, 1 ).toUpperCase() + property.substring( 1 ); } + /** + * Helper method, to obtain the fully qualified name of a type. + * + * @param type input type + * + * @return fully qualified name of type when the type is a {@link DeclaredType}, null when otherwise. + */ protected static String getQualifiedName(TypeMirror type) { DeclaredType declaredType = type.accept( new SimpleTypeVisitor6() { @@ -126,12 +211,4 @@ public class DefaultAccessorNamingStrategy implements AccessorNamingStrategy { return typeElement != null ? typeElement.getQualifiedName().toString() : null; } - private boolean isPresenceCheckMethod(ExecutableElement method) { - String methodName = method.getSimpleName().toString(); - - return methodName.startsWith( "has" ) && methodName.length() > 3 && - ( method.getReturnType().getKind() == TypeKind.BOOLEAN || - "java.lang.Boolean".equals( getQualifiedName( method.getReturnType() ) ) ); - } - }