#3071 Support defining custom processor options by custom SPI

This commit is contained in:
ro0sterjam 2023-04-30 11:02:39 -04:00 committed by GitHub
parent 2f78d3f4e2
commit 931591a385
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 361 additions and 4 deletions

View File

@ -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]
----
====

View File

@ -8,6 +8,7 @@ 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;
@ -33,17 +34,19 @@ import javax.lang.model.type.TypeMirror;
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;
@ -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;
@ -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 );
@ -138,8 +159,13 @@ public class MappingProcessor extends AbstractProcessor {
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() {
@ -225,6 +251,17 @@ public class MappingProcessor extends AbstractProcessor {
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).
@ -407,6 +444,35 @@ public class MappingProcessor extends AbstractProcessor {
);
}
/**
* 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
@ -425,4 +491,16 @@ public class MappingProcessor extends AbstractProcessor {
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 ) );
}
}

View File

@ -55,8 +55,10 @@ 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;
@ -64,6 +66,7 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen
this.messager = messager;
this.disableBuilder = disableBuilder;
this.verbose = verbose;
this.options = java.util.Collections.unmodifiableMap( options );
}
/**
@ -270,4 +273,8 @@ public class AnnotationProcessorContext implements MapStructProcessingEnvironmen
initialize();
return enumTransformationStrategies;
}
public Map<String, String> getOptions() {
return this.options;
}
}

View File

@ -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();

View File

@ -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();
}

View File

@ -7,6 +7,7 @@ package org.mapstruct.ap.spi;
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
@ -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();
}

View File

@ -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() {
}
}

View File

@ -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[]

View File

@ -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 {
}

View File

@ -0,0 +1,20 @@
/*
* 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 java.util.Collections;
import java.util.Set;
import org.mapstruct.ap.spi.AdditionalSupportedOptionsProvider;
public class InvalidAdditionalSupportedOptionsProvider implements AdditionalSupportedOptionsProvider {
@Override
public Set<String> getAdditionalSupportedOptions() {
return Collections.singleton( "mapstruct.custom.test" );
}
}

View File

@ -0,0 +1,14 @@
/*
* 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;
public enum Pet {
DOG,
CAT,
BEAR
}

View File

@ -0,0 +1,15 @@
/*
* 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;
public enum PetWithMissing {
DOG,
CAT,
BEAR,
MISSING
}

View File

@ -0,0 +1,29 @@
/*
* 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 javax.lang.model.element.TypeElement;
import org.mapstruct.ap.spi.DefaultEnumMappingStrategy;
import org.mapstruct.ap.spi.MapStructProcessingEnvironment;
public class UnknownEnumMappingStrategy extends DefaultEnumMappingStrategy {
private String defaultNullEnumConstant;
@Override
public void init(MapStructProcessingEnvironment processingEnvironment) {
super.init( processingEnvironment );
defaultNullEnumConstant = processingEnvironment.getOptions().get( "myorg.custom.defaultNullEnumConstant" );
}
@Override
public String getDefaultNullEnumConstant(TypeElement enumType) {
return defaultNullEnumConstant;
}
}
// end::documentation[]

View File

@ -0,0 +1,17 @@
/*
* 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;
import org.mapstruct.factory.Mappers;
@Mapper
public interface UnknownEnumMappingStrategyMapper {
UnknownEnumMappingStrategyMapper INSTANCE = Mappers.getMapper( UnknownEnumMappingStrategyMapper.class );
PetWithMissing map(Pet pet);
}