#832, #866 Documentation update on the SPI, adding DefaultAccessorNamingStrategy javadoc, fixing responsibility issue

This commit is contained in:
sjaakd 2016-08-27 14:20:47 +02:00
parent d99590306e
commit daddb255dd
3 changed files with 252 additions and 13 deletions

View File

@ -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. `@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).

View File

@ -34,6 +34,8 @@ import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind; import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Elements; 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.AfterMappingPrism;
import org.mapstruct.ap.internal.prism.BeforeMappingPrism; import org.mapstruct.ap.internal.prism.BeforeMappingPrism;
@ -75,9 +77,11 @@ public class Executables {
} }
public static boolean isPresenceCheckMethod(ExecutableElement method) { public static boolean isPresenceCheckMethod(ExecutableElement method) {
return isPublic( method ) && return isPublic( method )
method.getParameters().isEmpty() && && method.getParameters().isEmpty()
ACCESSOR_NAMING_STRATEGY.getMethodType( method ) == MethodType.PRESENCE_CHECKER; && ( 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) { public static boolean isSetterMethod(ExecutableElement method) {
@ -284,4 +288,33 @@ public class Executables {
public static boolean isBeforeMappingMethod(ExecutableElement executableElement) { public static boolean isBeforeMappingMethod(ExecutableElement executableElement) {
return BeforeMappingPrism.getInstanceOn( executableElement ) != null; return BeforeMappingPrism.getInstanceOn( executableElement ) != null;
} }
private static String getQualifiedName(TypeMirror type) {
DeclaredType declaredType = type.accept(
new SimpleTypeVisitor6<DeclaredType, Void>() {
@Override
public DeclaredType visitDeclared(DeclaredType t, Void p) {
return t;
}
},
null
);
if ( declaredType == null ) {
return null;
}
TypeElement typeElement = declaredType.asElement().accept(
new SimpleElementVisitor6<TypeElement, Void>() {
@Override
public TypeElement visitType(TypeElement e, Void p) {
return e;
}
},
null
);
return typeElement != null ? typeElement.getQualifiedName().toString() : null;
}
} }

View File

@ -32,7 +32,7 @@ import javax.lang.model.util.SimpleTypeVisitor6;
/** /**
* The default JavaBeans-compliant implementation of the {@link AccessorNamingStrategy} service provider interface. * 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 { 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.
* <p>
* 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(); String methodName = method.getSimpleName().toString();
boolean isNonBooleanGetterName = methodName.startsWith( "get" ) && methodName.length() > 3 && boolean isNonBooleanGetterName = methodName.startsWith( "get" ) && methodName.length() > 3 &&
@ -68,36 +80,109 @@ public class DefaultAccessorNamingStrategy implements AccessorNamingStrategy {
return isNonBooleanGetterName || ( isBooleanGetterName && returnTypeIsBoolean ); 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.
* <p>
* 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) { public boolean isSetterMethod(ExecutableElement method) {
String methodName = method.getSimpleName().toString(); String methodName = method.getSimpleName().toString();
return methodName.startsWith( "set" ) && methodName.length() > 3; 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 <em>singular</em> property name (as opposed to plural) of
* its corresponding property. For example: property "children", but "addChild". See also
* {@link #getElementName(ExecutableElement) }.
* <p>
* The calling MapStruct code guarantees there's only one argument.
* <p>
*
* @param method to be analyzed
*
* @return {@code true} when the method is an adder method.
*/
public boolean isAdderMethod(ExecutableElement method) { public boolean isAdderMethod(ExecutableElement method) {
String methodName = method.getSimpleName().toString(); String methodName = method.getSimpleName().toString();
return methodName.startsWith( "add" ) && methodName.length() > 3; return methodName.startsWith( "add" ) && methodName.length() > 3;
} }
/**
* Returns {@code true} when the {@link ExecutableElement} is a <em>presence check</em> 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.
* <p>
* 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 @Override
public String getPropertyName(ExecutableElement getterOrSetterMethod) { public String getPropertyName(ExecutableElement getterOrSetterMethod) {
String methodName = getterOrSetterMethod.getSimpleName().toString(); String methodName = getterOrSetterMethod.getSimpleName().toString();
return Introspector.decapitalize( methodName.substring( methodName.startsWith( "is" ) ? 2 : 3 ) ); 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 @Override
public String getElementName(ExecutableElement adderMethod) { public String getElementName(ExecutableElement adderMethod) {
String methodName = adderMethod.getSimpleName().toString(); String methodName = adderMethod.getSimpleName().toString();
return Introspector.decapitalize( methodName.substring( 3 ) ); 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 @Override
public String getCollectionGetterName(String property) { public String getCollectionGetterName(String property) {
return "get" + property.substring( 0, 1 ).toUpperCase() + property.substring( 1 ); 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) { protected static String getQualifiedName(TypeMirror type) {
DeclaredType declaredType = type.accept( DeclaredType declaredType = type.accept(
new SimpleTypeVisitor6<DeclaredType, Void>() { new SimpleTypeVisitor6<DeclaredType, Void>() {
@ -126,12 +211,4 @@ public class DefaultAccessorNamingStrategy implements AccessorNamingStrategy {
return typeElement != null ? typeElement.getQualifiedName().toString() : null; 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() ) ) );
}
} }