Skip to content
Merged
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
6 changes: 6 additions & 0 deletions NEXT_RELEASE_CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
### Features

* Support for Java 21 Sequenced Collections (#3240)
* Improved support for Kotlin. Requires use of `org.jetbrains.kotlin:kotlin-metadata-jvm`.
- Data Classes (#2281, #2577, #3031) - MapStruct now properly handles:
- Single field data classes
- Proper primary constructor detection
- Data classes with multiple constructors
- Data classes with all default parameters


### Enhancements
Expand Down
5 changes: 5 additions & 0 deletions distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
<groupId>org.mapstruct.tools.gem</groupId>
<artifactId>gem-api</artifactId>
</dependency>
<!-- Needed here so references to Kotlin classes can be resolved during JavaDoc generation -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
</dependency>

<dependency>
<groupId>jakarta.xml.bind</groupId>
Expand Down
71 changes: 65 additions & 6 deletions documentation/src/main/asciidoc/chapter-2-set-up.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ For Maven based projects add the following to your POM file in order to use MapS
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<version>3.14.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
Expand Down Expand Up @@ -141,10 +141,10 @@ When invoking javac directly, these options are passed to the compiler in the fo
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version>
<version>3.14.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<source>${java.version}</source>
<target>${java.version}</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
Expand Down Expand Up @@ -323,3 +323,62 @@ Some features include:

* Code completion in `target` and `source`
* Quick Fixes

[[kotlin-setup]]
=== Kotlin Support

MapStruct provides support for Kotlin interoperability with Java.

When using MapStruct with Kotlin, it's recommended to add the `org.jetbrains.kotlin:kotlin-metadata-jvm` library to enable proper introspection of Kotlin metadata:

.Maven configuration for Kotlin support
====
[source, xml, linenums]
[subs="verbatim,attributes"]
----
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<version>${kotlin.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
----
====

.Gradle configuration for Kotlin support
====
[source, groovy, linenums]
[subs="verbatim,attributes"]
----
dependencies {
implementation "org.mapstruct:mapstruct:${mapstructVersion}"

annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
annotationProcessor "org.jetbrains.kotlin:kotlin-metadata-jvm:${kotlinVersion}"
}
----
====

[NOTE]
====
The `org.jetbrains.kotlin:kotlin-metadata-jvm` dependency is optional but highly recommended when working with Kotlin and Java.
Without it, MapStruct will still work but may not handle complex Kotlin scenarios optimally.
====
5 changes: 5 additions & 0 deletions parent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
Expand Down
5 changes: 5 additions & 0 deletions processor/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@
<artifactId>mapstruct</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-metadata-jvm</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-compiler-embeddable</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
import org.mapstruct.ap.internal.util.accessor.ElementAccessor;
import org.mapstruct.ap.internal.util.accessor.PresenceCheckAccessor;
import org.mapstruct.ap.internal.util.accessor.ReadAccessor;
import org.mapstruct.ap.internal.util.kotlin.KotlinMetadata;

import static org.mapstruct.ap.internal.model.beanmapping.MappingReferences.forSourceMethod;
import static org.mapstruct.ap.internal.util.Collections.first;
Expand Down Expand Up @@ -904,6 +905,27 @@ private ConstructorAccessor getConstructorAccessor(Type type) {
return new ConstructorAccessor( parameterBindings, constructorAccessors );
}

KotlinMetadata kotlinMetadata = type.getKotlinMetadata();
if ( kotlinMetadata != null && kotlinMetadata.isDataClass() ) {
List<ExecutableElement> constructors = ElementFilter.constructorsIn( type.getTypeElement()
.getEnclosedElements() );

for ( ExecutableElement constructor : constructors ) {
if ( constructor.getModifiers().contains( Modifier.PRIVATE ) ) {
continue;
}

// prefer constructor annotated with @Default
if ( hasDefaultAnnotationFromAnyPackage( constructor ) ) {
return getConstructorAccessor( type, constructor );
}
}

ExecutableElement primaryConstructor = kotlinMetadata.determinePrimaryConstructor( constructors );

return primaryConstructor != null ? getConstructorAccessor( type, primaryConstructor ) : null;
}

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.SimpleTypeVisitor8;

import kotlin.Metadata;
import kotlin.metadata.Attributes;
import kotlin.metadata.KmClass;
import kotlin.metadata.KmConstructor;
import kotlin.metadata.jvm.JvmExtensionsKt;
import kotlin.metadata.jvm.JvmMethodSignature;
import kotlin.metadata.jvm.KotlinClassMetadata;
import org.mapstruct.ap.internal.gem.CollectionMappingStrategyGem;
import org.mapstruct.ap.internal.util.AccessorNamingUtils;
import org.mapstruct.ap.internal.util.ElementUtils;
Expand All @@ -58,6 +65,7 @@
import org.mapstruct.ap.internal.util.accessor.MapValueAccessor;
import org.mapstruct.ap.internal.util.accessor.PresenceCheckAccessor;
import org.mapstruct.ap.internal.util.accessor.ReadAccessor;
import org.mapstruct.ap.internal.util.kotlin.KotlinMetadata;

import static java.util.Collections.emptyList;
import static org.mapstruct.ap.internal.util.Collections.first;
Expand All @@ -75,6 +83,7 @@
*/
public class Type extends ModelElement implements Comparable<Type> {
private static final Method SEALED_PERMITTED_SUBCLASSES_METHOD;
private static final boolean KOTLIN_METADATA_JVM_PRESENT;

static {
Method permittedSubclassesMethod;
Expand All @@ -85,6 +94,16 @@ public class Type extends ModelElement implements Comparable<Type> {
permittedSubclassesMethod = null;
}
SEALED_PERMITTED_SUBCLASSES_METHOD = permittedSubclassesMethod;

boolean kotlinMetadataJvmPresent;
try {
Class.forName( "kotlin.metadata.jvm.KotlinClassMetadata", false, ModelElement.class.getClassLoader() );
kotlinMetadataJvmPresent = true;
}
catch ( ClassNotFoundException e ) {
kotlinMetadataJvmPresent = false;
}
KOTLIN_METADATA_JVM_PRESENT = kotlinMetadataJvmPresent;
}

private final TypeUtils typeUtils;
Expand Down Expand Up @@ -139,6 +158,8 @@ public class Type extends ModelElement implements Comparable<Type> {
private Type boxedEquivalent = null;

private Boolean hasAccessibleConstructor;
private KotlinMetadata kotlinMetadata;
private boolean kotlinMetadataInitialized;

private final Filters filters;

Expand Down Expand Up @@ -1370,6 +1391,23 @@ public boolean hasAccessibleConstructor() {
return hasAccessibleConstructor;
}

public KotlinMetadata getKotlinMetadata() {
if ( !kotlinMetadataInitialized ) {
kotlinMetadataInitialized = true;
if ( typeElement != null && KOTLIN_METADATA_JVM_PRESENT ) {
Metadata metadataAnnotation = typeElement.getAnnotation( Metadata.class );
if ( metadataAnnotation != null ) {
KotlinClassMetadata classMetadata = KotlinClassMetadata.readLenient( metadataAnnotation );
if ( classMetadata instanceof KotlinClassMetadata.Class ) {
kotlinMetadata = new KotlinMetadataImpl( (KotlinClassMetadata.Class) classMetadata );
}
}
}
}

return kotlinMetadata;
}

/**
* Returns the direct supertypes of a type. The interface types, if any,
* will appear last in the list.
Expand Down Expand Up @@ -1831,4 +1869,122 @@ public List<? extends TypeMirror> getPermittedSubclasses() {
}
}

private class KotlinMetadataImpl implements KotlinMetadata {

private final KotlinClassMetadata.Class kotlinClassMetadata;

private KotlinMetadataImpl(KotlinClassMetadata.Class kotlinClassMetadata) {
this.kotlinClassMetadata = kotlinClassMetadata;
}

@Override
public boolean isDataClass() {
return Attributes.isData( kotlinClassMetadata.getKmClass() );
}

@Override
public ExecutableElement determinePrimaryConstructor(List<ExecutableElement> constructors) {
if ( constructors.size() == 1 ) {
// If we have one constructor, that this constructor is the primary one
return constructors.get( 0 );
}
KmClass kmClass = kotlinClassMetadata.getKmClass();
KmConstructor primaryKmConstructor = null;
for ( KmConstructor constructor : kmClass.getConstructors() ) {
if ( !Attributes.isSecondary( constructor ) ) {
primaryKmConstructor = constructor;
}

}

if ( primaryKmConstructor == null ) {
return null;
}

List<ExecutableElement> sameParametersSizeConstructors = new ArrayList<>();
for ( ExecutableElement constructor : constructors ) {
if ( constructor.getParameters().size() == primaryKmConstructor.getValueParameters().size() ) {
sameParametersSizeConstructors.add( constructor );
}
}

if ( sameParametersSizeConstructors.size() == 1 ) {
return sameParametersSizeConstructors.get( 0 );
}

JvmMethodSignature signature = JvmExtensionsKt.getSignature( primaryKmConstructor );
if ( signature == null ) {
return null;
}

String signatureDescriptor = signature.getDescriptor();
for ( ExecutableElement constructor : constructors ) {
String constructorDescriptor = buildJvmConstructorDescriptor( constructor );
if ( signatureDescriptor.equals( constructorDescriptor ) ) {
return constructor;
}
}

return null;
}

private String buildJvmConstructorDescriptor(ExecutableElement constructor) {
StringBuilder signature = new StringBuilder( "(" );

for ( VariableElement param : constructor.getParameters() ) {
signature.append( getJvmTypeDescriptor( param.asType() ) );
}

signature.append( ")V" );
return signature.toString();
}

private String getJvmTypeDescriptor(TypeMirror type) {
return type.accept(
new SimpleTypeVisitor8<String, Void>() {
@Override
public String visitPrimitive(PrimitiveType t, Void p) {
switch ( t.getKind() ) {
case BOOLEAN:
return "Z";
case BYTE:
return "B";
case SHORT:
return "S";
case INT:
return "I";
case LONG:
return "J";
case CHAR:
return "C";
case FLOAT:
return "F";
case DOUBLE:
return "D";
default:
return "";
}
}

@Override
public String visitDeclared(DeclaredType t, Void p) {
TypeElement element = (TypeElement) t.asElement();
String binaryName = elementUtils.getBinaryName( element ).toString();
return "L" + binaryName.replace( '.', '/' ) + ";";
}

@Override
public String visitArray(ArrayType t, Void p) {
return "[" + getJvmTypeDescriptor( t.getComponentType() );
}

@Override
protected String defaultAction(TypeMirror e, Void p) {
return "";
}
}, null
);
}
}

}
Loading