org.mapstruct.ap.MappingProcessor Maven / Gradle / Ivy
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.stream.Collectors;
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;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Name;
import javax.lang.model.element.QualifiedNameable;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementKindVisitor6;
import javax.tools.Diagnostic.Kind;
import org.mapstruct.ap.internal.model.Mapper;
import org.mapstruct.ap.internal.option.Options;
import org.mapstruct.ap.internal.gem.MapperGem;
import org.mapstruct.ap.internal.gem.ReportingPolicyGem;
import org.mapstruct.ap.internal.processor.DefaultModelElementProcessorContext;
import org.mapstruct.ap.internal.processor.ModelElementProcessor;
import org.mapstruct.ap.internal.processor.ModelElementProcessor.ProcessorContext;
import org.mapstruct.ap.internal.util.AnnotationProcessingException;
import org.mapstruct.ap.internal.util.AnnotationProcessorContext;
import org.mapstruct.ap.internal.util.RoundContext;
import org.mapstruct.ap.spi.TypeHierarchyErroneousException;
import static javax.lang.model.element.ElementKind.CLASS;
/**
* A JSR 269 annotation {@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.
*
* The model instantiation and processing happens in several phases/passes by applying a sequence of
* {@link ModelElementProcessor}s. The processors to apply are retrieved using the Java service loader mechanism and are
* processed in order of their {@link ModelElementProcessor#getPriority() priority}. The general processing flow is
* this:
*
* - retrieve mapping methods
* - create the {@code Mapper} model
* - perform enrichments and modifications (e.g. add annotations for dependency injection)
* - if no error occurred, write out the model into Java source files
*
*
* For reading annotation attributes, gems as generated with help of Gem Tools. These gems allow comfortable access to annotations and
* their attributes without depending on their class objects.
*
* The creation of Java source files is done using the FreeMarker template engine.
* Each node of the mapper model has a corresponding FreeMarker template file which provides the Java representation of
* that element and can include sub-elements via a custom FreeMarker directive. That way writing out a root node of the
* model ({@code Mapper}) will recursively include all contained sub-elements (such as its methods, their property
* mappings etc.).
*
* @author Gunnar Morling
*/
@SupportedAnnotationTypes("org.mapstruct.Mapper")
@SupportedOptions({
MappingProcessor.SUPPRESS_GENERATOR_TIMESTAMP,
MappingProcessor.SUPPRESS_GENERATOR_VERSION_INFO_COMMENT,
MappingProcessor.UNMAPPED_TARGET_POLICY,
MappingProcessor.UNMAPPED_SOURCE_POLICY,
MappingProcessor.DEFAULT_COMPONENT_MODEL,
MappingProcessor.DEFAULT_INJECTION_STRATEGY,
MappingProcessor.DISABLE_BUILDERS,
MappingProcessor.VERBOSE
})
public class MappingProcessor extends AbstractProcessor {
/**
* Whether this processor claims all processed annotations exclusively or not.
*/
private static final boolean ANNOTATIONS_CLAIMED_EXCLUSIVELY = false;
protected static final String SUPPRESS_GENERATOR_TIMESTAMP = "mapstruct.suppressGeneratorTimestamp";
protected static final String SUPPRESS_GENERATOR_VERSION_INFO_COMMENT =
"mapstruct.suppressGeneratorVersionInfoComment";
protected static final String UNMAPPED_TARGET_POLICY = "mapstruct.unmappedTargetPolicy";
protected static final String UNMAPPED_SOURCE_POLICY = "mapstruct.unmappedSourcePolicy";
protected static final String DEFAULT_COMPONENT_MODEL = "mapstruct.defaultComponentModel";
protected static final String DEFAULT_INJECTION_STRATEGY = "mapstruct.defaultInjectionStrategy";
protected static final String ALWAYS_GENERATE_SERVICE_FILE = "mapstruct.alwaysGenerateServicesFile";
protected static final String DISABLE_BUILDERS = "mapstruct.disableBuilders";
protected static final String VERBOSE = "mapstruct.verbose";
private Options options;
private AnnotationProcessorContext annotationProcessorContext;
/**
* Any mappers for which an implementation cannot be generated in the current round because they have source/target
* types with incomplete hierarchies (as super-types are to be generated by other processors). They will be
* processed in subsequent rounds.
*
* If the hierarchy of a mapper's source/target types is never completed (i.e. the missing super-types are not
* generated by other processors), this mapper will not be generated; That's fine, the compiler will raise an error
* due to the inconsistent Java types used as source or target anyways.
*/
private Set deferredMappers = new HashSet<>();
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init( processingEnv );
options = createOptions();
annotationProcessorContext = new AnnotationProcessorContext(
processingEnv.getElementUtils(),
processingEnv.getTypeUtils(),
processingEnv.getMessager(),
options.isDisableBuilders(),
options.isVerbose()
);
}
private Options createOptions() {
String unmappedTargetPolicy = processingEnv.getOptions().get( UNMAPPED_TARGET_POLICY );
String unmappedSourcePolicy = processingEnv.getOptions().get( UNMAPPED_SOURCE_POLICY );
return new Options(
Boolean.valueOf( processingEnv.getOptions().get( SUPPRESS_GENERATOR_TIMESTAMP ) ),
Boolean.valueOf( processingEnv.getOptions().get( SUPPRESS_GENERATOR_VERSION_INFO_COMMENT ) ),
unmappedTargetPolicy != null ? ReportingPolicyGem.valueOf( unmappedTargetPolicy.toUpperCase() ) : null,
unmappedSourcePolicy != null ? ReportingPolicyGem.valueOf( unmappedSourcePolicy.toUpperCase() ) : null,
processingEnv.getOptions().get( DEFAULT_COMPONENT_MODEL ),
processingEnv.getOptions().get( DEFAULT_INJECTION_STRATEGY ),
Boolean.valueOf( processingEnv.getOptions().get( ALWAYS_GENERATE_SERVICE_FILE ) ),
Boolean.valueOf( processingEnv.getOptions().get( DISABLE_BUILDERS ) ),
Boolean.valueOf( processingEnv.getOptions().get( VERBOSE ) )
);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latestSupported();
}
@Override
public boolean process(final Set extends TypeElement> annotations, final RoundEnvironment roundEnvironment) {
// nothing to do in the last round
if ( !roundEnvironment.processingOver() ) {
RoundContext roundContext = new RoundContext( annotationProcessorContext );
// process any mappers left over from previous rounds
Set deferredMappers = getAndResetDeferredMappers();
processMapperElements( deferredMappers, roundContext );
// get and process any mappers from this round
Set mappers = getMappers( annotations, roundEnvironment );
processMapperElements( mappers, roundContext );
}
else if ( !deferredMappers.isEmpty() ) {
// If the processing is over and there are deferred mappers it means something wrong occurred and
// MapStruct didn't generate implementations for those
for ( DeferredMapper deferredMapper : deferredMappers ) {
TypeElement deferredMapperElement = deferredMapper.deferredMapperElement;
Element erroneousElement = deferredMapper.erroneousElement;
String erroneousElementName;
if ( erroneousElement instanceof QualifiedNameable ) {
erroneousElementName = ( (QualifiedNameable) erroneousElement ).getQualifiedName().toString();
}
else {
erroneousElementName =
erroneousElement != null ? erroneousElement.getSimpleName().toString() : null;
}
// When running on Java 8 we need to fetch the deferredMapperElement again.
// Otherwise the reporting will not work properly
deferredMapperElement = annotationProcessorContext.getElementUtils()
.getTypeElement( deferredMapperElement.getQualifiedName() );
processingEnv.getMessager()
.printMessage(
Kind.ERROR,
"No implementation was created for " + deferredMapperElement.getSimpleName() +
" due to having a problem in the erroneous element " + erroneousElementName + "." +
" Hint: this often means that some other annotation processor was supposed to" +
" process the erroneous element. You can also enable MapStruct verbose mode by setting" +
" -Amapstruct.verbose=true as a compilation argument.",
deferredMapperElement
);
}
}
return ANNOTATIONS_CLAIMED_EXCLUSIVELY;
}
/**
* Gets fresh copies of all mappers deferred from previous rounds (the originals may contain references to
* erroneous source/target type elements).
*/
private Set getAndResetDeferredMappers() {
Set deferred = new HashSet<>( deferredMappers.size() );
for ( DeferredMapper deferredMapper : deferredMappers ) {
TypeElement element = deferredMapper.deferredMapperElement;
deferred.add( processingEnv.getElementUtils().getTypeElement( element.getQualifiedName() ) );
}
deferredMappers.clear();
return deferred;
}
private Set getMappers(final Set extends TypeElement> annotations,
final RoundEnvironment roundEnvironment) {
Set mapperTypes = new HashSet<>();
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;
}
try {
Set extends Element> annotatedMappers = roundEnvironment.getElementsAnnotatedWith( annotation );
for (Element mapperElement : annotatedMappers) {
TypeElement mapperTypeElement = asTypeElement( mapperElement );
// on some JDKs, RoundEnvironment.getElementsAnnotatedWith( ... ) returns types with
// annotations unknown to the compiler, even though they are not declared Mappers
if ( mapperTypeElement != null && MapperGem.instanceOn( mapperTypeElement ) != null ) {
mapperTypes.add( mapperTypeElement );
}
}
}
catch ( Throwable t ) { // whenever that may happen, but just to stay on the save side
handleUncaughtError( annotation, t );
continue;
}
}
return mapperTypes;
}
private void processMapperElements(Set mapperElements, RoundContext roundContext) {
for ( TypeElement mapperElement : mapperElements ) {
try {
// create a new context for each generated mapper in order to have imports of referenced types
// correctly managed;
// note that this assumes that a new source file is created for each mapper which must not
// necessarily be the case, e.g. in case of several mapper interfaces declared as inner types
// of one outer interface
List extends Element> tst = mapperElement.getEnclosedElements();
ProcessorContext context = new DefaultModelElementProcessorContext(
processingEnv,
options,
roundContext,
getDeclaredTypesNotToBeImported( mapperElement ),
mapperElement
);
processMapperTypeElement( context, mapperElement );
}
catch ( TypeHierarchyErroneousException thie ) {
TypeMirror erroneousType = thie.getType();
Element erroneousElement = erroneousType != null ? roundContext.getAnnotationProcessorContext()
.getTypeUtils()
.asElement( erroneousType ) : null;
if ( options.isVerbose() ) {
processingEnv.getMessager().printMessage(
Kind.NOTE, "MapStruct: referred types not available (yet), deferring mapper: "
+ mapperElement );
}
deferredMappers.add( new DeferredMapper( mapperElement, erroneousElement ) );
}
catch ( Throwable t ) {
handleUncaughtError( mapperElement, t );
break;
}
}
}
private Map getDeclaredTypesNotToBeImported(TypeElement element) {
return element.getEnclosedElements().stream()
.filter( e -> CLASS.equals( e.getKind() ) )
.map( Element::getSimpleName )
.map( Name::toString )
.collect( Collectors.toMap( k -> k, v -> element.getQualifiedName().toString() + "." + v ) );
}
private void handleUncaughtError(Element element, Throwable thrown) {
StringWriter sw = new StringWriter();
thrown.printStackTrace( new PrintWriter( sw ) );
String reportableStacktrace = sw.toString().replace( System.lineSeparator( ), " " );
processingEnv.getMessager().printMessage(
Kind.ERROR, "Internal error in the mapping processor: " + reportableStacktrace, element );
}
/**
* Applies all registered {@link ModelElementProcessor}s to the given mapper
* type.
*
* @param context The processor context.
* @param mapperTypeElement The mapper type element.
*/
private void processMapperTypeElement(ProcessorContext context, TypeElement mapperTypeElement) {
Object model = null;
for ( ModelElementProcessor, ?> processor : getProcessors() ) {
try {
model = process( context, processor, mapperTypeElement, model );
}
catch ( AnnotationProcessingException e ) {
processingEnv.getMessager()
.printMessage(
Kind.ERROR,
e.getMessage(),
e.getElement(),
e.getAnnotationMirror(),
e.getAnnotationValue()
);
break;
}
}
}
private R process(ProcessorContext context, ModelElementProcessor
processor,
TypeElement mapperTypeElement, Object modelElement) {
@SuppressWarnings("unchecked")
P sourceElement = (P) modelElement;
return processor.process( context, mapperTypeElement, sourceElement );
}
/**
* Retrieves all model element processors, ordered by their priority value
* (with the method retrieval processor having the lowest priority value (1)
* and the code generation processor the highest priority value.
*
* @return A list with all model element processors.
*/
private Iterable> getProcessors() {
// TODO Re-consider which class loader to use in case processors are
// loaded from other modules, too
@SuppressWarnings("rawtypes")
Iterator processorIterator = ServiceLoader.load(
ModelElementProcessor.class,
MappingProcessor.class.getClassLoader()
)
.iterator();
List> processors = new ArrayList<>();
while ( processorIterator.hasNext() ) {
processors.add( processorIterator.next() );
}
Collections.sort( processors, new ProcessorComparator() );
return processors;
}
private TypeElement asTypeElement(Element element) {
return element.accept(
new ElementKindVisitor6() {
@Override
public TypeElement visitTypeAsInterface(TypeElement e, Void p) {
return e;
}
@Override
public TypeElement visitTypeAsClass(TypeElement e, Void p) {
return e;
}
}, null
);
}
private static class ProcessorComparator implements Comparator> {
@Override
public int compare(ModelElementProcessor, ?> o1, ModelElementProcessor, ?> o2) {
return Integer.compare( o1.getPriority(), o2.getPriority() );
}
}
private static class DeferredMapper {
private final TypeElement deferredMapperElement;
private final Element erroneousElement;
private DeferredMapper(TypeElement deferredMapperElement, Element erroneousElement) {
this.deferredMapperElement = deferredMapperElement;
this.erroneousElement = erroneousElement;
}
}
}