diff --git a/integrationtest/src/test/java/org/mapstruct/itest/tests/FullFeatureCompilationTest.java b/integrationtest/src/test/java/org/mapstruct/itest/tests/FullFeatureCompilationTest.java index fe3836e30..45802ff38 100644 --- a/integrationtest/src/test/java/org/mapstruct/itest/tests/FullFeatureCompilationTest.java +++ b/integrationtest/src/test/java/org/mapstruct/itest/tests/FullFeatureCompilationTest.java @@ -51,6 +51,7 @@ public class FullFeatureCompilationTest { // SPI not working correctly here.. (not picked up) additionalExcludes.add( "org/mapstruct/ap/test/bugs/_1596/*.java" ); + additionalExcludes.add( "org/mapstruct/ap/test/bugs/_1801/*.java" ); switch ( processorType ) { case ORACLE_JAVA_9: diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java index f5ddf3867..666dfb1aa 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/BeanMappingMethod.java @@ -140,21 +140,22 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { Type returnTypeImpl = getReturnTypeToConstructFromSelectionParameters( selectionParameters ); if ( returnTypeImpl != null ) { factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters ); - if ( factoryMethod != null || canBeConstructed( returnTypeImpl ) ) { + if ( factoryMethod != null || canResultTypeFromBeanMappingBeConstructed( returnTypeImpl ) ) { returnTypeToConstruct = returnTypeImpl; } - else { - reportResultTypeFromBeanMappingNotConstructableError( returnTypeImpl ); - } } - else { - returnTypeImpl = isBuilderRequired() ? returnTypeBuilder.getBuilder() : method.getReturnType(); + else if ( isBuilderRequired() ) { + returnTypeImpl = returnTypeBuilder.getBuilder(); factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters ); - if ( factoryMethod != null || canBeConstructed( returnTypeImpl ) ) { + if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) { returnTypeToConstruct = returnTypeImpl; } - else { - reportReturnTypeNotConstructableError( returnTypeImpl ); + } + else if ( !method.isUpdateMethod() ) { + returnTypeImpl = method.getReturnType(); + factoryMethod = getFactoryMethod( returnTypeImpl, selectionParameters ); + if ( factoryMethod != null || canReturnTypeBeConstructed( returnTypeImpl ) ) { + returnTypeToConstruct = returnTypeImpl; } } } @@ -383,57 +384,60 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { return null; } - private boolean canBeConstructed(Type typeToBeConstructed) { - return !typeToBeConstructed.isAbstract() - && typeToBeConstructed.isAssignableTo( this.method.getResultType() ) - && typeToBeConstructed.hasEmptyAccessibleConstructor(); - } - - private void reportResultTypeFromBeanMappingNotConstructableError(Type resultType) { + private boolean canResultTypeFromBeanMappingBeConstructed(Type resultType) { + boolean error = true; if ( resultType.isAbstract() ) { ctx.getMessager().printMessage( - method.getExecutable(), - BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror, - BEANMAPPING_ABSTRACT, - resultType, - method.getResultType() + method.getExecutable(), + BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror, + BEANMAPPING_ABSTRACT, + resultType, + method.getResultType() ); + error = false; } else if ( !resultType.isAssignableTo( method.getResultType() ) ) { ctx.getMessager().printMessage( - method.getExecutable(), - BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror, - BEANMAPPING_NOT_ASSIGNABLE, - resultType, - method.getResultType() + method.getExecutable(), + BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror, + BEANMAPPING_NOT_ASSIGNABLE, + resultType, + method.getResultType() ); + error = false; } else if ( !resultType.hasEmptyAccessibleConstructor() ) { ctx.getMessager().printMessage( - method.getExecutable(), - BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror, - Message.GENERAL_NO_SUITABLE_CONSTRUCTOR, - resultType + method.getExecutable(), + BeanMappingPrism.getInstanceOn( method.getExecutable() ).mirror, + Message.GENERAL_NO_SUITABLE_CONSTRUCTOR, + resultType ); + error = false; } + return error; } - private void reportReturnTypeNotConstructableError(Type returnType) { + private boolean canReturnTypeBeConstructed(Type returnType) { + boolean error = true; if ( returnType.isAbstract() ) { ctx.getMessager().printMessage( - method.getExecutable(), - GENERAL_ABSTRACT_RETURN_TYPE, - returnType + method.getExecutable(), + GENERAL_ABSTRACT_RETURN_TYPE, + returnType ); + error = false; } else if ( !returnType.hasEmptyAccessibleConstructor() ) { ctx.getMessager().printMessage( - method.getExecutable(), - Message.GENERAL_NO_SUITABLE_CONSTRUCTOR, - returnType + method.getExecutable(), + Message.GENERAL_NO_SUITABLE_CONSTRUCTOR, + returnType ); + error = false; } + return error; } /** @@ -1021,4 +1025,3 @@ public class BeanMappingMethod extends NormalTypeMappingMethod { } } - diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java index bae5c161b..1b34f4698 100644 --- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java +++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/BuilderType.java @@ -6,6 +6,7 @@ package org.mapstruct.ap.internal.model.common; import java.util.Collection; +import javax.lang.model.element.ElementKind; import javax.lang.model.element.ExecutableElement; import javax.lang.model.type.TypeMirror; import javax.lang.model.util.Types; @@ -102,6 +103,11 @@ public class BuilderType { owner = typeFactory.getType( builderCreationOwner ); } + // When the builderCreationMethod is constructor, its return type is Void. In this case the + // builder type should be the owner type. + if (builderInfo.getBuilderCreationMethod().getKind() == ElementKind.CONSTRUCTOR) { + builder = owner; + } return new BuilderType( builder, diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801BuilderProvider.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801BuilderProvider.java new file mode 100644 index 000000000..80766d853 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801BuilderProvider.java @@ -0,0 +1,96 @@ +/* + * 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.bugs._1801; + +import java.util.List; +import java.util.Objects; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeElement; +import javax.lang.model.util.ElementFilter; + +import org.mapstruct.ap.spi.BuilderInfo; +import org.mapstruct.ap.spi.BuilderProvider; +import org.mapstruct.ap.spi.ImmutablesBuilderProvider; + +/** + * @author Zhizhi Deng + */ +public class Issue1801BuilderProvider extends ImmutablesBuilderProvider implements BuilderProvider { + + @Override + protected BuilderInfo findBuilderInfo(TypeElement typeElement) { + Name name = typeElement.getQualifiedName(); + if ( name.toString().endsWith( ".Item" ) ) { + BuilderInfo info = findBuilderInfoFromInnerBuilderClass( typeElement ); + if ( info != null ) { + return info; + } + } + return super.findBuilderInfo( typeElement ); + } + + /** + * Looks for inner builder class in the Immutable interface / abstract class. + * + * The inner builder class should be be declared with the following line + * + *
+     *     public static Builder() extends ImmutableItem.Builder { }
+     * 
+ * + * The Immutable instance should be created with the following line + * + *
+     *     new Item.Builder().withId("123").build();
+     * 
+ * + * @see org.mapstruct.ap.test.bugs._1801.domain.Item + * + * @param typeElement + * @return + */ + private BuilderInfo findBuilderInfoFromInnerBuilderClass(TypeElement typeElement) { + if (shouldIgnore( typeElement )) { + return null; + } + + List innerTypes = ElementFilter.typesIn( typeElement.getEnclosedElements() ); + ExecutableElement defaultConstructor = innerTypes.stream() + .filter( this::isBuilderCandidate ) + .map( this::getEmptyArgPublicConstructor ) + .filter( Objects::nonNull ) + .findAny() + .orElse( null ); + + if ( defaultConstructor != null ) { + return new BuilderInfo.Builder() + .builderCreationMethod( defaultConstructor ) + .buildMethod( findBuildMethods( (TypeElement) defaultConstructor.getEnclosingElement(), typeElement ) ) + .build(); + } + return null; + } + + private boolean isBuilderCandidate(TypeElement innerType ) { + TypeElement outerType = (TypeElement) innerType.getEnclosingElement(); + String packageName = this.elementUtils.getPackageOf( outerType ).getQualifiedName().toString(); + Name outerSimpleName = outerType.getSimpleName(); + String builderClassName = packageName + ".Immutable" + outerSimpleName + ".Builder"; + return innerType.getSimpleName().contentEquals( "Builder" ) + && getTypeElement( innerType.getSuperclass() ).getQualifiedName().contentEquals( builderClassName ) + && innerType.getModifiers().contains( Modifier.PUBLIC ); + } + + private ExecutableElement getEmptyArgPublicConstructor(TypeElement builderType) { + return ElementFilter.constructorsIn( builderType.getEnclosedElements() ).stream() + .filter( c -> c.getParameters().isEmpty() ) + .filter( c -> c.getModifiers().contains( Modifier.PUBLIC ) ) + .findAny() + .orElse( null ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801Test.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801Test.java new file mode 100644 index 000000000..e407c331f --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/Issue1801Test.java @@ -0,0 +1,53 @@ +/* + * 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.bugs._1801; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mapstruct.ap.spi.AccessorNamingStrategy; +import org.mapstruct.ap.spi.BuilderProvider; +import org.mapstruct.ap.spi.ImmutablesAccessorNamingStrategy; +import org.mapstruct.ap.test.bugs._1801.domain.ImmutableItem; +import org.mapstruct.ap.test.bugs._1801.domain.Item; +import org.mapstruct.ap.test.bugs._1801.dto.ImmutableItemDTO; +import org.mapstruct.ap.test.bugs._1801.dto.ItemDTO; +import org.mapstruct.ap.testutil.IssueKey; +import org.mapstruct.ap.testutil.WithClasses; +import org.mapstruct.ap.testutil.WithServiceImplementation; +import org.mapstruct.ap.testutil.WithServiceImplementations; +import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Zhizhi Deng + */ +@WithClasses({ + ItemMapper.class, + Item.class, + ImmutableItem.class, + ItemDTO.class, + ImmutableItemDTO.class +}) +@RunWith(AnnotationProcessorTestRunner.class) +@IssueKey("1801") +@WithServiceImplementations( { + @WithServiceImplementation( provides = BuilderProvider.class, value = Issue1801BuilderProvider.class), + @WithServiceImplementation( provides = AccessorNamingStrategy.class, value = ImmutablesAccessorNamingStrategy.class) +}) +public class Issue1801Test { + + @Test + public void shouldIncludeBuildeType() { + + ItemDTO item = ImmutableItemDTO.builder().id( "test" ).build(); + + Item target = ItemMapper.INSTANCE.map( item ); + + assertThat( target ).isNotNull(); + assertThat( target.getId() ).isEqualTo( "test" ); + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/ItemMapper.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/ItemMapper.java new file mode 100644 index 000000000..f43bf12ca --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/ItemMapper.java @@ -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.bugs._1801; + +import org.mapstruct.Builder; +import org.mapstruct.Mapper; +import org.mapstruct.ap.test.bugs._1801.domain.Item; +import org.mapstruct.ap.test.bugs._1801.dto.ItemDTO; +import org.mapstruct.factory.Mappers; + +/** + * @author Zhizhi Deng + */ +@Mapper( builder = @Builder) +public abstract class ItemMapper { + + public static final ItemMapper INSTANCE = Mappers.getMapper( ItemMapper.class ); + + public abstract Item map(ItemDTO itemDTO); +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java new file mode 100644 index 000000000..68231f12e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/ImmutableItem.java @@ -0,0 +1,154 @@ +/* + * 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.bugs._1801.domain; + +import java.util.ArrayList; +import java.util.List; + +/** + * Immutable implementation of {@link Item}. + *

+ * Superclass should expose a static subclass of the Builder to create immutable instance + * {@code public static Builder extends ImmutableItem.Builder}. + * + * @author Zhizhi Deng + */ +@SuppressWarnings({"all"}) +public final class ImmutableItem extends Item { + private final String id; + + private ImmutableItem(String id) { + this.id = id; + } + + /** + * @return The value of the {@code id} attribute + */ + @Override + public String getId() { + return id; + } + + /** + * Copy the current immutable object by setting a value for the {@link Item#getId() id} attribute. + * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}. + * @param value A new value for id + * @return A modified copy of the {@code this} object + */ + public final ImmutableItem withId(String value) { + if (this.id == value) return this; + return new ImmutableItem(value); + } + + /** + * This instance is equal to all instances of {@code ImmutableItem} that have equal attribute values. + * @return {@code true} if {@code this} is equal to {@code another} instance + */ + @Override + public boolean equals(Object another) { + if (this == another) return true; + return another instanceof ImmutableItem + && equalTo((ImmutableItem) another); + } + + private boolean equalTo(ImmutableItem another) { + return id.equals(another.id); + } + + /** + * Computes a hash code from attributes: {@code id}. + * @return hashCode value + */ + @Override + public int hashCode() { + int h = 5381; + h += (h << 5) + id.hashCode(); + return h; + } + + /** + * Prints the immutable value {@code Item} with attribute values. + * @return A string representation of the value + */ + @Override + public String toString() { + return "Item{" + + "id=" + id + + "}"; + } + + /** + * Creates an immutable copy of a {@link Item} value. + * Uses accessors to get values to initialize the new immutable instance. + * If an instance is already immutable, it is returned as is. + * @param instance The instance to copy + * @return A copied immutable Item instance + */ + public static ImmutableItem copyOf(Item instance) { + if (instance instanceof ImmutableItem) { + return (ImmutableItem) instance; + } + return new Builder() + .from(instance) + .build(); + } + + /** + * Builds instances of type {@link ImmutableItem ImmutableItem}. + *

{@code Builder} is not thread-safe and generally should not be stored in a field or collection, + * but instead used immediately to create instances. + */ + public static class Builder { + private static final long INIT_BIT_ID = 0x1L; + private long initBits = 0x1L; + + private String id; + + Builder() { + } + + /** + * Fill a builder with attribute values from the provided {@code Item} instance. + * Regular attribute values will be replaced with those from the given instance. + * Absent optional values will not replace present values. + * @param instance The instance from which to copy values + * @return {@code this} builder for use in a chained invocation + */ + public final Builder from(Item instance) { + id(instance.getId()); + return this; + } + + /** + * Initializes the value for the {@link Item#getId() id} attribute. + * @param id The value for id + * @return {@code this} builder for use in a chained invocation + */ + public final Builder id(String id) { + this.id = id; + initBits &= ~INIT_BIT_ID; + return this; + } + + /** + * Builds a new {@link ImmutableItem ImmutableItem}. + * @return An immutable instance of Item + * @throws java.lang.IllegalStateException if any required attributes are missing + */ + public ImmutableItem build() { + if (initBits != 0) { + throw new IllegalStateException(formatRequiredAttributesMessage()); + } + return new ImmutableItem(id); + } + + private String formatRequiredAttributesMessage() { + List attributes = new ArrayList(); + if ((initBits & INIT_BIT_ID) != 0) attributes.add("id"); + return "Cannot build Item, some of required attributes are not set " + attributes; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/Item.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/Item.java new file mode 100644 index 000000000..e0f389907 --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/domain/Item.java @@ -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.bugs._1801.domain; + +/** + * @author Zhizhi Deng + */ +public abstract class Item { + public abstract String getId(); + + public static class Builder extends ImmutableItem.Builder { } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ImmutableItemDTO.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ImmutableItemDTO.java new file mode 100644 index 000000000..bf10721aa --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ImmutableItemDTO.java @@ -0,0 +1,184 @@ +/* + * 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.bugs._1801.dto; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Immutable implementation of {@link ItemDTO}. + *

+ * Use the builder to create immutable instances: + * {@code ImmutableItemDTO.builder()}. + * + * @author Zhizhi Deng + */ +public final class ImmutableItemDTO extends ItemDTO { + private final String id; + + private ImmutableItemDTO(String id) { + this.id = id; + } + + /** + * @return The value of the {@code id} attribute + */ + @Override + public String getId() { + return id; + } + + /** + * Copy the current immutable object by setting a value for the {@link ItemDTO#getId() id} attribute. + * A shallow reference equality check is used to prevent copying of the same value by returning {@code this}. + * + * @param value A new value for id + * + * @return A modified copy of the {@code this} object + */ + public ImmutableItemDTO withId(String value) { + if ( Objects.equals( this.id, value ) ) { + return this; + } + return new ImmutableItemDTO( value ); + } + + /** + * This instance is equal to all instances of {@code ImmutableItemDTO} that have equal attribute values. + * + * @return {@code true} if {@code this} is equal to {@code another} instance + */ + @Override + public boolean equals(Object another) { + if ( this == another ) { + return true; + } + return another instanceof ImmutableItemDTO + && equalTo( (ImmutableItemDTO) another ); + } + + private boolean equalTo(ImmutableItemDTO another) { + return id.equals( another.id ); + } + + /** + * Computes a hash code from attributes: {@code id}. + * + * @return hashCode value + */ + @Override + public int hashCode() { + int h = 5381; + h += ( h << 5 ) + id.hashCode(); + return h; + } + + /** + * Prints the immutable value {@code ItemDTO} with attribute values. + * + * @return A string representation of the value + */ + @Override + public String toString() { + return "ItemDTO{" + + "id=" + id + + "}"; + } + + /** + * Creates an immutable copy of a {@link ItemDTO} value. + * Uses accessors to get values to initialize the new immutable instance. + * If an instance is already immutable, it is returned as is. + * + * @param instance The instance to copy + * + * @return A copied immutable ItemDTO instance + */ + public static ImmutableItemDTO copyOf(ItemDTO instance) { + if ( instance instanceof ImmutableItemDTO ) { + return (ImmutableItemDTO) instance; + } + return ImmutableItemDTO.builder() + .from( instance ) + .build(); + } + + /** + * Creates a builder for {@link ImmutableItemDTO ImmutableItemDTO}. + * + * @return A new ImmutableItemDTO builder + */ + public static ImmutableItemDTO.Builder builder() { + return new ImmutableItemDTO.Builder(); + } + + /** + * Builds instances of type {@link ImmutableItemDTO ImmutableItemDTO}. + * Initialize attributes and then invoke the {@link #build()} method to create an + * immutable instance. + *

{@code Builder} is not thread-safe and generally should not be stored in a field or collection, + * but instead used immediately to create instances. + */ + public static final class Builder { + private static final long INIT_BIT_ID = 0x1L; + private long initBits = 0x1L; + + private String id; + + private Builder() { + } + + /** + * Fill a builder with attribute values from the provided {@code ItemDTO} instance. + * Regular attribute values will be replaced with those from the given instance. + * Absent optional values will not replace present values. + * + * @param instance The instance from which to copy values + * + * @return {@code this} builder for use in a chained invocation + */ + public Builder from(ItemDTO instance) { + id( instance.getId() ); + return this; + } + + /** + * Initializes the value for the {@link ItemDTO#getId() id} attribute. + * + * @param id The value for id + * + * @return {@code this} builder for use in a chained invocation + */ + public Builder id(String id) { + this.id = id; + initBits &= ~INIT_BIT_ID; + return this; + } + + /** + * Builds a new {@link ImmutableItemDTO ImmutableItemDTO}. + * + * @return An immutable instance of ItemDTO + * + * @throws java.lang.IllegalStateException if any required attributes are missing + */ + public ImmutableItemDTO build() { + if ( initBits != 0 ) { + throw new IllegalStateException( formatRequiredAttributesMessage() ); + } + return new ImmutableItemDTO( id ); + } + + private String formatRequiredAttributesMessage() { + List attributes = new ArrayList<>(); + if ( ( initBits & INIT_BIT_ID ) != 0 ) { + attributes.add( "id" ); + } + return "Cannot build ItemDTO, some of required attributes are not set " + attributes; + } + } +} diff --git a/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ItemDTO.java b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ItemDTO.java new file mode 100644 index 000000000..1b8866b5e --- /dev/null +++ b/processor/src/test/java/org/mapstruct/ap/test/bugs/_1801/dto/ItemDTO.java @@ -0,0 +1,13 @@ +/* + * 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.bugs._1801.dto; + +/** + * @author Zhizhi Deng + */ +public abstract class ItemDTO { + public abstract String getId(); +}