Skip to content
Open
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 @@ -1015,8 +1015,8 @@ private ConstructorAccessor getConstructorAccessor(Type type) {
return primaryConstructor != null ? getConstructorAccessor( type, primaryConstructor ) : null;
}

List<ExecutableElement> constructors = ElementFilter.constructorsIn( type.getTypeElement()
.getEnclosedElements() );
List<ExecutableElement> constructors = ignoreDeprecatedConstructors( ElementFilter.constructorsIn(
type.getTypeElement().getEnclosedElements() ) );

// The rules for picking a constructor are the following:
// 1. Constructor annotated with @Default (from any package) has highest precedence
Expand Down Expand Up @@ -1099,6 +1099,48 @@ private ConstructorAccessor getConstructorAccessor(Type type) {

}

/**
* Removes the deprecated constructors from the given constructors, so that they do not take precedence over
* the constructors that are meant to be used.
* <p>
* Kotlin compiler plugins generate constructors that are hidden from Kotlin and marked as deprecated. The
* {@code no-arg} plugin (applied by {@code kotlin("plugin.jpa")}) for example adds a parameterless
* constructor to every annotated class. Such a constructor must not win over the constructor that is declared
* in the class, since for a class with {@code val} properties the declared constructor is the only way to
* populate the object.
*
* @param constructors the constructors of the type
* @return the constructors that should be taken into account. The deprecated constructors are only kept when
* there is no accessible non-deprecated constructor to fall back to, or when they are explicitly annotated
* with {@code @Default}
*/
private List<ExecutableElement> ignoreDeprecatedConstructors(List<ExecutableElement> constructors) {
boolean hasNonDeprecatedCandidate = false;
for ( ExecutableElement constructor : constructors ) {
if ( !constructor.getModifiers().contains( Modifier.PRIVATE ) && !isDeprecated( constructor ) ) {
hasNonDeprecatedCandidate = true;
break;
}
}

if ( !hasNonDeprecatedCandidate ) {
return constructors;
}

List<ExecutableElement> candidates = new ArrayList<>( constructors.size() );
for ( ExecutableElement constructor : constructors ) {
if ( !isDeprecated( constructor ) || hasDefaultAnnotationFromAnyPackage( constructor ) ) {
candidates.add( constructor );
}
}

return candidates;
}

private boolean isDeprecated(ExecutableElement constructor) {
return ctx.getElementUtils().isDeprecated( constructor );
}

private ConstructorAccessor getConstructorAccessor(Type type, ExecutableElement constructor) {
List<Parameter> constructorParameters = ctx.getTypeFactory()
.getParameters( (DeclaredType) type.getTypeMirror(), constructor );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* 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.constructor.deprecated;

/**
* @author Oleg Babichev
*/
public class AddressDto {

private String street;
private String city;

public AddressDto(String street, String city) {
this.street = street;
this.city = city;
}

public String getStreet() {
return street;
}

public String getCity() {
return city;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* 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.constructor.deprecated;

/**
* The shape that kapt generates for a Kotlin JPA entity since Kotlin 2.3.0.
* <p>
* For
* <pre>
* &#64;Entity class AddressEntity(val street: String, val city: String)
* </pre>
* the {@code no-arg} compiler plugin, applied by {@code kotlin("plugin.jpa")}, adds the parameterless constructor
* that JPA requires. Until Kotlin 2.3.0 it was missing from the stubs (KT-53122), so MapStruct never saw it. The
* generated constructor carries {@code @java.lang.Deprecated} and is not synthetic, which is exactly what javac
* produces for the declaration below.
* <p>
* Since the properties of the entity are {@code val}, there are no setters: the declared constructor is the only way
* to populate the object.
*
* @author Oleg Babichev
*/
public class AddressEntity {

private final String street;
private final String city;

@Deprecated
public AddressEntity() {
this( null, null );
}

public AddressEntity(String street, String city) {
this.street = street;
this.city = city;
}

public String getStreet() {
return street;
}

public String getCity() {
return city;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.constructor.deprecated;

import java.util.List;

import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

/**
* Only the mapping between the lists is declared, so the mapping of the elements has to be generated. That is where
* the compilation fails when the deprecated parameterless constructor is used for {@link AddressEntity}, because the
* entity has no write accessors at all then.
*
* @author Oleg Babichev
*/
@Mapper
public interface AddressEntityMapper {

AddressEntityMapper INSTANCE = Mappers.getMapper( AddressEntityMapper.class );

List<AddressEntity> map(List<AddressDto> dtos);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.constructor.deprecated;

import java.util.List;

import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses;

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

/**
* A type with a deprecated parameterless constructor next to the constructor that is declared for its properties has
* to be mapped through the declared constructor.
* <p>
* Without that, the compilation fails with
* {@code No target bean properties found: can't map Collection element "AddressDto addressDto" to
* "AddressEntity addressEntity"}, which is what Kotlin JPA entities started to run into with Kotlin 2.3.0.
*
* @author Oleg Babichev
* @see <a href="https://github.com/mapstruct/mapstruct/issues/3960">#3960</a>
*/
@WithClasses({
AddressDto.class,
AddressEntity.class,
AddressEntityMapper.class
})
class DeprecatedConstructorTest {

@ProcessorTest
void shouldMapThroughTheDeclaredConstructor() {
List<AddressEntity> entities = AddressEntityMapper.INSTANCE.map(
List.of( new AddressDto( "Sesame Street", "New York" ) )
);

assertThat( entities ).hasSize( 1 );
assertThat( entities.get( 0 ).getStreet() ).isEqualTo( "Sesame Street" );
assertThat( entities.get( 0 ).getCity() ).isEqualTo( "New York" );
}
}