#32 Establishing processing chain for model creation and transformation

This commit is contained in:
Gunnar Morling 2013-06-27 22:56:49 +02:00
parent 9a4e51801f
commit a565bed0c7
12 changed files with 692 additions and 380 deletions

View File

@ -18,9 +18,11 @@
*/
package org.mapstruct.ap;
import java.util.Arrays;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
@ -28,18 +30,45 @@ import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.ElementKindVisitor6;
import net.java.dev.hickory.prism.GeneratePrism;
import net.java.dev.hickory.prism.GeneratePrisms;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.ap.model.Mapper;
import org.mapstruct.ap.model.Options;
import org.mapstruct.ap.model.ReportingPolicy;
import org.mapstruct.ap.processor.DefaultModelElementProcessorContext;
import org.mapstruct.ap.processor.MapperCreationProcessor;
import org.mapstruct.ap.processor.MapperRenderingProcessor;
import org.mapstruct.ap.processor.MethodRetrievalProcessor;
import org.mapstruct.ap.processor.ModelElementProcessor;
import org.mapstruct.ap.processor.ModelElementProcessor.ProcessorContext;
/**
* A {@link Processor} which generates the implementations for mapper interfaces
* (interfaces annotated with {@code @Mapper}.
* </p>
* Implementation notes:
* </p>
* The generation happens by incrementally building up a model representation of
* each mapper to be generated (a {@link Mapper} object), which is then written
* into the resulting Java source file using the FreeMarker template engine.
* </p>
* The model instantiation and processing happens in several phases/passes by applying
* a sequence of {@link ModelElementProcessor}s.
* </p>
* For reading annotation attributes, prisms as generated with help of the <a
* href="https://java.net/projects/hickory">Hickory</a> tool are used. These
* prisms allow a comfortable access to annotations and their attributes without
* depending on their class objects.
*
* @author Gunnar Morling
*/
@SupportedAnnotationTypes("org.mapstruct.Mapper")
@GeneratePrisms({
@GeneratePrism(value = Mapper.class, publicAccess = true),
@GeneratePrism(value = org.mapstruct.Mapper.class, publicAccess = true),
@GeneratePrism(value = Mapping.class, publicAccess = true),
@GeneratePrism(value = Mappings.class, publicAccess = true)
})
@ -63,32 +92,6 @@ public class MappingProcessor extends AbstractProcessor {
options = createOptions();
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(
final Set<? extends TypeElement> annotations,
final RoundEnvironment roundEnvironment) {
for ( TypeElement oneAnnotation : annotations ) {
//Indicates that the annotation's type isn't on the class path of the compiled
//project. Let the compiler deal with that and print an appropriate error.
if ( oneAnnotation.getKind() != ElementKind.ANNOTATION_TYPE ) {
continue;
}
for ( Element oneAnnotatedElement : roundEnvironment.getElementsAnnotatedWith( oneAnnotation ) ) {
oneAnnotatedElement.accept( new MapperGenerationVisitor( processingEnv, options ), null );
}
}
return ANNOTATIONS_CLAIMED_EXCLUSIVELY;
}
private Options createOptions() {
String unmappedTargetPolicy = processingEnv.getOptions().get( UNMAPPED_TARGET_POLICY );
@ -97,4 +100,65 @@ public class MappingProcessor extends AbstractProcessor {
unmappedTargetPolicy != null ? ReportingPolicy.valueOf( unmappedTargetPolicy ) : null
);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnvironment) {
ProcessorContext context = new DefaultModelElementProcessorContext( processingEnv, options );
for ( TypeElement annotation : annotations ) {
//Indicates that the annotation's type isn't on the class path of the compiled
//project. Let the compiler deal with that and print an appropriate error.
if ( annotation.getKind() != ElementKind.ANNOTATION_TYPE ) {
continue;
}
for ( Element mapperElement : roundEnvironment.getElementsAnnotatedWith( annotation ) ) {
TypeElement mapperTypeElement = asTypeElement( mapperElement );
processMapperTypeElement( context, mapperTypeElement );
}
}
return ANNOTATIONS_CLAIMED_EXCLUSIVELY;
}
private void processMapperTypeElement(ProcessorContext context, TypeElement mapperTypeElement) {
Object mapper = mapperTypeElement;
for ( ModelElementProcessor<?, ?> processor : getProcessors() ) {
mapper = process( context, processor, mapperTypeElement, mapper );
}
}
private <P, R> R process(ProcessorContext context, ModelElementProcessor<P, R> processor,
TypeElement mapperTypeElement, Object modelElement) {
@SuppressWarnings("unchecked")
P sourceElement = (P) modelElement;
return processor.process( context, mapperTypeElement, sourceElement );
}
//TODO Retrieve via service loader
private Iterable<ModelElementProcessor<?, ?>> getProcessors() {
return Arrays.<ModelElementProcessor<?, ?>>asList(
new MethodRetrievalProcessor(),
new MapperCreationProcessor(),
new MapperRenderingProcessor()
);
}
private TypeElement asTypeElement(Element element) {
return element.accept(
new ElementKindVisitor6<TypeElement, Void>() {
@Override
public TypeElement visitTypeAsInterface(TypeElement e, Void p) {
return e;
}
}, null
);
}
}

View File

@ -28,12 +28,12 @@ import java.util.Set;
*
* @author Gunnar Morling
*/
public class SimpleMappingMethod extends MappingMethod {
public class BeanMappingMethod extends MappingMethod {
private final List<PropertyMapping> propertyMappings;
public SimpleMappingMethod(String name, String parameterName, Type sourceType, Type targetType,
List<PropertyMapping> propertyMappings) {
public BeanMappingMethod(String name, String parameterName, Type sourceType, Type targetType,
List<PropertyMapping> propertyMappings) {
super( name, parameterName, sourceType, targetType );
this.propertyMappings = propertyMappings;
}

View File

@ -33,10 +33,11 @@ public class Mapper extends AbstractModelElement {
private final List<Type> usedMapperTypes;
private final Options options;
private final SortedSet<Type> importedTypes;
private final boolean isErroneous;
public Mapper(String packageName, String interfaceName,
String implementationName, List<MappingMethod> mappingMethods, List<Type> usedMapperTypes,
Options options) {
Options options, boolean isErroneous) {
this.packageName = packageName;
this.interfaceName = interfaceName;
this.implementationName = implementationName;
@ -44,6 +45,7 @@ public class Mapper extends AbstractModelElement {
this.usedMapperTypes = usedMapperTypes;
this.options = options;
this.importedTypes = determineImportedTypes();
this.isErroneous = isErroneous;
}
private SortedSet<Type> determineImportedTypes() {
@ -122,4 +124,8 @@ public class Mapper extends AbstractModelElement {
public SortedSet<Type> getImportedTypes() {
return importedTypes;
}
public boolean isErroneous() {
return isErroneous;
}
}

View File

@ -83,6 +83,10 @@ public class Type implements Comparable<Type> {
this( null, name, null, false, false, false );
}
public Type(String packageName, String name) {
this( packageName, name, null, false, false, false );
}
public Type(String packageName, String name, Type elementType, boolean isEnumType, boolean isCollectionType,
boolean isIterableType) {
this.packageName = packageName;

View File

@ -0,0 +1,68 @@
/**
* Copyright 2012-2013 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.processor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.mapstruct.ap.model.Options;
/**
* Default implementation of the processor context.
*
* @author Gunnar Morling
*/
public class DefaultModelElementProcessorContext implements ModelElementProcessor.ProcessorContext {
private ProcessingEnvironment processingEnvironment;
private Options options;
public DefaultModelElementProcessorContext(ProcessingEnvironment processingEnvironment, Options options) {
this.processingEnvironment = processingEnvironment;
this.options = options;
}
@Override
public Filer getFiler() {
return processingEnvironment.getFiler();
}
@Override
public Types getTypeUtils() {
return processingEnvironment.getTypeUtils();
}
@Override
public Elements getElementUtils() {
return processingEnvironment.getElementUtils();
}
@Override
public Messager getMessager() {
return processingEnvironment.getMessager();
}
@Override
public Options getOptions() {
return options;
}
}

View File

@ -16,10 +16,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap;
package org.mapstruct.ap.processor;
import java.beans.Introspector;
import java.io.IOException;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.HashMap;
@ -28,23 +27,20 @@ import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.Messager;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementVisitor;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementKindVisitor6;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import javax.tools.JavaFileObject;
import org.mapstruct.ap.MapperPrism;
import org.mapstruct.ap.conversion.Conversion;
import org.mapstruct.ap.conversion.Conversions;
import org.mapstruct.ap.model.BeanMappingMethod;
import org.mapstruct.ap.model.IterableMappingMethod;
import org.mapstruct.ap.model.Mapper;
import org.mapstruct.ap.model.MappingMethod;
@ -52,111 +48,63 @@ import org.mapstruct.ap.model.MappingMethodReference;
import org.mapstruct.ap.model.Options;
import org.mapstruct.ap.model.PropertyMapping;
import org.mapstruct.ap.model.ReportingPolicy;
import org.mapstruct.ap.model.SimpleMappingMethod;
import org.mapstruct.ap.model.Type;
import org.mapstruct.ap.model.source.Mapping;
import org.mapstruct.ap.model.source.Method;
import org.mapstruct.ap.model.source.Parameter;
import org.mapstruct.ap.util.Executables;
import org.mapstruct.ap.util.Filters;
import org.mapstruct.ap.util.Strings;
import org.mapstruct.ap.util.TypeUtil;
import org.mapstruct.ap.writer.ModelWriter;
import static javax.lang.model.util.ElementFilter.methodsIn;
/**
* An {@link ElementVisitor} which generates the implementations for mapper
* interfaces (interfaces annotated with {@code @Mapper}.
* </p>
* Implementation notes:
* </p>
* The mapper generation happens by building up a model representation of
* the mapper to be generated (a {@link Mapper} object), which is then written
* into a file using the FreeMarker template engine.
* </p>
* The model instantiation happens in two phases/passes: The first one retrieves
* the mapping methods of the given interfaces and their configuration (the
* <i>source</i> model). In the second pass the individual methods are
* aggregated into the <i>target</i> model, which contains a {@link BeanMapping}
* each pair of source and target type which has references to forward and
* reverse mapping methods as well as the methods for mapping the element types
* (if it is a collection mapping) and {@link Conversion}s if applicable.
* </p>
* For reading annotation attributes, prisms as generated with help of the <a
* href="https://java.net/projects/hickory">Hickory</a> tool are used. These
* prisms allow a comfortable access to annotations and their attributes without
* depending on their class objects.
* A {@link ModelElementProcessor} which creates a {@link Mapper} from the given
* list of {@link Method}s.
*
* @author Gunnar Morling
*/
public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
public class MapperCreationProcessor implements ModelElementProcessor<List<Method>, Mapper> {
private static final String IMPLEMENTATION_SUFFIX = "Impl";
private final ProcessingEnvironment processingEnvironment;
private final Types typeUtils;
private final Elements elementUtils;
private final TypeUtil typeUtil;
private final Conversions conversions;
private final Options options;
private Elements elementUtils;
private Types typeUtils;
private Messager messager;
private Options options;
private boolean mappingErroneous = false;
private TypeUtil typeUtil;
private Conversions conversions;
private Executables executables;
public MapperGenerationVisitor(ProcessingEnvironment processingEnvironment, Options options) {
this.processingEnvironment = processingEnvironment;
this.typeUtils = processingEnvironment.getTypeUtils();
this.elementUtils = processingEnvironment.getElementUtils();
this.typeUtil = new TypeUtil( elementUtils, typeUtils );
this.conversions = new Conversions( elementUtils, typeUtils, typeUtil );
this.options = options;
}
private boolean isErroneous = false;
@Override
public Void visitTypeAsInterface(TypeElement element, Void p) {
Mapper model = retrieveModel( element );
public Mapper process(ProcessorContext context, TypeElement mapperTypeElement, List<Method> sourceElement) {
this.elementUtils = context.getElementUtils();
this.typeUtils = context.getTypeUtils();
this.messager = context.getMessager();
this.options = context.getOptions();
if ( !mappingErroneous ) {
String sourceFileName = element.getQualifiedName() + IMPLEMENTATION_SUFFIX;
writeModelToSourceFile( sourceFileName, model );
}
this.typeUtil = new TypeUtil( context.getElementUtils(), context.getTypeUtils() );
this.conversions = new Conversions( elementUtils, typeUtils, typeUtil );
this.executables = new Executables( typeUtil );
return null;
return getMapper( mapperTypeElement, sourceElement );
}
private void writeModelToSourceFile(String fileName, Mapper model) {
JavaFileObject sourceFile;
try {
sourceFile = processingEnvironment.getFiler().createSourceFile( fileName );
}
catch ( IOException e ) {
throw new RuntimeException( e );
}
new ModelWriter().writeModel( sourceFile, model );
}
private Mapper retrieveModel(TypeElement element) {
//1.) build up "source" model
List<Method> methods = retrieveMethods( element, true );
//2.) build up aggregated "target" model
List<MappingMethod> mappings = getMappingMethods(
methods,
getEffectiveUnmappedTargetPolicy( element )
);
public Mapper getMapper(TypeElement element, List<Method> methods) {
List<Type> usedMapperTypes = getUsedMapperTypes( element );
Mapper mapper = new Mapper(
ReportingPolicy unmappedTargetPolicy = getEffectiveUnmappedTargetPolicy( element );
return new Mapper(
elementUtils.getPackageOf( element ).getQualifiedName().toString(),
element.getSimpleName().toString(),
element.getSimpleName() + IMPLEMENTATION_SUFFIX,
mappings,
getMappingMethods( methods, unmappedTargetPolicy ),
usedMapperTypes,
options
options,
isErroneous
);
return mapper;
}
/**
@ -184,6 +132,15 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
}
}
private List<Type> getUsedMapperTypes(TypeElement element) {
List<Type> usedMapperTypes = new LinkedList<Type>();
MapperPrism mapperPrism = MapperPrism.getInstanceOn( element );
for ( TypeMirror usedMapper : mapperPrism.uses() ) {
usedMapperTypes.add( typeUtil.retrieveType( usedMapper ) );
}
return usedMapperTypes;
}
private List<MappingMethod> getMappingMethods(List<Method> methods, ReportingPolicy unmappedTargetPolicy) {
List<MappingMethod> mappingMethods = new ArrayList<MappingMethod>();
@ -203,7 +160,7 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
mappingMethods.add( getIterableMappingMethod( methods, method ) );
}
else {
mappingMethods.add( getSimpleMappingMethod( methods, method, unmappedTargetPolicy ) );
mappingMethods.add( getBeanMappingMethod( methods, method, unmappedTargetPolicy ) );
}
}
return mappingMethods;
@ -218,8 +175,8 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
return reversed;
}
private MappingMethod getSimpleMappingMethod(List<Method> methods, Method method,
ReportingPolicy unmappedTargetPolicy) {
private MappingMethod getBeanMappingMethod(List<Method> methods, Method method,
ReportingPolicy unmappedTargetPolicy) {
List<PropertyMapping> propertyMappings = new ArrayList<PropertyMapping>();
Set<String> mappedTargetProperties = new HashSet<String>();
@ -240,21 +197,21 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
elementUtils.getAllMembers( returnTypeElement )
);
Set<String> sourceProperties = Executables.getPropertyNames(
Set<String> sourceProperties = executables.getPropertyNames(
Filters.getterMethodsIn( sourceGetters )
);
Set<String> targetProperties = Executables.getPropertyNames(
Set<String> targetProperties = executables.getPropertyNames(
Filters.setterMethodsIn( targetSetters )
);
reportErrorIfMappedPropertiesDontExist( method, sourceProperties, targetProperties );
for ( ExecutableElement getterMethod : sourceGetters ) {
String sourcePropertyName = Executables.getPropertyName( getterMethod );
String sourcePropertyName = executables.getPropertyName( getterMethod );
Mapping mapping = mappings.get( sourcePropertyName );
for ( ExecutableElement setterMethod : targetSetters ) {
String targetPropertyName = Executables.getPropertyName( setterMethod );
String targetPropertyName = executables.getPropertyName( setterMethod );
if ( targetPropertyName.equals( mapping != null ? mapping.getTargetName() : sourcePropertyName ) ) {
PropertyMapping property = getPropertyMapping(
@ -278,7 +235,7 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
mappedTargetProperties
);
return new SimpleMappingMethod(
return new BeanMappingMethod(
method.getName(),
method.getParameterName(),
method.getSourceType(),
@ -287,10 +244,65 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
);
}
private void reportErrorForUnmappedTargetPropertiesIfRequired(Method method,
ReportingPolicy unmappedTargetPolicy,
Set<String> targetProperties,
Set<String> mappedTargetProperties) {
if ( targetProperties.size() > mappedTargetProperties.size() &&
unmappedTargetPolicy.requiresReport() ) {
targetProperties.removeAll( mappedTargetProperties );
printMessage(
unmappedTargetPolicy,
MessageFormat.format(
"Unmapped target {0,choice,1#property|1<properties}: \"{1}\"",
targetProperties.size(),
Strings.join( targetProperties, ", " )
),
method.getExecutable()
);
}
}
private Method getReverseMappingMethod(List<Method> rawMethods, Method method) {
for ( Method oneMethod : rawMethods ) {
if ( oneMethod.reverses( method ) ) {
return oneMethod;
}
}
return null;
}
private void reportErrorIfMappedPropertiesDontExist(Method method, Set<String> sourceProperties,
Set<String> targetProperties) {
for ( Mapping mappedProperty : method.getMappings().values() ) {
if ( !sourceProperties.contains( mappedProperty.getSourceName() ) ) {
printMessage(
ReportingPolicy.ERROR,
String.format(
"Unknown property \"%s\" in parameter type %s.",
mappedProperty.getSourceName(),
method.getSourceType()
), method.getExecutable(), mappedProperty.getMirror(), mappedProperty.getSourceAnnotationValue()
);
}
if ( !targetProperties.contains( mappedProperty.getTargetName() ) ) {
printMessage(
ReportingPolicy.ERROR,
String.format(
"Unknown property \"%s\" in return type %s.",
mappedProperty.getTargetName(),
method.getTargetType()
), method.getExecutable(), mappedProperty.getMirror(), mappedProperty.getTargetAnnotationValue()
);
}
}
}
private PropertyMapping getPropertyMapping(List<Method> methods, Method method, ExecutableElement getterMethod,
ExecutableElement setterMethod) {
Type sourceType = retrieveReturnType( getterMethod );
Type targetType = retrieveParameter( setterMethod ).getType();
Type sourceType = executables.retrieveReturnType( getterMethod );
Type targetType = executables.retrieveParameter( setterMethod ).getType();
MappingMethodReference propertyMappingMethod = getMappingMethodReference( methods, sourceType, targetType );
Conversion conversion = conversions.getConversion(
@ -301,10 +313,10 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
PropertyMapping property = new PropertyMapping(
method.getParameterName(),
Introspector.decapitalize( method.getTargetType().getName() ),
Executables.getPropertyName( getterMethod ),
executables.getPropertyName( getterMethod ),
getterMethod.getSimpleName().toString(),
sourceType,
Executables.getPropertyName( setterMethod ),
executables.getPropertyName( setterMethod ),
setterMethod.getSimpleName().toString(),
targetType,
propertyMappingMethod,
@ -344,24 +356,35 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
);
}
private void reportErrorForUnmappedTargetPropertiesIfRequired(Method method,
ReportingPolicy unmappedTargetPolicy,
Set<String> targetProperties,
Set<String> mappedTargetProperties) {
private String getIterableConversionString(Conversions conversions, Type sourceElementType, Type targetElementType,
boolean isToConversion) {
Conversion conversion = conversions.getConversion( sourceElementType, targetElementType );
if ( targetProperties.size() > mappedTargetProperties.size() &&
unmappedTargetPolicy.requiresReport() ) {
targetProperties.removeAll( mappedTargetProperties );
printMessage(
unmappedTargetPolicy,
MessageFormat.format(
"Unmapped target {0,choice,1#property|1<properties}: \"{1}\"",
targetProperties.size(),
Strings.join( targetProperties, ", " )
),
method.getExecutable()
);
if ( conversion == null ) {
return null;
}
return conversion.to(
Introspector.decapitalize( sourceElementType.getName() ),
targetElementType
);
}
private MappingMethodReference getMappingMethodReference(Iterable<Method> methods, Type parameterType,
Type returnType) {
for ( Method oneMethod : methods ) {
if ( oneMethod.getSourceType().equals( parameterType ) && oneMethod.getTargetType().equals( returnType ) ) {
return new MappingMethodReference(
oneMethod.getDeclaringMapper(),
oneMethod.getName(),
oneMethod.getParameterName(),
oneMethod.getSourceType(),
oneMethod.getTargetType()
);
}
}
return null;
}
private void reportErrorIfPropertyCanNotBeMapped(Method method, PropertyMapping property) {
@ -390,239 +413,16 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
}
}
private String getIterableConversionString(Conversions conversions, Type sourceElementType, Type targetElementType,
boolean isToConversion) {
Conversion conversion = conversions.getConversion( sourceElementType, targetElementType );
if ( conversion == null ) {
return null;
}
return conversion.to(
Introspector.decapitalize( sourceElementType.getName() ),
targetElementType
);
}
private List<Type> getUsedMapperTypes(TypeElement element) {
List<Type> usedMapperTypes = new LinkedList<Type>();
MapperPrism mapperPrism = MapperPrism.getInstanceOn( element );
for ( TypeMirror usedMapper : mapperPrism.uses() ) {
usedMapperTypes.add( typeUtil.retrieveType( usedMapper ) );
}
return usedMapperTypes;
}
private MappingMethodReference getMappingMethodReference(Iterable<Method> methods, Type parameterType,
Type returnType) {
for ( Method oneMethod : methods ) {
if ( oneMethod.getSourceType().equals( parameterType ) && oneMethod.getTargetType().equals( returnType ) ) {
return new MappingMethodReference(
oneMethod.getDeclaringMapper(),
oneMethod.getName(),
oneMethod.getParameterName(),
oneMethod.getSourceType(),
oneMethod.getTargetType()
);
}
}
return null;
}
private Method getReverseMappingMethod(List<Method> rawMethods, Method method) {
for ( Method oneMethod : rawMethods ) {
if ( oneMethod.reverses( method ) ) {
return oneMethod;
}
}
return null;
}
/**
* Retrieves the mapping methods declared by the given mapper type.
*
* @param element The type of interest
* @param implementationRequired Whether an implementation of this type must be generated or
* not. {@code true} if the type is the currently processed
* mapper interface, {@code false} if the given type is one
* referred to via {@code Mapper#uses()}.
*
* @return All mapping methods declared by the given type
*/
private List<Method> retrieveMethods(TypeElement element, boolean implementationRequired) {
List<Method> methods = new ArrayList<Method>();
MapperPrism mapperPrism = implementationRequired ? MapperPrism.getInstanceOn( element ) : null;
//TODO Extract to separate method
for ( ExecutableElement method : methodsIn( element.getEnclosedElements() ) ) {
Parameter parameter = retrieveParameter( method );
Type returnType = retrieveReturnType( method );
boolean mappingErroneous = false;
if ( implementationRequired ) {
if ( parameter.getType().isIterableType() && !returnType.isIterableType() ) {
printMessage(
ReportingPolicy.ERROR,
"Can't generate mapping method from iterable type to non-iterable type.",
method
);
mappingErroneous = true;
}
if ( !parameter.getType().isIterableType() && returnType.isIterableType() ) {
printMessage(
ReportingPolicy.ERROR,
"Can't generate mapping method from non-iterable type to iterable type.",
method
);
mappingErroneous = true;
}
if ( parameter.getType().isPrimitive() ) {
printMessage(
ReportingPolicy.ERROR,
"Can't generate mapping method with primitive parameter type.",
method
);
mappingErroneous = true;
}
if ( returnType.isPrimitive() ) {
printMessage(
ReportingPolicy.ERROR,
"Can't generate mapping method with primitive return type.",
method
);
mappingErroneous = true;
}
if ( mappingErroneous ) {
continue;
}
}
//add method with property mappings if an implementation needs to be generated
if ( implementationRequired ) {
methods.add(
Method.forMethodRequiringImplementation(
method,
parameter.getName(),
parameter.getType(),
returnType,
getMappings( method )
)
);
}
//otherwise add reference to existing mapper method
else {
methods.add(
Method.forReferencedMethod(
typeUtil.getType( typeUtils.getDeclaredType( element ) ),
method,
parameter.getName(),
parameter.getType(),
returnType
)
);
}
}
//Add all methods of used mappers in order to reference them in the aggregated model
if ( implementationRequired ) {
for ( TypeMirror usedMapper : mapperPrism.uses() ) {
methods.addAll(
retrieveMethods(
(TypeElement) ( (DeclaredType) usedMapper ).asElement(),
false
)
);
}
}
return methods;
}
private void reportErrorIfMappedPropertiesDontExist(Method method, Set<String> sourceProperties,
Set<String> targetProperties) {
for ( Mapping mappedProperty : method.getMappings().values() ) {
if ( !sourceProperties.contains( mappedProperty.getSourceName() ) ) {
printMessage(
ReportingPolicy.ERROR,
String.format(
"Unknown property \"%s\" in parameter type %s.",
mappedProperty.getSourceName(),
method.getSourceType()
), method.getExecutable(), mappedProperty.getMirror(), mappedProperty.getSourceAnnotationValue()
);
}
if ( !targetProperties.contains( mappedProperty.getTargetName() ) ) {
printMessage(
ReportingPolicy.ERROR,
String.format(
"Unknown property \"%s\" in return type %s.",
mappedProperty.getTargetName(),
method.getTargetType()
), method.getExecutable(), mappedProperty.getMirror(), mappedProperty.getTargetAnnotationValue()
);
}
}
}
/**
* Retrieves the mappings configured via {@code @Mapping} from the given
* method.
*
* @param method The method of interest
*
* @return The mappings for the given method, keyed by source property name
*/
private Map<String, Mapping> getMappings(ExecutableElement method) {
Map<String, Mapping> mappings = new HashMap<String, Mapping>();
MappingPrism mappingAnnotation = MappingPrism.getInstanceOn( method );
MappingsPrism mappingsAnnotation = MappingsPrism.getInstanceOn( method );
if ( mappingAnnotation != null ) {
mappings.put( mappingAnnotation.source(), Mapping.fromMappingPrism( mappingAnnotation ) );
}
if ( mappingsAnnotation != null ) {
mappings.putAll( Mapping.fromMappingsPrism( mappingsAnnotation ) );
}
return mappings;
}
private Parameter retrieveParameter(ExecutableElement method) {
List<? extends VariableElement> parameters = method.getParameters();
if ( parameters.size() != 1 ) {
//TODO: Log error
return null;
}
VariableElement parameter = parameters.get( 0 );
return new Parameter(
parameter.getSimpleName().toString(),
typeUtil.retrieveType( parameter.asType() )
);
}
private Type retrieveReturnType(ExecutableElement method) {
return typeUtil.retrieveType( method.getReturnType() );
}
private void printMessage(ReportingPolicy reportingPolicy, String message, Element element) {
processingEnvironment.getMessager().printMessage( reportingPolicy.getDiagnosticKind(), message, element );
messager.printMessage( reportingPolicy.getDiagnosticKind(), message, element );
if ( reportingPolicy.failsBuild() ) {
mappingErroneous = true;
isErroneous = true;
}
}
private void printMessage(ReportingPolicy reportingPolicy, String message, Element element,
AnnotationMirror annotationMirror, AnnotationValue annotationValue) {
processingEnvironment.getMessager()
messager
.printMessage(
reportingPolicy.getDiagnosticKind(),
message,
@ -631,7 +431,7 @@ public class MapperGenerationVisitor extends ElementKindVisitor6<Void, Void> {
annotationValue
);
if ( reportingPolicy.failsBuild() ) {
mappingErroneous = true;
isErroneous = true;
}
}
}

View File

@ -0,0 +1,58 @@
/**
* Copyright 2012-2013 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.processor;
import java.io.IOException;
import javax.annotation.processing.Filer;
import javax.lang.model.element.TypeElement;
import javax.tools.JavaFileObject;
import org.mapstruct.ap.model.Mapper;
import org.mapstruct.ap.writer.ModelWriter;
/**
* A {@link ModelElementProcessor} which creates a Java source file representing
* the given {@link Mapper} object.
*
* @author Gunnar Morling
*/
public class MapperRenderingProcessor implements ModelElementProcessor<Mapper, Void> {
@Override
public Void process(ProcessorContext context, TypeElement mapperTypeElement, Mapper sourceElement) {
if ( !sourceElement.isErroneous() ) {
writeToSourceFile( context.getFiler(), sourceElement );
}
return null;
}
private void writeToSourceFile(Filer filer, Mapper model) {
String fileName = model.getPackageName() + "." + model.getImplementationName();
JavaFileObject sourceFile;
try {
sourceFile = filer.createSourceFile( fileName );
}
catch ( IOException e ) {
throw new RuntimeException( e );
}
new ModelWriter().writeModel( sourceFile, model );
}
}

View File

@ -0,0 +1,202 @@
/**
* Copyright 2012-2013 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.processor;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.processing.Messager;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import org.mapstruct.ap.MapperPrism;
import org.mapstruct.ap.MappingPrism;
import org.mapstruct.ap.MappingsPrism;
import org.mapstruct.ap.model.ReportingPolicy;
import org.mapstruct.ap.model.Type;
import org.mapstruct.ap.model.source.Mapping;
import org.mapstruct.ap.model.source.Method;
import org.mapstruct.ap.model.source.Parameter;
import org.mapstruct.ap.util.Executables;
import org.mapstruct.ap.util.TypeUtil;
import static javax.lang.model.util.ElementFilter.methodsIn;
/**
* A {@link ModelElementProcessor} which retrieves a list of {@link Method}s
* representing all the mapping methods of the given bean mapper type as well as
* all referenced mapper methods declared by other mappers referenced by the
* current mapper.
*
* @author Gunnar Morling
*/
public class MethodRetrievalProcessor implements ModelElementProcessor<TypeElement, List<Method>> {
private Types typeUtils;
private Messager messager;
private TypeUtil typeUtil;
private Executables executables;
@Override
public List<Method> process(ProcessorContext context, TypeElement mapperTypeElement, TypeElement sourceElement) {
this.typeUtils = context.getTypeUtils();
this.messager = context.getMessager();
this.typeUtil = new TypeUtil( context.getElementUtils(), typeUtils );
this.executables = new Executables( typeUtil );
return retrieveMethods( mapperTypeElement, true );
}
/**
* Retrieves the mapping methods declared by the given mapper type.
*
* @param element The type of interest
* @param implementationRequired Whether an implementation of this type must be generated or
* not. {@code true} if the type is the currently processed
* mapper interface, {@code false} if the given type is one
* referred to via {@code Mapper#uses()}.
*
* @return All mapping methods declared by the given type
*/
private List<Method> retrieveMethods(TypeElement element, boolean implementationRequired) {
List<Method> methods = new ArrayList<Method>();
MapperPrism mapperPrism = implementationRequired ? MapperPrism.getInstanceOn( element ) : null;
//TODO Extract to separate method
for ( ExecutableElement method : methodsIn( element.getEnclosedElements() ) ) {
Parameter parameter = executables.retrieveParameter( method );
Type returnType = executables.retrieveReturnType( method );
boolean mappingErroneous = false;
if ( implementationRequired ) {
if ( parameter.getType().isIterableType() && !returnType.isIterableType() ) {
printMessage(
ReportingPolicy.ERROR,
"Can't generate mapping method from iterable type to non-iterable type.",
method
);
mappingErroneous = true;
}
if ( !parameter.getType().isIterableType() && returnType.isIterableType() ) {
printMessage(
ReportingPolicy.ERROR,
"Can't generate mapping method from non-iterable type to iterable type.",
method
);
mappingErroneous = true;
}
if ( parameter.getType().isPrimitive() ) {
printMessage(
ReportingPolicy.ERROR,
"Can't generate mapping method with primitive parameter type.",
method
);
mappingErroneous = true;
}
if ( returnType.isPrimitive() ) {
printMessage(
ReportingPolicy.ERROR,
"Can't generate mapping method with primitive return type.",
method
);
mappingErroneous = true;
}
if ( mappingErroneous ) {
continue;
}
}
//add method with property mappings if an implementation needs to be generated
if ( implementationRequired ) {
methods.add(
Method.forMethodRequiringImplementation(
method,
parameter.getName(),
parameter.getType(),
returnType,
getMappings( method )
)
);
}
//otherwise add reference to existing mapper method
else {
methods.add(
Method.forReferencedMethod(
typeUtil.getType( typeUtils.getDeclaredType( element ) ),
method,
parameter.getName(),
parameter.getType(),
returnType
)
);
}
}
//Add all methods of used mappers in order to reference them in the aggregated model
if ( implementationRequired ) {
for ( TypeMirror usedMapper : mapperPrism.uses() ) {
methods.addAll(
retrieveMethods(
(TypeElement) ( (DeclaredType) usedMapper ).asElement(),
false
)
);
}
}
return methods;
}
/**
* Retrieves the mappings configured via {@code @Mapping} from the given
* method.
*
* @param method The method of interest
*
* @return The mappings for the given method, keyed by source property name
*/
private Map<String, Mapping> getMappings(ExecutableElement method) {
Map<String, Mapping> mappings = new HashMap<String, Mapping>();
MappingPrism mappingAnnotation = MappingPrism.getInstanceOn( method );
MappingsPrism mappingsAnnotation = MappingsPrism.getInstanceOn( method );
if ( mappingAnnotation != null ) {
mappings.put( mappingAnnotation.source(), Mapping.fromMappingPrism( mappingAnnotation ) );
}
if ( mappingsAnnotation != null ) {
mappings.putAll( Mapping.fromMappingsPrism( mappingsAnnotation ) );
}
return mappings;
}
private void printMessage(ReportingPolicy reportingPolicy, String message, Element element) {
messager.printMessage( reportingPolicy.getDiagnosticKind(), message, element );
}
}

View File

@ -0,0 +1,80 @@
/**
* Copyright 2012-2013 Gunnar Morling (http://www.gunnarmorling.de/)
* and/or other contributors as indicated by the @authors tag. See the
* copyright.txt file in the distribution for a full listing of all
* contributors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.mapstruct.ap.processor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Messager;
import javax.lang.model.element.TypeElement;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import org.mapstruct.ap.model.Options;
/**
* A processor which performs one task of the mapper generation, e.g. retrieving
* methods from the source {@link TypeElement}, performing validity checks or
* generating the output source file.
*
* @param <P> The parameter type processed by this processor
* @param <R> The return type created by this processor
*
* @author Gunnar Morling
*/
public interface ModelElementProcessor<P, R> {
/**
* Context object passed to
* {@link ModelElementProcessor#process(ProcessorContext, TypeElement, Object)}
* providing access to common infrastructure objects such as {@link Types}
* etc.
*
* @author Gunnar Morling
*/
public interface ProcessorContext {
Filer getFiler();
Types getTypeUtils();
Elements getElementUtils();
Messager getMessager();
Options getOptions();
}
/**
* Processes the given source element, representing a Java bean mapper in
* one form or another.
*
* @param context Context providing common infrastructure objects.
* @param mapperTypeElement The original type element from which the given mapper object
* is derived.
* @param sourceElement The current representation of the bean mapper. Never
* {@code null} (the very first processor receives the original
* type element).
*
* @return The resulting representation of the bean mapper; may be the same
* as the source representation, e.g. if a given implementation just
* performs some sort of validity check. Implementations must never
* return {@code null} except for the very last processor which
* generates the resulting Java source file.
*/
R process(ProcessorContext context, TypeElement mapperTypeElement, P sourceElement);
}

View File

@ -23,8 +23,12 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeKind;
import org.mapstruct.ap.model.Type;
import org.mapstruct.ap.model.source.Parameter;
/**
* Provides functionality around {@link ExecutableElement}s.
*
@ -32,14 +36,17 @@ import javax.lang.model.type.TypeKind;
*/
public class Executables {
private Executables() {
private final TypeUtil typeUtil;
public Executables(TypeUtil typeUtil) {
this.typeUtil = typeUtil;
}
public static boolean isGetterMethod(ExecutableElement method) {
public boolean isGetterMethod(ExecutableElement method) {
return isNonBooleanGetterMethod( method ) || isBooleanGetterMethod( method );
}
private static boolean isNonBooleanGetterMethod(ExecutableElement method) {
private boolean isNonBooleanGetterMethod(ExecutableElement method) {
String name = method.getSimpleName().toString();
return method.getParameters().isEmpty() &&
@ -48,7 +55,7 @@ public class Executables {
method.getReturnType().getKind() != TypeKind.VOID;
}
private static boolean isBooleanGetterMethod(ExecutableElement method) {
private boolean isBooleanGetterMethod(ExecutableElement method) {
String name = method.getSimpleName().toString();
return method.getParameters().isEmpty() &&
@ -57,7 +64,7 @@ public class Executables {
method.getReturnType().getKind() == TypeKind.BOOLEAN;
}
public static boolean isSetterMethod(ExecutableElement method) {
public boolean isSetterMethod(ExecutableElement method) {
String name = method.getSimpleName().toString();
if ( name.startsWith( "set" ) && name.length() > 3 && method.getParameters()
@ -68,7 +75,7 @@ public class Executables {
return false;
}
public static String getPropertyName(ExecutableElement getterOrSetterMethod) {
public String getPropertyName(ExecutableElement getterOrSetterMethod) {
if ( isNonBooleanGetterMethod( getterOrSetterMethod ) ) {
return Introspector.decapitalize(
getterOrSetterMethod.getSimpleName().toString().substring( 3 )
@ -88,13 +95,33 @@ public class Executables {
throw new IllegalArgumentException( "Executable " + getterOrSetterMethod + " is not getter or setter method." );
}
public static Set<String> getPropertyNames(List<ExecutableElement> propertyAccessors) {
public Set<String> getPropertyNames(List<ExecutableElement> propertyAccessors) {
Set<String> propertyNames = new HashSet<String>();
for ( ExecutableElement executableElement : propertyAccessors ) {
propertyNames.add( Executables.getPropertyName( executableElement ) );
propertyNames.add( getPropertyName( executableElement ) );
}
return propertyNames;
}
public Parameter retrieveParameter(ExecutableElement method) {
List<? extends VariableElement> parameters = method.getParameters();
if ( parameters.size() != 1 ) {
//TODO: Log error
return null;
}
VariableElement parameter = parameters.get( 0 );
return new Parameter(
parameter.getSimpleName().toString(),
typeUtil.retrieveType( parameter.asType() )
);
}
public Type retrieveReturnType(ExecutableElement method) {
return typeUtil.retrieveType( method.getReturnType() );
}
}

View File

@ -32,6 +32,9 @@ import static javax.lang.model.util.ElementFilter.methodsIn;
*/
public class Filters {
//TODO
private static Executables executables = new Executables( null );
private Filters() {
}
@ -39,7 +42,7 @@ public class Filters {
List<ExecutableElement> getterMethods = new LinkedList<ExecutableElement>();
for ( ExecutableElement method : methodsIn( elements ) ) {
if ( Executables.isGetterMethod( method ) ) {
if ( executables.isGetterMethod( method ) ) {
getterMethods.add( method );
}
}
@ -51,7 +54,7 @@ public class Filters {
List<ExecutableElement> setterMethods = new LinkedList<ExecutableElement>();
for ( ExecutableElement method : methodsIn( elements ) ) {
if ( Executables.isSetterMethod( method ) ) {
if ( executables.isSetterMethod( method ) ) {
setterMethods.add( method );
}
}