#1792 Annotation processor option for default injection strategy

This commit is contained in:
Andrei Arlou 2019-09-18 20:51:36 +03:00 committed by Filip Hrisafov
parent d018aed251
commit 750ce48023
16 changed files with 519 additions and 8 deletions

View File

@ -254,6 +254,19 @@ Supported values are:
If a component model is given for a specific mapper via `@Mapper#componentModel()`, the value from the annotation takes precedence.
|`default`
|`mapstruct.defaultInjectionStrategy`
| The type of the injection in mapper via parameter `uses`. This is only used on annotated based component models
such as CDI, Spring and JSR 330.
Supported values are:
* `field`: dependencies will be injected in fields
* `constructor`: will be generated constructor. Dependencies will be injected via constructor.
When CDI `componentModel` a default constructor will also be generated.
If a injection strategy is given for a specific mapper via `@Mapper#injectionStrategy()`, the value from the annotation takes precedence over the option.
|`field`
|`mapstruct.unmappedTargetPolicy`
|The default reporting policy to be applied in case an attribute of the target object of a mapping method is not populated with a source value.

View File

@ -120,7 +120,7 @@ public interface CarMapper {
The generated mapper will inject all classes defined in the **uses** attribute.
When `InjectionStrategy#CONSTRUCTOR` is used, the constructor will have the appropriate annotation and the fields won't.
When `InjectionStrategy#FIELD` is used, the annotation is on the field itself.
For now, the default injection strategy is field injection.
For now, the default injection strategy is field injection, but it can be configured with <<configuration-options>>.
It is recommended to use constructor injection to simplify testing.
[TIP]

View File

@ -83,6 +83,7 @@ import static javax.lang.model.element.ElementKind.CLASS;
MappingProcessor.SUPPRESS_GENERATOR_VERSION_INFO_COMMENT,
MappingProcessor.UNMAPPED_TARGET_POLICY,
MappingProcessor.DEFAULT_COMPONENT_MODEL,
MappingProcessor.DEFAULT_INJECTION_STRATEGY,
MappingProcessor.VERBOSE
})
public class MappingProcessor extends AbstractProcessor {
@ -97,6 +98,7 @@ public class MappingProcessor extends AbstractProcessor {
"mapstruct.suppressGeneratorVersionInfoComment";
protected static final String UNMAPPED_TARGET_POLICY = "mapstruct.unmappedTargetPolicy";
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 VERBOSE = "mapstruct.verbose";
@ -136,6 +138,7 @@ public class MappingProcessor extends AbstractProcessor {
Boolean.valueOf( processingEnv.getOptions().get( SUPPRESS_GENERATOR_VERSION_INFO_COMMENT ) ),
unmappedTargetPolicy != null ? ReportingPolicyPrism.valueOf( unmappedTargetPolicy.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( VERBOSE ) )
);

View File

@ -19,15 +19,18 @@ public class Options {
private final ReportingPolicyPrism unmappedTargetPolicy;
private final boolean alwaysGenerateSpi;
private final String defaultComponentModel;
private final String defaultInjectionStrategy;
private final boolean verbose;
public Options(boolean suppressGeneratorTimestamp, boolean suppressGeneratorVersionComment,
ReportingPolicyPrism unmappedTargetPolicy,
String defaultComponentModel, boolean alwaysGenerateSpi, boolean verbose) {
String defaultComponentModel, String defaultInjectionStrategy,
boolean alwaysGenerateSpi, boolean verbose) {
this.suppressGeneratorTimestamp = suppressGeneratorTimestamp;
this.suppressGeneratorVersionComment = suppressGeneratorVersionComment;
this.unmappedTargetPolicy = unmappedTargetPolicy;
this.defaultComponentModel = defaultComponentModel;
this.defaultInjectionStrategy = defaultInjectionStrategy;
this.alwaysGenerateSpi = alwaysGenerateSpi;
this.verbose = verbose;
}
@ -48,6 +51,10 @@ public class Options {
return defaultComponentModel;
}
public String getDefaultInjectionStrategy() {
return defaultInjectionStrategy;
}
public boolean isAlwaysGenerateSpi() {
return alwaysGenerateSpi;
}

View File

@ -45,7 +45,7 @@ public abstract class AnnotationBasedComponentModelProcessor implements ModelEle
MapperConfiguration mapperConfiguration = MapperConfiguration.getInstanceOn( mapperTypeElement );
String componentModel = mapperConfiguration.componentModel( context.getOptions() );
InjectionStrategyPrism injectionStrategy = mapperConfiguration.getInjectionStrategy();
InjectionStrategyPrism injectionStrategy = mapperConfiguration.getInjectionStrategy( context.getOptions() );
if ( !getComponentModelIdentifier().equalsIgnoreCase( componentModel ) ) {
return mapper;

View File

@ -202,13 +202,21 @@ public class MapperConfiguration {
}
}
public InjectionStrategyPrism getInjectionStrategy() {
if ( mapperConfigPrism != null && mapperPrism.values.injectionStrategy() == null ) {
return InjectionStrategyPrism.valueOf( mapperConfigPrism.injectionStrategy() );
}
else {
public InjectionStrategyPrism getInjectionStrategy(Options options) {
if ( mapperPrism.values.injectionStrategy() != null ) {
return InjectionStrategyPrism.valueOf( mapperPrism.injectionStrategy() );
}
if ( mapperConfigPrism != null && mapperConfigPrism.values.injectionStrategy() != null ) {
return InjectionStrategyPrism.valueOf( mapperConfigPrism.injectionStrategy() );
}
if ( options.getDefaultInjectionStrategy() != null ) {
return InjectionStrategyPrism.valueOf( options.getDefaultInjectionStrategy().toUpperCase() );
}
// fall back to default defined in the annotation
return InjectionStrategyPrism.valueOf( mapperPrism.injectionStrategy() );
}
public NullValueMappingStrategyPrism getNullValueMappingStrategy() {

View File

@ -0,0 +1,19 @@
/*
* 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.injectionstrategy.jsr330._default;
import org.mapstruct.Mapper;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity;
/**
* @author Andrei Arlou
*/
@Mapper(componentModel = "jsr330", uses = GenderJsr330DefaultCompileOptionFieldMapper.class)
public interface CustomerJsr330DefaultCompileOptionFieldMapper {
CustomerDto asTarget(CustomerEntity customerEntity);
}

View File

@ -0,0 +1,25 @@
/*
* 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.injectionstrategy.jsr330._default;
import org.mapstruct.Mapper;
import org.mapstruct.ValueMapping;
import org.mapstruct.ValueMappings;
import org.mapstruct.ap.test.injectionstrategy.shared.Gender;
import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto;
/**
* @author Andrei Arlou
*/
@Mapper(componentModel = "jsr330")
public interface GenderJsr330DefaultCompileOptionFieldMapper {
@ValueMappings({
@ValueMapping(source = "MALE", target = "M"),
@ValueMapping(source = "FEMALE", target = "F")
})
GenderDto mapToDto(Gender gender);
}

View File

@ -0,0 +1,94 @@
/*
* 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.injectionstrategy.jsr330._default;
import javax.inject.Inject;
import javax.inject.Named;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity;
import org.mapstruct.ap.test.injectionstrategy.shared.Gender;
import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
import org.mapstruct.ap.testutil.runner.GeneratedSource;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import static java.lang.System.lineSeparator;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test field injection for component model jsr330.
* Default value option mapstruct.defaultInjectionStrategy is "field"
*
* @author Andrei Arlou
*/
@WithClasses({
CustomerDto.class,
CustomerEntity.class,
Gender.class,
GenderDto.class,
CustomerJsr330DefaultCompileOptionFieldMapper.class,
GenderJsr330DefaultCompileOptionFieldMapper.class
})
@RunWith(AnnotationProcessorTestRunner.class)
@ComponentScan(basePackageClasses = CustomerJsr330DefaultCompileOptionFieldMapper.class)
@Configuration
public class Jsr330DefaultCompileOptionFieldMapperTest {
@Rule
public final GeneratedSource generatedSource = new GeneratedSource();
@Inject
@Named
private CustomerJsr330DefaultCompileOptionFieldMapper customerMapper;
private ConfigurableApplicationContext context;
@Before
public void springUp() {
context = new AnnotationConfigApplicationContext( getClass() );
context.getAutowireCapableBeanFactory().autowireBean( this );
}
@After
public void springDown() {
if ( context != null ) {
context.close();
}
}
@Test
public void shouldConvertToTarget() {
// given
CustomerEntity customerEntity = new CustomerEntity();
customerEntity.setName( "Samuel" );
customerEntity.setGender( Gender.MALE );
// when
CustomerDto customerDto = customerMapper.asTarget( customerEntity );
// then
assertThat( customerDto ).isNotNull();
assertThat( customerDto.getName() ).isEqualTo( "Samuel" );
assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M );
}
@Test
public void shouldHaveFieldInjection() {
generatedSource.forMapper( CustomerJsr330DefaultCompileOptionFieldMapper.class )
.content()
.contains( "@Inject" + lineSeparator() + " private GenderJsr330DefaultCompileOptionFieldMapper" )
.doesNotContain( "public CustomerJsr330DefaultCompileOptionFieldMapperImpl(" );
}
}

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.test.injectionstrategy.jsr330.compileoptionconstructor;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity;
/**
* @author Andrei Arlou
*/
@Mapper( componentModel = "jsr330",
uses = GenderJsr330CompileOptionConstructorMapper.class )
public interface CustomerJsr330CompileOptionConstructorMapper {
@Mapping(source = "gender", target = "gender")
CustomerDto asTarget(CustomerEntity customerEntity);
}

View File

@ -0,0 +1,25 @@
/*
* 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.injectionstrategy.jsr330.compileoptionconstructor;
import org.mapstruct.Mapper;
import org.mapstruct.ValueMapping;
import org.mapstruct.ValueMappings;
import org.mapstruct.ap.test.injectionstrategy.shared.Gender;
import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto;
/**
* @author Andrei Arlou
*/
@Mapper(componentModel = "jsr330")
public interface GenderJsr330CompileOptionConstructorMapper {
@ValueMappings({
@ValueMapping(source = "MALE", target = "M"),
@ValueMapping(source = "FEMALE", target = "F")
})
GenderDto mapToDto(Gender gender);
}

View File

@ -0,0 +1,95 @@
/*
* 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.injectionstrategy.jsr330.compileoptionconstructor;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity;
import org.mapstruct.ap.test.injectionstrategy.shared.Gender;
import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
import org.mapstruct.ap.testutil.runner.GeneratedSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import static java.lang.System.lineSeparator;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test constructor injection for component model jsr330 with compile option
* mapstruct.defaultInjectionStrategy=constructor
*
* @author Andrei Arlou
*/
@WithClasses({
CustomerDto.class,
CustomerEntity.class,
Gender.class,
GenderDto.class,
CustomerJsr330CompileOptionConstructorMapper.class,
GenderJsr330CompileOptionConstructorMapper.class
})
@RunWith(AnnotationProcessorTestRunner.class)
@ProcessorOption( name = "mapstruct.defaultInjectionStrategy", value = "constructor")
@ComponentScan(basePackageClasses = CustomerJsr330CompileOptionConstructorMapper.class)
@Configuration
public class Jsr330CompileOptionConstructorMapperTest {
@Rule
public final GeneratedSource generatedSource = new GeneratedSource();
@Autowired
private CustomerJsr330CompileOptionConstructorMapper customerMapper;
private ConfigurableApplicationContext context;
@Before
public void springUp() {
context = new AnnotationConfigApplicationContext( getClass() );
context.getAutowireCapableBeanFactory().autowireBean( this );
}
@After
public void springDown() {
if ( context != null ) {
context.close();
}
}
@Test
public void shouldConvertToTarget() {
// given
CustomerEntity customerEntity = new CustomerEntity();
customerEntity.setName( "Samuel" );
customerEntity.setGender( Gender.MALE );
// when
CustomerDto customerDto = customerMapper.asTarget( customerEntity );
// then
assertThat( customerDto ).isNotNull();
assertThat( customerDto.getName() ).isEqualTo( "Samuel" );
assertThat( customerDto.getGender() ).isEqualTo( GenderDto.M );
}
@Test
public void shouldHaveConstructorInjectionFromCompileOption() {
generatedSource.forMapper( CustomerJsr330CompileOptionConstructorMapper.class )
.content()
.contains( "private final GenderJsr330CompileOptionConstructorMapper" )
.contains( "@Inject" + lineSeparator() +
" public CustomerJsr330CompileOptionConstructorMapperImpl" +
"(GenderJsr330CompileOptionConstructorMapper" );
}
}

View File

@ -0,0 +1,21 @@
/*
* 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.injectionstrategy.spring.compileoptionconstructor;
import org.mapstruct.Mapper;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerRecordDto;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerRecordEntity;
/**
* @author Andrei Arlou
*/
@Mapper(componentModel = "spring",
uses = { CustomerSpringCompileOptionConstructorMapper.class, GenderSpringCompileOptionConstructorMapper.class },
disableSubMappingMethodsGeneration = true)
public interface CustomerRecordSpringCompileOptionConstructorMapper {
CustomerRecordDto asTarget(CustomerRecordEntity customerRecordEntity);
}

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.injectionstrategy.spring.compileoptionconstructor;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity;
/**
* @author Andrei Arlou
*/
@Mapper( componentModel = "spring",
uses = GenderSpringCompileOptionConstructorMapper.class)
public interface CustomerSpringCompileOptionConstructorMapper {
@Mapping( source = "gender", target = "gender" )
CustomerDto asTarget(CustomerEntity customerEntity);
}

View File

@ -0,0 +1,25 @@
/*
* 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.injectionstrategy.spring.compileoptionconstructor;
import org.mapstruct.Mapper;
import org.mapstruct.ValueMapping;
import org.mapstruct.ValueMappings;
import org.mapstruct.ap.test.injectionstrategy.shared.Gender;
import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto;
/**
* @author Andrei Arlou
*/
@Mapper(componentModel = "spring")
public interface GenderSpringCompileOptionConstructorMapper {
@ValueMappings({
@ValueMapping(source = "MALE", target = "M"),
@ValueMapping(source = "FEMALE", target = "F")
})
GenderDto mapToDto(Gender gender);
}

View File

@ -0,0 +1,131 @@
/*
* 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.injectionstrategy.spring.compileoptionconstructor;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.TimeZone;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerDto;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerEntity;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerRecordDto;
import org.mapstruct.ap.test.injectionstrategy.shared.CustomerRecordEntity;
import org.mapstruct.ap.test.injectionstrategy.shared.Gender;
import org.mapstruct.ap.test.injectionstrategy.shared.GenderDto;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.compilation.annotation.ProcessorOption;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;
import org.mapstruct.ap.testutil.runner.GeneratedSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import static java.lang.System.lineSeparator;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test constructor injection for component model spring with
* compile option mapstruct.defaultInjectStrategy=constructor
*
* @author Andrei Arlou
*/
@WithClasses( {
CustomerRecordDto.class,
CustomerRecordEntity.class,
CustomerDto.class,
CustomerEntity.class,
Gender.class,
GenderDto.class,
CustomerRecordSpringCompileOptionConstructorMapper.class,
CustomerSpringCompileOptionConstructorMapper.class,
GenderSpringCompileOptionConstructorMapper.class
} )
@RunWith(AnnotationProcessorTestRunner.class)
@ProcessorOption( name = "mapstruct.defaultInjectionStrategy", value = "constructor")
@ComponentScan(basePackageClasses = CustomerSpringCompileOptionConstructorMapper.class)
@Configuration
public class SpringCompileOptionConstructorMapperTest {
private static TimeZone originalTimeZone;
@Rule
public final GeneratedSource generatedSource = new GeneratedSource();
@Autowired
private CustomerRecordSpringCompileOptionConstructorMapper customerRecordMapper;
private ConfigurableApplicationContext context;
@BeforeClass
public static void setDefaultTimeZoneToCet() {
originalTimeZone = TimeZone.getDefault();
TimeZone.setDefault( TimeZone.getTimeZone( "Europe/Berlin" ) );
}
@AfterClass
public static void restoreOriginalTimeZone() {
TimeZone.setDefault( originalTimeZone );
}
@Before
public void springUp() {
context = new AnnotationConfigApplicationContext( getClass() );
context.getAutowireCapableBeanFactory().autowireBean( this );
}
@After
public void springDown() {
if ( context != null ) {
context.close();
}
}
@Test
public void shouldConvertToTarget() throws Exception {
// given
CustomerEntity customerEntity = new CustomerEntity();
customerEntity.setName( "Samuel" );
customerEntity.setGender( Gender.MALE );
CustomerRecordEntity customerRecordEntity = new CustomerRecordEntity();
customerRecordEntity.setCustomer( customerEntity );
customerRecordEntity.setRegistrationDate( createDate( "31-08-1982 10:20:56" ) );
// when
CustomerRecordDto customerRecordDto = customerRecordMapper.asTarget( customerRecordEntity );
// then
assertThat( customerRecordDto ).isNotNull();
assertThat( customerRecordDto.getCustomer() ).isNotNull();
assertThat( customerRecordDto.getCustomer().getName() ).isEqualTo( "Samuel" );
assertThat( customerRecordDto.getCustomer().getGender() ).isEqualTo( GenderDto.M );
assertThat( customerRecordDto.getRegistrationDate() ).isNotNull();
assertThat( customerRecordDto.getRegistrationDate().toString() ).isEqualTo( "1982-08-31T10:20:56.000+02:00" );
}
private Date createDate(String date) throws ParseException {
SimpleDateFormat sdf = new SimpleDateFormat( "dd-M-yyyy hh:mm:ss" );
return sdf.parse( date );
}
@Test
public void shouldConstructorInjectionFromCompileOption() {
generatedSource.forMapper( CustomerSpringCompileOptionConstructorMapper.class )
.content()
.contains( "private final GenderSpringCompileOptionConstructorMapper" )
.contains( "@Autowired" + lineSeparator() +
" public CustomerSpringCompileOptionConstructorMapperImpl" +
"(GenderSpringCompileOptionConstructorMapper" );
}
}