diff --git a/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java b/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java index 60715995e..e3f42776a 100644 --- a/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java +++ b/processor/src/main/java/org/mapstruct/ap/MappingProcessor.java @@ -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}. + *

+ * Implementation notes: + *

+ * 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. + *

+ * The model instantiation and processing happens in several phases/passes by applying + * a sequence of {@link ModelElementProcessor}s. + *

+ * For reading annotation attributes, prisms as generated with help of the Hickory 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 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 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 R process(ProcessorContext context, ModelElementProcessor processor, + TypeElement mapperTypeElement, Object modelElement) { + @SuppressWarnings("unchecked") + P sourceElement = (P) modelElement; + return processor.process( context, mapperTypeElement, sourceElement ); + } + + //TODO Retrieve via service loader + private Iterable> getProcessors() { + return Arrays.>asList( + new MethodRetrievalProcessor(), + new MapperCreationProcessor(), + new MapperRenderingProcessor() + ); + } + + private TypeElement asTypeElement(Element element) { + return element.accept( + new ElementKindVisitor6() { + @Override + public TypeElement visitTypeAsInterface(TypeElement e, Void p) { + return e; + } + + }, null + ); + } } diff --git a/processor/src/main/java/org/mapstruct/ap/model/SimpleMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java similarity index 88% rename from processor/src/main/java/org/mapstruct/ap/model/SimpleMappingMethod.java rename to processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java index 088104f99..619ac6e82 100644 --- a/processor/src/main/java/org/mapstruct/ap/model/SimpleMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java @@ -28,12 +28,12 @@ import java.util.Set; * * @author Gunnar Morling */ -public class SimpleMappingMethod extends MappingMethod { +public class BeanMappingMethod extends MappingMethod { private final List propertyMappings; - public SimpleMappingMethod(String name, String parameterName, Type sourceType, Type targetType, - List propertyMappings) { + public BeanMappingMethod(String name, String parameterName, Type sourceType, Type targetType, + List propertyMappings) { super( name, parameterName, sourceType, targetType ); this.propertyMappings = propertyMappings; } diff --git a/processor/src/main/java/org/mapstruct/ap/model/Mapper.java b/processor/src/main/java/org/mapstruct/ap/model/Mapper.java index 83db622cc..d330504b1 100644 --- a/processor/src/main/java/org/mapstruct/ap/model/Mapper.java +++ b/processor/src/main/java/org/mapstruct/ap/model/Mapper.java @@ -33,10 +33,11 @@ public class Mapper extends AbstractModelElement { private final List usedMapperTypes; private final Options options; private final SortedSet importedTypes; + private final boolean isErroneous; public Mapper(String packageName, String interfaceName, String implementationName, List mappingMethods, List 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 determineImportedTypes() { @@ -122,4 +124,8 @@ public class Mapper extends AbstractModelElement { public SortedSet getImportedTypes() { return importedTypes; } + + public boolean isErroneous() { + return isErroneous; + } } diff --git a/processor/src/main/java/org/mapstruct/ap/model/Type.java b/processor/src/main/java/org/mapstruct/ap/model/Type.java index 07d726838..5068fda49 100644 --- a/processor/src/main/java/org/mapstruct/ap/model/Type.java +++ b/processor/src/main/java/org/mapstruct/ap/model/Type.java @@ -83,6 +83,10 @@ public class Type implements Comparable { 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; diff --git a/processor/src/main/java/org/mapstruct/ap/processor/DefaultModelElementProcessorContext.java b/processor/src/main/java/org/mapstruct/ap/processor/DefaultModelElementProcessorContext.java new file mode 100644 index 000000000..a7e3019ae --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/processor/DefaultModelElementProcessorContext.java @@ -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; + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/MapperGenerationVisitor.java b/processor/src/main/java/org/mapstruct/ap/processor/MapperCreationProcessor.java similarity index 58% rename from processor/src/main/java/org/mapstruct/ap/MapperGenerationVisitor.java rename to processor/src/main/java/org/mapstruct/ap/processor/MapperCreationProcessor.java index 6eac511b0..185012996 100644 --- a/processor/src/main/java/org/mapstruct/ap/MapperGenerationVisitor.java +++ b/processor/src/main/java/org/mapstruct/ap/processor/MapperCreationProcessor.java @@ -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}. - *

- * Implementation notes: - *

- * 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. - *

- * The model instantiation happens in two phases/passes: The first one retrieves - * the mapping methods of the given interfaces and their configuration (the - * source model). In the second pass the individual methods are - * aggregated into the target 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. - *

- * For reading annotation attributes, prisms as generated with help of the Hickory 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 { +public class MapperCreationProcessor implements ModelElementProcessor, 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 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 methods = retrieveMethods( element, true ); - - //2.) build up aggregated "target" model - List mappings = getMappingMethods( - methods, - getEffectiveUnmappedTargetPolicy( element ) - ); + public Mapper getMapper(TypeElement element, List methods) { List 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 { } } + private List getUsedMapperTypes(TypeElement element) { + List usedMapperTypes = new LinkedList(); + MapperPrism mapperPrism = MapperPrism.getInstanceOn( element ); + for ( TypeMirror usedMapper : mapperPrism.uses() ) { + usedMapperTypes.add( typeUtil.retrieveType( usedMapper ) ); + } + return usedMapperTypes; + } + private List getMappingMethods(List methods, ReportingPolicy unmappedTargetPolicy) { List mappingMethods = new ArrayList(); @@ -203,7 +160,7 @@ public class MapperGenerationVisitor extends ElementKindVisitor6 { 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 { return reversed; } - private MappingMethod getSimpleMappingMethod(List methods, Method method, - ReportingPolicy unmappedTargetPolicy) { + private MappingMethod getBeanMappingMethod(List methods, Method method, + ReportingPolicy unmappedTargetPolicy) { List propertyMappings = new ArrayList(); Set mappedTargetProperties = new HashSet(); @@ -240,21 +197,21 @@ public class MapperGenerationVisitor extends ElementKindVisitor6 { elementUtils.getAllMembers( returnTypeElement ) ); - Set sourceProperties = Executables.getPropertyNames( + Set sourceProperties = executables.getPropertyNames( Filters.getterMethodsIn( sourceGetters ) ); - Set targetProperties = Executables.getPropertyNames( + Set 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 { mappedTargetProperties ); - return new SimpleMappingMethod( + return new BeanMappingMethod( method.getName(), method.getParameterName(), method.getSourceType(), @@ -287,10 +244,65 @@ public class MapperGenerationVisitor extends ElementKindVisitor6 { ); } + private void reportErrorForUnmappedTargetPropertiesIfRequired(Method method, + ReportingPolicy unmappedTargetPolicy, + Set targetProperties, + Set mappedTargetProperties) { + + if ( targetProperties.size() > mappedTargetProperties.size() && + unmappedTargetPolicy.requiresReport() ) { + targetProperties.removeAll( mappedTargetProperties ); + printMessage( + unmappedTargetPolicy, + MessageFormat.format( + "Unmapped target {0,choice,1#property|1 rawMethods, Method method) { + for ( Method oneMethod : rawMethods ) { + if ( oneMethod.reverses( method ) ) { + return oneMethod; + } + } + return null; + } + + private void reportErrorIfMappedPropertiesDontExist(Method method, Set sourceProperties, + Set 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 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 { 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 { ); } - private void reportErrorForUnmappedTargetPropertiesIfRequired(Method method, - ReportingPolicy unmappedTargetPolicy, - Set targetProperties, - Set 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 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 { } } - 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 getUsedMapperTypes(TypeElement element) { - List usedMapperTypes = new LinkedList(); - MapperPrism mapperPrism = MapperPrism.getInstanceOn( element ); - for ( TypeMirror usedMapper : mapperPrism.uses() ) { - usedMapperTypes.add( typeUtil.retrieveType( usedMapper ) ); - } - return usedMapperTypes; - } - - private MappingMethodReference getMappingMethodReference(Iterable 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 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 retrieveMethods(TypeElement element, boolean implementationRequired) { - List methods = new ArrayList(); - - 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 sourceProperties, - Set 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 getMappings(ExecutableElement method) { - Map mappings = new HashMap(); - - 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 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 { annotationValue ); if ( reportingPolicy.failsBuild() ) { - mappingErroneous = true; + isErroneous = true; } } } diff --git a/processor/src/main/java/org/mapstruct/ap/processor/MapperRenderingProcessor.java b/processor/src/main/java/org/mapstruct/ap/processor/MapperRenderingProcessor.java new file mode 100644 index 000000000..34056dbdb --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/processor/MapperRenderingProcessor.java @@ -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 { + + @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 ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/processor/MethodRetrievalProcessor.java b/processor/src/main/java/org/mapstruct/ap/processor/MethodRetrievalProcessor.java new file mode 100644 index 000000000..34604ba97 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/processor/MethodRetrievalProcessor.java @@ -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> { + + private Types typeUtils; + private Messager messager; + private TypeUtil typeUtil; + private Executables executables; + + @Override + public List 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 retrieveMethods(TypeElement element, boolean implementationRequired) { + List methods = new ArrayList(); + + 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 getMappings(ExecutableElement method) { + Map mappings = new HashMap(); + + 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 ); + } +} diff --git a/processor/src/main/java/org/mapstruct/ap/processor/ModelElementProcessor.java b/processor/src/main/java/org/mapstruct/ap/processor/ModelElementProcessor.java new file mode 100644 index 000000000..cd9872544 --- /dev/null +++ b/processor/src/main/java/org/mapstruct/ap/processor/ModelElementProcessor.java @@ -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

The parameter type processed by this processor + * @param The return type created by this processor + * + * @author Gunnar Morling + */ +public interface ModelElementProcessor { + + /** + * 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); +} diff --git a/processor/src/main/java/org/mapstruct/ap/util/Executables.java b/processor/src/main/java/org/mapstruct/ap/util/Executables.java index 440db5c8a..47115fa8f 100644 --- a/processor/src/main/java/org/mapstruct/ap/util/Executables.java +++ b/processor/src/main/java/org/mapstruct/ap/util/Executables.java @@ -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 getPropertyNames(List propertyAccessors) { + public Set getPropertyNames(List propertyAccessors) { Set propertyNames = new HashSet(); for ( ExecutableElement executableElement : propertyAccessors ) { - propertyNames.add( Executables.getPropertyName( executableElement ) ); + propertyNames.add( getPropertyName( executableElement ) ); } return propertyNames; } + + public Parameter retrieveParameter(ExecutableElement method) { + List 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() ); + } } diff --git a/processor/src/main/java/org/mapstruct/ap/util/Filters.java b/processor/src/main/java/org/mapstruct/ap/util/Filters.java index 9f675f7bd..ba3187a4e 100644 --- a/processor/src/main/java/org/mapstruct/ap/util/Filters.java +++ b/processor/src/main/java/org/mapstruct/ap/util/Filters.java @@ -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 getterMethods = new LinkedList(); for ( ExecutableElement method : methodsIn( elements ) ) { - if ( Executables.isGetterMethod( method ) ) { + if ( executables.isGetterMethod( method ) ) { getterMethods.add( method ); } } @@ -51,7 +54,7 @@ public class Filters { List setterMethods = new LinkedList(); for ( ExecutableElement method : methodsIn( elements ) ) { - if ( Executables.isSetterMethod( method ) ) { + if ( executables.isSetterMethod( method ) ) { setterMethods.add( method ); } } diff --git a/processor/src/main/resources/org.mapstruct.ap.model.SimpleMappingMethod.ftl b/processor/src/main/resources/org.mapstruct.ap.model.BeanMappingMethod.ftl similarity index 100% rename from processor/src/main/resources/org.mapstruct.ap.model.SimpleMappingMethod.ftl rename to processor/src/main/resources/org.mapstruct.ap.model.BeanMappingMethod.ftl