Skip to content

Commit daddb25

Browse files
author
sjaakd
committed
mapstruct#832, mapstruct#866 Documentation update on the SPI, adding DefaultAccessorNamingStrategy javadoc, fixing responsibility issue
1 parent d995903 commit daddb25

3 files changed

Lines changed: 252 additions & 13 deletions

File tree

documentation/src/main/asciidoc/mapstruct-reference-guide.asciidoc

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1974,3 +1974,132 @@ The order in which the selected methods are applied is roughly determined by the
19741974
====
19751975
`@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.
19761976
====
1977+
[[using-spi]]
1978+
== Using the MapStruct SPI
1979+
=== Custom Accessor Naming Strategy
1980+
1981+
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.
1982+
1983+
.Source object GolfPlayer with fluent API.
1984+
====
1985+
[source, java, linenums]
1986+
[subs="verbatim,attributes"]
1987+
----
1988+
public class GolfPlayer {
1989+
1990+
private double handicap;
1991+
private String name;
1992+
1993+
public double handicap() {
1994+
return handicap;
1995+
}
1996+
1997+
public GolfPlayer withHandicap(double handicap) {
1998+
this.handicap = handicap;
1999+
return this;
2000+
}
2001+
2002+
public String name() {
2003+
return name;
2004+
}
2005+
2006+
public GolfPlayer withName(String name) {
2007+
this.name = name;
2008+
return this;
2009+
}
2010+
}
2011+
----
2012+
====
2013+
2014+
.Source object GolfPlayerDto with fluent API.
2015+
====
2016+
[source, java, linenums]
2017+
[subs="verbatim,attributes"]
2018+
----
2019+
public class GolfPlayerDto {
2020+
2021+
private double handicap;
2022+
private String name;
2023+
2024+
public double handicap() {
2025+
return handicap;
2026+
}
2027+
2028+
public GolfPlayerDto withHandicap(double handicap) {
2029+
this.handicap = handicap;
2030+
return this;
2031+
}
2032+
2033+
public String name() {
2034+
return name;
2035+
}
2036+
2037+
public GolfPlayerDto withName(String name) {
2038+
this.name = name;
2039+
return this
2040+
}
2041+
}
2042+
----
2043+
====
2044+
2045+
We want `GolfPlayer` to be mapped to a target object `GolfPlayerDto` similar like we 'always' do this:
2046+
2047+
.Source object with fluent API.
2048+
====
2049+
[source, java, linenums]
2050+
[subs="verbatim,attributes"]
2051+
----
2052+
@Mapper
2053+
public interface GolfPlayerMapper {
2054+
2055+
GolfPlayerMapper INSTANCE = Mappers.getMapper( GolfPlayerMapper.class );
2056+
2057+
GolfPlayerDto toDto(GolfPlayer player);
2058+
2059+
GolfPlayer toPlayer(GolfPlayerDto player);
2060+
2061+
}
2062+
----
2063+
====
2064+
2065+
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`:
2066+
2067+
.CustomAccessorNamingStrategy
2068+
====
2069+
[source, java, linenums]
2070+
[subs="verbatim,attributes"]
2071+
----
2072+
/**
2073+
* A custom {@link AccessorNamingStrategy} recognizing getters in the form of {@code property()} and setters in the
2074+
* form of {@code withProperty(value)}.
2075+
*/
2076+
public class CustomAccessorNamingStrategy extends DefaultAccessorNamingStrategy {
2077+
2078+
@Override
2079+
public boolean isGetterMethod(ExecutableElement method) {
2080+
String methodName = method.getSimpleName().toString();
2081+
return !methodName.startsWith( "with" ) && method.getReturnType().getKind() != TypeKind.VOID;
2082+
}
2083+
2084+
@Override
2085+
public boolean isSetterMethod(ExecutableElement method) {
2086+
String methodName = method.getSimpleName().toString();
2087+
return methodName.startsWith( "with" ) && methodName.length() > 4;
2088+
}
2089+
2090+
@Override
2091+
public String getPropertyName(ExecutableElement getterOrSetterMethod) {
2092+
String methodName = getterOrSetterMethod.getSimpleName().toString();
2093+
return Introspector.decapitalize( methodName.startsWith( "with" ) ? methodName.substring( 4 ) : methodName );
2094+
}
2095+
}
2096+
----
2097+
====
2098+
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.
2099+
2100+
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).
2101+
2102+
[TIP]
2103+
Fore more details: There's the above example is present in our our examples repository (https://github.com/mapstruct/mapstruct-examples).
2104+
2105+

processor/src/main/java/org/mapstruct/ap/internal/util/Executables.java

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@
3434
import javax.lang.model.type.TypeKind;
3535
import javax.lang.model.type.TypeMirror;
3636
import javax.lang.model.util.Elements;
37+
import javax.lang.model.util.SimpleElementVisitor6;
38+
import javax.lang.model.util.SimpleTypeVisitor6;
3739

3840
import org.mapstruct.ap.internal.prism.AfterMappingPrism;
3941
import org.mapstruct.ap.internal.prism.BeforeMappingPrism;
@@ -75,9 +77,11 @@ public static boolean isGetterMethod(ExecutableElement method) {
7577
}
7678

7779
public static boolean isPresenceCheckMethod(ExecutableElement method) {
78-
return isPublic( method ) &&
79-
method.getParameters().isEmpty() &&
80-
ACCESSOR_NAMING_STRATEGY.getMethodType( method ) == MethodType.PRESENCE_CHECKER;
80+
return isPublic( method )
81+
&& method.getParameters().isEmpty()
82+
&& ( method.getReturnType().getKind() == TypeKind.BOOLEAN ||
83+
"java.lang.Boolean".equals( getQualifiedName( method.getReturnType() ) ) )
84+
&& ACCESSOR_NAMING_STRATEGY.getMethodType( method ) == MethodType.PRESENCE_CHECKER;
8185
}
8286

8387
public static boolean isSetterMethod(ExecutableElement method) {
@@ -284,4 +288,33 @@ public static boolean isAfterMappingMethod(ExecutableElement executableElement)
284288
public static boolean isBeforeMappingMethod(ExecutableElement executableElement) {
285289
return BeforeMappingPrism.getInstanceOn( executableElement ) != null;
286290
}
291+
292+
private static String getQualifiedName(TypeMirror type) {
293+
DeclaredType declaredType = type.accept(
294+
new SimpleTypeVisitor6<DeclaredType, Void>() {
295+
@Override
296+
public DeclaredType visitDeclared(DeclaredType t, Void p) {
297+
return t;
298+
}
299+
},
300+
null
301+
);
302+
303+
if ( declaredType == null ) {
304+
return null;
305+
}
306+
307+
TypeElement typeElement = declaredType.asElement().accept(
308+
new SimpleElementVisitor6<TypeElement, Void>() {
309+
@Override
310+
public TypeElement visitType(TypeElement e, Void p) {
311+
return e;
312+
}
313+
},
314+
null
315+
);
316+
317+
return typeElement != null ? typeElement.getQualifiedName().toString() : null;
318+
}
319+
287320
}

processor/src/main/java/org/mapstruct/ap/spi/DefaultAccessorNamingStrategy.java

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
/**
3333
* The default JavaBeans-compliant implementation of the {@link AccessorNamingStrategy} service provider interface.
3434
*
35-
* @author Christian Schuster
35+
* @author Christian Schuster, Sjaak Derken
3636
*/
3737
public class DefaultAccessorNamingStrategy implements AccessorNamingStrategy {
3838

@@ -55,7 +55,19 @@ else if ( isPresenceCheckMethod( method ) ) {
5555
}
5656
}
5757

58-
private boolean isGetterMethod(ExecutableElement method) {
58+
/**
59+
* Returns {@code true} when the {@link ExecutableElement} is a getter method. A method is a getter when it starts
60+
* with 'get' and the return type is any type other than {@code void}, OR the getter starts with 'is' and the type
61+
* returned is a primitive or the wrapper for {@code boolean}. NOTE: the latter does strictly not comply to the bean
62+
* convention. The remainder of the name is supposed to reflect the property name.
63+
* <p>
64+
* The calling MapStruct code guarantees that the given method has no arguments.
65+
*
66+
* @param method to be analyzed
67+
*
68+
* @return {@code true} when the method is a getter.
69+
*/
70+
public boolean isGetterMethod(ExecutableElement method) {
5971
String methodName = method.getSimpleName().toString();
6072

6173
boolean isNonBooleanGetterName = methodName.startsWith( "get" ) && methodName.length() > 3 &&
@@ -68,36 +80,109 @@ private boolean isGetterMethod(ExecutableElement method) {
6880
return isNonBooleanGetterName || ( isBooleanGetterName && returnTypeIsBoolean );
6981
}
7082

83+
84+
/**
85+
* Returns {@code true} when the {@link ExecutableElement} is a setter method. A setter starts with 'set'. The
86+
* remainder of the name is supposed to reflect the property name.
87+
* <p>
88+
* The calling MapStruct code guarantees that there's only one argument.
89+
*
90+
* @param method to be analyzed
91+
* @return {@code true} when the method is a setter.
92+
*/
7193
public boolean isSetterMethod(ExecutableElement method) {
7294
String methodName = method.getSimpleName().toString();
7395

7496
return methodName.startsWith( "set" ) && methodName.length() > 3;
7597
}
7698

99+
/**
100+
* Returns {@code true} when the {@link ExecutableElement} is an adder method. An adder method starts with 'add'.
101+
* The remainder of the name is supposed to reflect the <em>singular</em> property name (as opposed to plural) of
102+
* its corresponding property. For example: property "children", but "addChild". See also
103+
* {@link #getElementName(ExecutableElement) }.
104+
* <p>
105+
* The calling MapStruct code guarantees there's only one argument.
106+
* <p>
107+
*
108+
* @param method to be analyzed
109+
*
110+
* @return {@code true} when the method is an adder method.
111+
*/
77112
public boolean isAdderMethod(ExecutableElement method) {
78113
String methodName = method.getSimpleName().toString();
79114

80115
return methodName.startsWith( "add" ) && methodName.length() > 3;
81116
}
82117

118+
/**
119+
* Returns {@code true} when the {@link ExecutableElement} is a <em>presence check</em> method that checks if the
120+
* corresponding property is present (e.g. not null, not nil, ..). A presence check method method starts with
121+
* 'has'. The remainder of the name is supposed to reflect the property name.
122+
* <p>
123+
* The calling MapStruct code guarantees there's no argument and that the return type is boolean or a
124+
* {@link Boolean}
125+
*
126+
* @param method to be analyzed
127+
* @return {@code true} when the method is a presence check method.
128+
*/
129+
public boolean isPresenceCheckMethod(ExecutableElement method) {
130+
String methodName = method.getSimpleName().toString();
131+
return methodName.startsWith( "has" ) && methodName.length() > 3;
132+
}
83133

134+
/**
135+
* Analyzes the method (getter or setter) and derives the property name.
136+
* See {@link #isGetterMethod(ExecutableElement)} {@link #isSetterMethod(ExecutableElement)}. The first three
137+
* ('get' / 'set' scenario) characters are removed from the simple name, or the first 2 characters ('is' scenario).
138+
* From the remainder the first character is made into small case (to counter camel casing) and the result forms
139+
* the property name.
140+
*
141+
* @param getterOrSetterMethod getter or setter method.
142+
*
143+
* @return the property name.
144+
*/
84145
@Override
85146
public String getPropertyName(ExecutableElement getterOrSetterMethod) {
86147
String methodName = getterOrSetterMethod.getSimpleName().toString();
87148
return Introspector.decapitalize( methodName.substring( methodName.startsWith( "is" ) ? 2 : 3 ) );
88149
}
89150

151+
/**
152+
* Adder methods are used to add elements to collections on a target bean. A typical use case is JPA. The
153+
* convention is that the element name will be equal to the remainder of the add method. Example: 'addElement'
154+
* element name will be 'element'.
155+
*
156+
* @param adderMethod getter or setter method.
157+
*
158+
* @return the property name.
159+
*/
90160
@Override
91161
public String getElementName(ExecutableElement adderMethod) {
92162
String methodName = adderMethod.getSimpleName().toString();
93163
return Introspector.decapitalize( methodName.substring( 3 ) );
94164
}
95165

166+
/**
167+
* Returns the getter name of a collection given the property name. This will start with 'get' and the
168+
* first character of the remainder will be placed in upper case.
169+
*
170+
* @param property the property
171+
*
172+
* @return getter name for collections.
173+
*/
96174
@Override
97175
public String getCollectionGetterName(String property) {
98176
return "get" + property.substring( 0, 1 ).toUpperCase() + property.substring( 1 );
99177
}
100178

179+
/**
180+
* Helper method, to obtain the fully qualified name of a type.
181+
*
182+
* @param type input type
183+
*
184+
* @return fully qualified name of type when the type is a {@link DeclaredType}, null when otherwise.
185+
*/
101186
protected static String getQualifiedName(TypeMirror type) {
102187
DeclaredType declaredType = type.accept(
103188
new SimpleTypeVisitor6<DeclaredType, Void>() {
@@ -126,12 +211,4 @@ public TypeElement visitType(TypeElement e, Void p) {
126211
return typeElement != null ? typeElement.getQualifiedName().toString() : null;
127212
}
128213

129-
private boolean isPresenceCheckMethod(ExecutableElement method) {
130-
String methodName = method.getSimpleName().toString();
131-
132-
return methodName.startsWith( "has" ) && methodName.length() > 3 &&
133-
( method.getReturnType().getKind() == TypeKind.BOOLEAN ||
134-
"java.lang.Boolean".equals( getQualifiedName( method.getReturnType() ) ) );
135-
}
136-
137214
}

0 commit comments

Comments
 (0)