diff --git a/core-common/src/main/java/org/mapstruct/MapNullToDefault.java b/core-common/src/main/java/org/mapstruct/MapNullToDefault.java new file mode 100644 index 000000000..a61fc41e7 --- /dev/null +++ b/core-common/src/main/java/org/mapstruct/MapNullToDefault.java @@ -0,0 +1,45 @@ +/** + * Copyright 2012-2014 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; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Advises the code generator to apply all the {@link Mapping}s from an inverse mapping method to the annotated method + * as well. An inverse mapping method is a method which has the annotated method's source type as target type (return + * type or indicated through a parameter annotated with {@link MappingTarget}) and the annotated method's target type as + * source type. + *

+ * Any mappings given on the annotated method itself are added to those mappings inherited from the inverse method. In + * case of a conflict local mappings take precedence over inherited mappings. + *

+ * If more than one matching inverse method exists, the name of the method to inherit the configuration from must be + * specified via {@link #name()} + * + * @author Sjaak Derksen + */ +@Target( { ElementType.METHOD, ElementType.TYPE } ) +@Retention( RetentionPolicy.SOURCE ) +public @interface MapNullToDefault { + + boolean value() default true; +} diff --git a/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java index 401fd9e40..b7c605146 100644 --- a/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/model/BeanMappingMethod.java @@ -40,6 +40,7 @@ import org.mapstruct.ap.model.source.Mapping; import org.mapstruct.ap.model.source.SourceMethod; import org.mapstruct.ap.model.source.SourceReference; import org.mapstruct.ap.option.ReportingPolicy; +import org.mapstruct.ap.prism.MapNullToDefaultPrism; import org.mapstruct.ap.prism.CollectionMappingStrategyPrism; import org.mapstruct.ap.util.Executables; import org.mapstruct.ap.util.MapperConfig; @@ -58,6 +59,7 @@ public class BeanMappingMethod extends MappingMethod { private final Map> mappingsByParameter; private final List constantMappings; private final MethodReference factoryMethod; + private final boolean mapNullToDefault; public static class Builder { @@ -97,8 +99,12 @@ public class BeanMappingMethod extends MappingMethod { // report errors on unmapped properties reportErrorForUnmappedTargetPropertiesIfRequired(); + // mapNullToDefault + MapNullToDefaultPrism prism = MapNullToDefaultPrism.getInstanceOn( method.getExecutable() ); + boolean mapNullToDefault = ( prism != null ) && prism.value(); + MethodReference factoryMethod = AssignmentFactory.createFactoryMethod( method.getReturnType(), ctx ); - return new BeanMappingMethod( method, propertyMappings, factoryMethod ); + return new BeanMappingMethod( method, propertyMappings, factoryMethod, mapNullToDefault ); } /** @@ -472,7 +478,8 @@ public class BeanMappingMethod extends MappingMethod { private BeanMappingMethod(SourceMethod method, List propertyMappings, - MethodReference factoryMethod) { + MethodReference factoryMethod, + boolean mapNullToDefault ) { super( method ); this.propertyMappings = propertyMappings; @@ -491,6 +498,7 @@ public class BeanMappingMethod extends MappingMethod { } } this.factoryMethod = factoryMethod; + this.mapNullToDefault = mapNullToDefault; } public List getPropertyMappings() { @@ -505,6 +513,10 @@ public class BeanMappingMethod extends MappingMethod { return mappingsByParameter; } + public boolean isMapNullToDefault() { + return mapNullToDefault; + } + @Override public Set getImportTypes() { Set types = super.getImportTypes(); diff --git a/processor/src/main/java/org/mapstruct/ap/prism/PrismGenerator.java b/processor/src/main/java/org/mapstruct/ap/prism/PrismGenerator.java index 3178ee7c8..2750f732c 100644 --- a/processor/src/main/java/org/mapstruct/ap/prism/PrismGenerator.java +++ b/processor/src/main/java/org/mapstruct/ap/prism/PrismGenerator.java @@ -27,6 +27,7 @@ import org.mapstruct.DecoratedWith; import org.mapstruct.InheritInverseConfiguration; import org.mapstruct.IterableMapping; import org.mapstruct.MapMapping; +import org.mapstruct.MapNullToDefault; import org.mapstruct.Mapper; import org.mapstruct.MapperConfig; import org.mapstruct.Mapping; @@ -52,6 +53,7 @@ import org.mapstruct.TargetType; @GeneratePrism(value = MapperConfig.class, publicAccess = true), @GeneratePrism(value = InheritInverseConfiguration.class, publicAccess = true), @GeneratePrism( value = Qualifier.class, publicAccess = true ), + @GeneratePrism( value = MapNullToDefault.class, publicAccess = true ), // external types @GeneratePrism(value = XmlElementDecl.class, publicAccess = true) diff --git a/processor/src/main/resources/org.mapstruct.ap.model.BeanMappingMethod.ftl b/processor/src/main/resources/org.mapstruct.ap.model.BeanMappingMethod.ftl index 2db9a52e9..a02209ed4 100644 --- a/processor/src/main/resources/org.mapstruct.ap.model.BeanMappingMethod.ftl +++ b/processor/src/main/resources/org.mapstruct.ap.model.BeanMappingMethod.ftl @@ -20,9 +20,11 @@ --> @Override <#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, ) <@throws/> { + <#if !mapNullToDefault> if ( <#list sourceParametersExcludingPrimitives as sourceParam>${sourceParam.name} == null<#if sourceParam_has_next> && ) { return<#if returnType.name != "void"> null; } + <#if !existingInstanceMapping><@includeModel object=resultType/> ${resultName} = <#if factoryMethod??><@includeModel object=factoryMethod targetType=resultType raw=true/><#else>new <@includeModel object=resultType/>(); <#if (sourceParameters?size > 1)> @@ -43,9 +45,11 @@ <#else> + <#if mapNullToDefault>if ( ${sourceParameters[0].name} != null ) { <#list propertyMappingsByParameter[sourceParameters[0].name] as propertyMapping> <@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping/> + <#if mapNullToDefault>} <#list constantMappings as constantMapping> <@includeModel object=constantMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping/> diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/CarMapper.java b/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/CarMapper.java new file mode 100644 index 000000000..2d3ff0482 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/CarMapper.java @@ -0,0 +1,53 @@ +/** + * Copyright 2012-2014 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.test.mapnulltodefault; + +import java.util.UUID; + +import org.mapstruct.Mapper; +import org.mapstruct.MapNullToDefault; +import org.mapstruct.Mapping; +import org.mapstruct.Mappings; +import org.mapstruct.ap.test.mapnulltodefault.source.Car; +import org.mapstruct.ap.test.mapnulltodefault.target.CarDto; +import org.mapstruct.factory.Mappers; + +@Mapper(imports = UUID.class) +public interface CarMapper { + + CarMapper INSTANCE = Mappers.getMapper( CarMapper.class ); + + @MapNullToDefault + @Mappings({ + @Mapping(target = "seatCount", source = "numberOfSeats"), + @Mapping(target = "model", constant = "ModelT"), + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") + }) + CarDto carToCarDto(Car car); + + @MapNullToDefault + @Mappings({ + @Mapping(target = "seatCount", source = "car.numberOfSeats"), + @Mapping(target = "model", source = "model"), // TODO, should not be needed, must be made based on name only + @Mapping(target = "catalogId", expression = "java( UUID.randomUUID().toString() )") + }) + CarDto carToCarDto(Car car, String model); + + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/CarMapperTest.java b/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/CarMapperTest.java new file mode 100644 index 000000000..a42408c7d --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/CarMapperTest.java @@ -0,0 +1,95 @@ +/** + * Copyright 2012-2014 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.test.mapnulltodefault; + +import static org.fest.assertions.Assertions.assertThat; + + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.test.mapnulltodefault.source.Car; +import org.mapstruct.ap.test.mapnulltodefault.target.CarDto; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +@WithClasses({ + Car.class, + CarDto.class, + CarMapper.class +}) +@RunWith(AnnotationProcessorTestRunner.class) +public class CarMapperTest { + + @Test + public void shouldProvideMapperInstance() throws Exception { + assertThat( CarMapper.INSTANCE ).isNotNull(); + } + + @Test + public void shouldMapExpressionAndConstantRegardlessNullArg() { + //given + Car car = new Car( "Morris", 2 ); + + //when + CarDto carDto1 = CarMapper.INSTANCE.carToCarDto( car ); + + //then + assertThat( carDto1 ).isNotNull(); + assertThat( carDto1.getMake() ).isEqualTo( car.getMake() ); + assertThat( carDto1.getSeatCount() ).isEqualTo( car.getNumberOfSeats() ); + assertThat( carDto1.getModel() ).isEqualTo( "ModelT" ); + assertThat( carDto1.getCatalogId() ).isNotEmpty(); + + //when + CarDto carDto2 = CarMapper.INSTANCE.carToCarDto( null ); + + //then + assertThat( carDto2 ).isNotNull(); + assertThat( carDto2.getMake() ).isNull(); + assertThat( carDto2.getSeatCount() ).isEqualTo( 0 ); + assertThat( carDto2.getModel() ).isEqualTo( "ModelT" ); + assertThat( carDto2.getCatalogId() ).isNotEmpty(); + } + + @Test + public void shouldMapExpressionAndConstantRegardlessNullArgSeveralSources() { + //given + Car car = new Car( "Morris", 2 ); + + //when + CarDto carDto1 = CarMapper.INSTANCE.carToCarDto( car, "ModelT" ); + + //then + assertThat( carDto1 ).isNotNull(); + assertThat( carDto1.getMake() ).isEqualTo( car.getMake() ); + assertThat( carDto1.getSeatCount() ).isEqualTo( car.getNumberOfSeats() ); + assertThat( carDto1.getCatalogId() ).isNotEmpty(); + + //when + CarDto carDto2 = CarMapper.INSTANCE.carToCarDto( null, "ModelT" ); + + //then + assertThat( carDto2 ).isNotNull(); + assertThat( carDto2.getMake() ).isNull(); + assertThat( carDto2.getSeatCount() ).isEqualTo( 0 ); + assertThat( carDto2.getModel() ).isEqualTo( "ModelT" ); + assertThat( carDto2.getCatalogId() ).isNotEmpty(); + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/source/Car.java b/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/source/Car.java new file mode 100644 index 000000000..576f53604 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/source/Car.java @@ -0,0 +1,53 @@ +/** + * Copyright 2012-2014 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.test.mapnulltodefault.source; + + +public class Car { + + private String make; + private int numberOfSeats; + + + public Car() { + } + + public Car(String make, int numberOfSeats) { + this.make = make; + this.numberOfSeats = numberOfSeats; + + } + + public String getMake() { + return make; + } + + public void setMake(String make) { + this.make = make; + } + + public int getNumberOfSeats() { + return numberOfSeats; + } + + public void setNumberOfSeats(int numberOfSeats) { + this.numberOfSeats = numberOfSeats; + } + +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/target/CarDto.java b/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/target/CarDto.java new file mode 100644 index 000000000..8e4ccb9f1 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/mapnulltodefault/target/CarDto.java @@ -0,0 +1,69 @@ +/** + * Copyright 2012-2014 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.test.mapnulltodefault.target; + +public class CarDto { + + private String make; + private int seatCount; + private String model; + private String catalogId; + + + public CarDto() { + } + + public CarDto(String make, int seatCount) { + this.make = make; + this.seatCount = seatCount; + } + + public String getMake() { + return make; + } + + public void setMake(String make) { + this.make = make; + } + + public int getSeatCount() { + return seatCount; + } + + public void setSeatCount(int seatCount) { + this.seatCount = seatCount; + } + + public String getModel() { + return model; + } + + public void setModel( String model ) { + this.model = model; + } + + public String getCatalogId() { + return catalogId; + } + + public void setCatalogId( String catalogId ) { + this.catalogId = catalogId; + } + +}