Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,55 @@ A nice example is to provide support for a custom transformation strategy.
----
include::{processor-ap-test}/value/nametransformation/CustomEnumTransformationStrategy.java[tag=documentation]
----
====

[[additional-supported-options-provider]]
=== Additional Supported Options Provider

SPI name: `org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider`

MapStruct offers the ability to pass through declared compiler args (or "options") provided to the MappingProcessor
to the individual SPIs, by implementing `AdditionalSupportedOptionsProvider` via the Service Provider Interface (SPI).

.Custom Additional Supported Options Provider that declares `myorg.custom.defaultNullEnumConstant` as an option to pass through
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
include::{processor-ap-test}/additionalsupportedoptions/CustomAdditionalSupportedOptionsProvider.java[tag=documentation]
----
====

The value of this option is provided by including an `arg` to the `compilerArgs` tag when defining your custom SPI
implementation.

.Example maven configuration with additional options
====
[source, maven, linenums]
[subs="verbatim,attributes"]
----
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.myorg</groupId>
<artifactId>custom-spi-impl</artifactId>
<version>${project.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amyorg.custom.defaultNullEnumConstant=MISSING</arg>
</compilerArgs>
</configuration>
----
====

Your custom SPI implementations can then access this configured value via `MapStructProcessingEnvironment#getOptions()`.

.Accessing your custom options
====
[source, java, linenums]
[subs="verbatim,attributes"]
----
include::{processor-ap-test}/additionalsupportedoptions/UnknownEnumMappingStrategy.java[tag=documentation]
----
====
84 changes: 81 additions & 3 deletions processor/src/main/java/org/mapstruct/ap/MappingProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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;
Expand All @@ -33,17 +34,19 @@
import javax.lang.model.util.ElementKindVisitor6;
import javax.tools.Diagnostic.Kind;

import org.mapstruct.ap.internal.gem.MapperGem;
import org.mapstruct.ap.internal.gem.NullValueMappingStrategyGem;
import org.mapstruct.ap.internal.gem.ReportingPolicyGem;
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.internal.util.Services;
import org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider;
import org.mapstruct.ap.spi.TypeHierarchyErroneousException;

import static javax.lang.model.element.ElementKind.CLASS;
Expand Down Expand Up @@ -113,6 +116,9 @@ public class MappingProcessor extends AbstractProcessor {
protected static final String NULL_VALUE_ITERABLE_MAPPING_STRATEGY = "mapstruct.nullValueIterableMappingStrategy";
protected static final String NULL_VALUE_MAP_MAPPING_STRATEGY = "mapstruct.nullValueMapMappingStrategy";

private final Set<String> additionalSupportedOptions;
private final String additionalSupportedOptionsError;

private Options options;

private AnnotationProcessorContext annotationProcessorContext;
Expand All @@ -128,6 +134,21 @@ public class MappingProcessor extends AbstractProcessor {
*/
private Set<DeferredMapper> deferredMappers = new HashSet<>();

public MappingProcessor() {
Set<String> additionalSupportedOptions;
String additionalSupportedOptionsError;
try {
additionalSupportedOptions = resolveAdditionalSupportedOptions();
additionalSupportedOptionsError = null;
}
catch ( IllegalStateException ex ) {
additionalSupportedOptions = Collections.emptySet();
additionalSupportedOptionsError = ex.getMessage();
}
this.additionalSupportedOptions = additionalSupportedOptions;
this.additionalSupportedOptionsError = additionalSupportedOptionsError;
}

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init( processingEnv );
Expand All @@ -138,8 +159,13 @@ public synchronized void init(ProcessingEnvironment processingEnv) {
processingEnv.getTypeUtils(),
processingEnv.getMessager(),
options.isDisableBuilders(),
options.isVerbose()
options.isVerbose(),
resolveAdditionalOptions( processingEnv.getOptions() )
);

if ( additionalSupportedOptionsError != null ) {
processingEnv.getMessager().printMessage( Kind.ERROR, additionalSupportedOptionsError );
}
}

private Options createOptions() {
Expand Down Expand Up @@ -225,6 +251,17 @@ else if ( !deferredMappers.isEmpty() ) {
return ANNOTATIONS_CLAIMED_EXCLUSIVELY;
}

@Override
public Set<String> getSupportedOptions() {
Set<String> supportedOptions = super.getSupportedOptions();
if ( additionalSupportedOptions.isEmpty() ) {
return supportedOptions;
}
Set<String> allSupportedOptions = new HashSet<>( supportedOptions );
allSupportedOptions.addAll( additionalSupportedOptions );
return allSupportedOptions;
}

/**
* Gets fresh copies of all mappers deferred from previous rounds (the originals may contain references to
* erroneous source/target type elements).
Expand Down Expand Up @@ -407,6 +444,35 @@ public TypeElement visitTypeAsClass(TypeElement e, Void p) {
);
}

/**
* Fetch the additional supported options provided by the SPI {@link AdditionalSupportedOptionsProvider}.
*
* @return the additional supported options
*/
private static Set<String> resolveAdditionalSupportedOptions() {
Set<String> additionalSupportedOptions = null;
for ( AdditionalSupportedOptionsProvider optionsProvider :
Services.all( AdditionalSupportedOptionsProvider.class ) ) {
if ( additionalSupportedOptions == null ) {
additionalSupportedOptions = new HashSet<>();
}
Set<String> providerOptions = optionsProvider.getAdditionalSupportedOptions();

for ( String providerOption : providerOptions ) {
// Ensure additional options are not in the mapstruct namespace
if ( providerOption.startsWith( "mapstruct" ) ) {
throw new IllegalStateException(
"Additional SPI options cannot start with \"mapstruct\". Provider " + optionsProvider +
" provided option " + providerOption );
}
additionalSupportedOptions.add( providerOption );
}

}

return additionalSupportedOptions == null ? Collections.emptySet() : additionalSupportedOptions;
}

private static class ProcessorComparator implements Comparator<ModelElementProcessor<?, ?>> {

@Override
Expand All @@ -425,4 +491,16 @@ private DeferredMapper(TypeElement deferredMapperElement, Element erroneousEleme
this.erroneousElement = erroneousElement;
}
}

/**
* Filters only the options belonging to the declared additional supported options.
*
* @param options all processor environment options
* @return filtered options
*/
private Map<String, String> resolveAdditionalOptions(Map<String, String> options) {
return options.entrySet().stream()
.filter( entry -> additionalSupportedOptions.contains( entry.getKey() ) )
.collect( Collectors.toMap( Map.Entry::getKey, Map.Entry::getValue ) );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,18 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen
private boolean disableBuilder;
private boolean verbose;

private Map<String, String> options;

public AnnotationProcessorContext(Elements elementUtils, Types typeUtils, Messager messager, boolean disableBuilder,
boolean verbose) {
boolean verbose, Map<String, String> options) {
astModifyingAnnotationProcessors = java.util.Collections.unmodifiableList(
findAstModifyingAnnotationProcessors( messager ) );
this.elementUtils = elementUtils;
this.typeUtils = typeUtils;
this.messager = messager;
this.disableBuilder = disableBuilder;
this.verbose = verbose;
this.options = java.util.Collections.unmodifiableMap( options );
}

/**
Expand Down Expand Up @@ -270,4 +273,8 @@ public Map<String, EnumTransformationStrategy> getEnumTransformationStrategies()
initialize();
return enumTransformationStrategies;
}

public Map<String, String> getOptions() {
return this.options;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public class Services {
private Services() {
}

public static <T> Iterable<T> all(Class<T> serviceType) {
return ServiceLoader.load( serviceType, Services.class.getClassLoader() );
}

public static <T> T get(Class<T> serviceType, T defaultValue) {

Iterator<T> services = ServiceLoader.load( serviceType, Services.class.getClassLoader() ).iterator();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* 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.spi;

import java.util.Set;

/**
* Provider for any additional supported options required for custom SPI implementations.
* The resolved values are retrieved from {@link MapStructProcessingEnvironment#getOptions()}.
*/
public interface AdditionalSupportedOptionsProvider {

/**
* Returns the supported options required for custom SPI implementations.
*
* @return the additional supported options.
*/
Set<String> getAdditionalSupportedOptions();

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;
import java.util.Map;

/**
* MapStruct will provide the implementations of its SPIs with on object implementing this interface so they can use
Expand Down Expand Up @@ -36,4 +37,12 @@ public interface MapStructProcessingEnvironment {
*/
Types getTypeUtils();

/**
* Returns the resolved options specified by the impl of
* {@link AdditionalSupportedOptionsProvider}.
*
* @return resolved options
*/
Map<String, String> getOptions();

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.test.additionalsupportedoptions;

import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.mapstruct.ap.spi.EnumMappingStrategy;
import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.WithServiceImplementation;
import org.mapstruct.ap.testutil.compilation.annotation.CompilationResult;
import org.mapstruct.ap.testutil.compilation.annotation.Diagnostic;
import org.mapstruct.ap.testutil.compilation.annotation.ExpectedCompilationOutcome;
import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption;
import org.mapstruct.ap.testutil.runner.Compiler;

import static org.assertj.core.api.Assertions.assertThat;

@Execution( ExecutionMode.CONCURRENT )
public class AdditionalSupportedOptionsProviderTest {

@ProcessorTest
@WithClasses({
Pet.class,
PetWithMissing.class,
UnknownEnumMappingStrategyMapper.class
})
@WithServiceImplementation(CustomAdditionalSupportedOptionsProvider.class)
@WithServiceImplementation(value = UnknownEnumMappingStrategy.class, provides = EnumMappingStrategy.class)
@ProcessorOption(name = "myorg.custom.defaultNullEnumConstant", value = "MISSING")
public void shouldUseConfiguredPrefix() {
assertThat( UnknownEnumMappingStrategyMapper.INSTANCE.map( null ) )
.isEqualTo( PetWithMissing.MISSING );
}

@ProcessorTest(Compiler.JDK) // The eclipse compiler does not parse the error message properly
@WithClasses({
EmptyMapper.class
})
@WithServiceImplementation(InvalidAdditionalSupportedOptionsProvider.class)
@ExpectedCompilationOutcome(
value = CompilationResult.FAILED,
diagnostics = @Diagnostic(
kind = javax.tools.Diagnostic.Kind.ERROR,
messageRegExp = "Additional SPI options cannot start with \"mapstruct\". Provider " +
"org.mapstruct.ap.test.additionalsupportedoptions.InvalidAdditionalSupportedOptionsProvider@.*" +
" provided option mapstruct.custom.test"
)
)
public void shouldFailWhenOptionsProviderUsesMapstructPrefix() {
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.test.additionalsupportedoptions;

// tag::documentation[]
import java.util.Collections;
import java.util.Set;

import org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider;

public class CustomAdditionalSupportedOptionsProvider implements AdditionalSupportedOptionsProvider {

@Override
public Set<String> getAdditionalSupportedOptions() {
return Collections.singleton( "myorg.custom.defaultNullEnumConstant" );
}

}
// end::documentation[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/*
* 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.test.additionalsupportedoptions;

import org.mapstruct.Mapper;

@Mapper
public interface EmptyMapper {
}
Loading