diff --git a/copyright.txt b/copyright.txt
index d6dc8d1b8a..ed1c0a8dda 100644
--- a/copyright.txt
+++ b/copyright.txt
@@ -38,6 +38,7 @@ Joshua Spoerri - https://github.com/spoerri
Jude Niroshan - https://github.com/JudeNiroshan
Justyna Kubica-Ledzion - https://github.com/JKLedzion
Kemal Özcan - https://github.com/yekeoe
+Ken Wang - https://github.com/ro0sterjam
Kevin Grüneberg - https://github.com/kevcodez
Lukas Lazar - https://github.com/LukeLaz
Nikolas Charalambidis - https://github.com/Nikolas-Charalambidis
@@ -69,4 +70,4 @@ Tomek Gubala - https://github.com/vgtworld
Valentin Kulesh - https://github.com/unshare
Vincent Alexander Beelte - https://github.com/grandmasterpixel
Winter Andreas - https://github.dev/wandi34
-Xiu Hong Kooi - https://github.com/kooixh
+Xiu Hong Kooi - https://github.com/kooixh
\ No newline at end of file
diff --git a/core/src/main/java/org/mapstruct/NullValueMappingStrategy.java b/core/src/main/java/org/mapstruct/NullValueMappingStrategy.java
index 519a28bd81..d978750400 100644
--- a/core/src/main/java/org/mapstruct/NullValueMappingStrategy.java
+++ b/core/src/main/java/org/mapstruct/NullValueMappingStrategy.java
@@ -28,6 +28,7 @@ public enum NullValueMappingStrategy {
* case.
*
+ * - For {@code Optional} MapStruct generates an {@code Optional.empty()}
* - For {@code List} MapStruct generates an {@code ArrayList}
* - For {@code Map} a {@code HashMap}
* - For arrays an empty array
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/InitDefaultValue.java b/processor/src/main/java/org/mapstruct/ap/internal/model/InitDefaultValue.java
new file mode 100644
index 0000000000..086f87ead9
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/InitDefaultValue.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.internal.model;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import org.mapstruct.ap.internal.model.common.ModelElement;
+import org.mapstruct.ap.internal.model.common.Type;
+
+/**
+ * Model element to generate code that initializes a default value for a given type.
+ *
+ * Uses the provided factory method if available.
+ * Otherwise, constructs the target object using a sensible default including but not limited to:
+ * - an empty array for array types
+ * - an empty collection for collection types
+ * - an empty optional for optional types
+ * - using the public default constructor for other types if available
+ *
+ *
If no default value can be constructed, a null is returned instead.
+ * TODO: Consider throwing an exception instead of returning null.
+ *
+ * @author Ken Wang
+ */
+public class InitDefaultValue extends ModelElement {
+
+ private final Type targetType;
+ private final MethodReference factoryMethod;
+
+ public InitDefaultValue(Type targetType, MethodReference factoryMethod) {
+ this.targetType = targetType;
+ this.factoryMethod = factoryMethod;
+ }
+
+ @Override
+ public Set getImportTypes() {
+ Set types = new HashSet<>();
+ types.add( targetType );
+ return types;
+ }
+
+ public Type getTargetType() {
+ return targetType;
+ }
+
+ public MethodReference getFactoryMethod() {
+ return factoryMethod;
+ }
+}
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java
index 79cd0e0566..fc893d4439 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/NormalTypeMappingMethod.java
@@ -67,6 +67,10 @@ public MethodReference getFactoryMethod() {
return this.factoryMethod;
}
+ public InitDefaultValue getInitDefaultValueForResultType() {
+ return new InitDefaultValue( this.getResultType(), this.getFactoryMethod() );
+ }
+
public List getAnnotations() {
return annotations;
}
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/OptionalMappingMethod.java b/processor/src/main/java/org/mapstruct/ap/internal/model/OptionalMappingMethod.java
new file mode 100644
index 0000000000..abc7cd3225
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/OptionalMappingMethod.java
@@ -0,0 +1,122 @@
+/*
+ * 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.internal.model;
+
+import org.mapstruct.ap.internal.model.common.Assignment;
+import org.mapstruct.ap.internal.model.common.Type;
+import org.mapstruct.ap.internal.model.source.Method;
+import org.mapstruct.ap.internal.model.source.SelectionParameters;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.mapstruct.ap.internal.util.Collections.first;
+
+/**
+ * Maps from a source to a target where one or the other (or both) are an {@link Optional} type.
+ *
+ * @author Ken Wang
+ */
+public class OptionalMappingMethod extends ContainerMappingMethod {
+
+ public static class Builder extends ContainerMappingMethodBuilder {
+
+ public Builder() {
+ super( Builder.class, "optional element" );
+ }
+
+ @Override
+ protected Type getElementType(Type parameterType) {
+ if ( parameterType.isOptionalType() ) {
+ return parameterType.getTypeParameters().get( 0 );
+ }
+ else {
+ return parameterType;
+ }
+ }
+
+ @Override
+ protected Assignment getWrapper(Assignment assignment, Method method) {
+ return assignment;
+ }
+
+ @Override
+ protected OptionalMappingMethod instantiateMappingMethod(Method method, Collection existingVariables,
+ Assignment assignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName,
+ List beforeMappingMethods,
+ List afterMappingMethods, SelectionParameters selectionParameters) {
+ return new OptionalMappingMethod(
+ method,
+ getMethodAnnotations(),
+ existingVariables,
+ assignment,
+ factoryMethod,
+ mapNullToDefault,
+ loopVariableName,
+ beforeMappingMethods,
+ afterMappingMethods,
+ selectionParameters
+ );
+ }
+ }
+
+ private OptionalMappingMethod(Method method,
+ List annotations,
+ Collection existingVariables,
+ Assignment parameterAssignment,
+ MethodReference factoryMethod,
+ boolean mapNullToDefault,
+ String loopVariableName,
+ List beforeMappingReferences,
+ List afterMappingReferences,
+ SelectionParameters selectionParameters) {
+ super(
+ method,
+ annotations,
+ existingVariables,
+ parameterAssignment,
+ factoryMethod,
+ mapNullToDefault,
+ loopVariableName,
+ beforeMappingReferences,
+ afterMappingReferences,
+ selectionParameters
+ );
+ }
+
+ @Override
+ public Set getImportTypes() {
+ Set types = super.getImportTypes();
+
+ types.add( getSourceElementType() );
+ return types;
+ }
+
+ public Type getSourceElementType() {
+ Type sourceParameterType = getSourceParameter().getType();
+
+ if ( sourceParameterType.isOptionalType() ) {
+ return first( sourceParameterType.determineTypeArguments( Optional.class ) ).getTypeBound();
+ }
+ else {
+ return sourceParameterType;
+ }
+ }
+
+ @Override
+ public Type getResultElementType() {
+ Type resultParameterType = getResultType();
+
+ if ( resultParameterType.isOptionalType() ) {
+ return first( resultParameterType.determineTypeArguments( Optional.class ) );
+ }
+ else {
+ return resultParameterType;
+ }
+ }
+}
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java
index 50f23ecbff..29fab5fef5 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/PropertyMapping.java
@@ -316,6 +316,9 @@ else if ( ( sourceType.isIterableType() && targetType.isStreamType() )
|| ( sourceType.isStreamType() && targetType.isIterableType() ) ) {
assignment = forgeStreamMapping( sourceType, targetType, rightHandSide );
}
+ else if ( sourceType.isOptionalType() || targetType.isOptionalType() ) {
+ assignment = forgeOptionalMapping( sourceType, targetType, rightHandSide );
+ }
else {
assignment = forgeMapping( rightHandSide );
}
@@ -755,6 +758,24 @@ private Assignment forgeWithElementMapping(Type sourceType, Type targetType, Sou
return createForgedAssignment( source, methodRef, iterableMappingMethod );
}
+ private Assignment forgeOptionalMapping(Type sourceType, Type targetType, SourceRHS source) {
+
+ targetType = targetType.withoutBounds();
+ ForgedMethod methodRef = prepareForgedMethod( sourceType, targetType, source, "?" );
+
+ OptionalMappingMethod.Builder builder = new OptionalMappingMethod.Builder();
+
+ ContainerMappingMethod optionalMappingMethod = builder
+ .mappingContext( ctx )
+ .method( methodRef )
+ .selectionParameters( selectionParameters )
+ .callingContextTargetPropertyName( targetPropertyName )
+ .positionHint( positionHint )
+ .build();
+
+ return createForgedAssignment( source, methodRef, optionalMappingMethod );
+ }
+
private ForgedMethod prepareForgedMethod(Type sourceType, Type targetType, SourceRHS source, String suffix) {
String name = getName( sourceType, targetType );
name = Strings.getSafeVariableName( name, ctx.getReservedNames() );
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java
index 73ec84b6b7..9b63da938c 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/beanmapping/SourceReference.java
@@ -120,7 +120,8 @@ public SourceReference build() {
);
}
- String[] segments = sourceNameTrimmed.split( "\\." );
+ // Split by "." but also include "?" as a separate segment for optionals
+ String[] segments = sourceNameTrimmed.split( "\\.|(?=\\?)" );
// start with an invalid source reference
SourceReference result = new SourceReference( null, new ArrayList<>( ), false );
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java
index f742e1ba0f..4e3c2ef274 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/Type.java
@@ -52,6 +52,7 @@
import org.mapstruct.ap.internal.util.accessor.AccessorType;
import org.mapstruct.ap.internal.util.accessor.FieldElementAccessor;
import org.mapstruct.ap.internal.util.accessor.MapValueAccessor;
+import org.mapstruct.ap.internal.util.accessor.OptionalValueAccessor;
import org.mapstruct.ap.internal.util.accessor.PresenceCheckAccessor;
import org.mapstruct.ap.internal.util.accessor.ReadAccessor;
@@ -108,6 +109,8 @@ public class Type extends ModelElement implements Comparable {
private final boolean isMapType;
private final boolean isVoid;
private final boolean isStream;
+
+ private final boolean isOptional;
private final boolean isLiteral;
private final boolean loggingVerbose;
@@ -135,6 +138,7 @@ public class Type extends ModelElement implements Comparable {
private Type boxedEquivalent = null;
private Boolean hasAccessibleConstructor;
+ private Boolean hasAccessibleDefaultConstructor;
private final Filters filters;
@@ -146,7 +150,7 @@ public Type(TypeUtils typeUtils, ElementUtils elementUtils, TypeFactory typeFact
String packageName, String name, String qualifiedName,
boolean isInterface, boolean isEnumType, boolean isIterableType,
boolean isCollectionType, boolean isMapType, boolean isStreamType,
- Map toBeImportedTypes,
+ boolean isOptionalType, Map toBeImportedTypes,
Map notToBeImportedTypes,
Boolean isToBeImported,
boolean isLiteral, boolean loggingVerbose) {
@@ -172,6 +176,7 @@ public Type(TypeUtils typeUtils, ElementUtils elementUtils, TypeFactory typeFact
this.isCollectionType = isCollectionType;
this.isMapType = isMapType;
this.isStream = isStreamType;
+ this.isOptional = isOptionalType;
this.isVoid = typeMirror.getKind() == TypeKind.VOID;
this.isLiteral = isLiteral;
@@ -231,7 +236,7 @@ public String getName() {
/**
* Returns a String that could be used in generated code to reference to this {@link Type}.
- *
+ *
* The first time a name is referred-to it will be marked as to be imported. For instance
* {@code LocalDateTime} can be one of {@code java.time.LocalDateTime} and {@code org.joda.LocalDateTime})
*
@@ -259,7 +264,7 @@ public String createReferenceName() {
return name;
}
- if ( isTopLevelTypeToBeImported() && nameWithTopLevelTypeName != null) {
+ if ( isTopLevelTypeToBeImported() && nameWithTopLevelTypeName != null ) {
return nameWithTopLevelTypeName;
}
@@ -368,7 +373,7 @@ public boolean isArrayType() {
}
public boolean isTypeVar() {
- return (typeMirror.getKind() == TypeKind.TYPEVAR);
+ return ( typeMirror.getKind() == TypeKind.TYPEVAR );
}
public boolean isIntersection() {
@@ -392,6 +397,10 @@ public boolean isStreamType() {
return isStream;
}
+ public boolean isOptionalType() {
+ return isOptional;
+ }
+
/**
* A wild card type can have two types of bounds (mutual exclusive): extends and super.
*
@@ -422,7 +431,7 @@ public boolean hasExtendsBound() {
/**
* A type variable type can have two types of bounds (mutual exclusive): lower and upper.
- *
+ *
* Note that its use is only permitted on a definition (not on the place where its used). For instance:
* {@code T map( T in)}
*
@@ -439,7 +448,7 @@ public boolean hasLowerBound() {
/**
* A type variable type can have two types of bounds (mutual exclusive): lower and upper.
- *
+ *
* Note that its use is only permitted on a definition (not on the place where its used). For instance:
* {@code> T map( T in)}
*
@@ -554,6 +563,7 @@ public Type erasure() {
isCollectionType,
isMapType,
isStream,
+ isOptional,
toBeImportedTypes,
notToBeImportedTypes,
isToBeImported,
@@ -597,6 +607,7 @@ public Type withoutBounds() {
isCollectionType,
isMapType,
isStream,
+ isOptional,
toBeImportedTypes,
notToBeImportedTypes,
isToBeImported,
@@ -625,7 +636,6 @@ private Type replaceGeneric(Type oldGenericType, Type newType) {
* as well.
*
* @param other The other type.
- *
* @return {@code true} if and only if this type is assignable to the given other type.
*/
public boolean isAssignableTo(Type other) {
@@ -644,7 +654,6 @@ public boolean isAssignableTo(Type other) {
* they need to be resolved first.
*
* @param other The other type.
- *
* @return {@code true} if and only if this type is assignable to the given other type.
*/
public boolean isRawAssignableTo(Type other) {
@@ -659,6 +668,7 @@ public boolean isRawAssignableTo(Type other) {
/**
* removes any bounds from this type.
+ *
* @return the raw type
*/
public Type asRawType() {
@@ -680,6 +690,15 @@ public ReadAccessor getReadAccessor(String propertyName, boolean allowedMapToBea
.orElse( null );
return new MapValueAccessor( getMethod, typeParameters.get( 1 ).getTypeMirror(), propertyName );
}
+ else if ( propertyName.equals( "?" ) && isOptionalType() ) {
+ ExecutableElement getMethod = getAllMethods()
+ .stream()
+ .filter( m -> m.getSimpleName().contentEquals( "orElse" ) )
+ .filter( m -> m.getParameters().size() == 1 )
+ .findAny()
+ .orElse( null );
+ return new OptionalValueAccessor( getMethod, typeParameters.get( 0 ).getTypeMirror(), propertyName );
+ }
Map readAccessors = getPropertyReadAccessors();
@@ -704,7 +723,7 @@ public Map getPropertyReadAccessors() {
if ( readAccessors == null ) {
Map recordAccessors = filters.recordAccessorsIn( getRecordComponents() );
- Map modifiableGetters = new LinkedHashMap<>(recordAccessors);
+ Map modifiableGetters = new LinkedHashMap<>( recordAccessors );
List getterList = filters.getterMethodsIn( getAllMethods() );
for ( ReadAccessor getter : getterList ) {
@@ -782,7 +801,7 @@ public Map getPropertyPresenceCheckers() {
* @param cmStrategy collection mapping strategy
* @return an unmodifiable map of all write accessors indexed by property name
*/
- public Map getPropertyWriteAccessors( CollectionMappingStrategyGem cmStrategy ) {
+ public Map getPropertyWriteAccessors(CollectionMappingStrategyGem cmStrategy) {
// collect all candidate target accessors
List candidates = new ArrayList<>( getSetters() );
candidates.addAll( getAlternativeTargetAccessors() );
@@ -828,7 +847,7 @@ else if ( candidate.getAccessorType() == AccessorType.GETTER ) {
continue;
}
}
- else if ( candidate.getAccessorType() == AccessorType.FIELD && ( Executables.isFinal( candidate ) ||
+ else if ( candidate.getAccessorType() == AccessorType.FIELD && ( Executables.isFinal( candidate ) ||
result.containsKey( targetPropertyName ) ) ) {
// if the candidate is a field and a mapping already exists, then use that one, skip it.
continue;
@@ -865,7 +884,7 @@ private Type determineTargetType(Accessor candidate) {
return parameter.getType();
}
else if ( candidate.getAccessorType() == AccessorType.GETTER
- || candidate.getAccessorType().isFieldAssignment() ) {
+ || candidate.getAccessorType().isFieldAssignment() ) {
return typeFactory.getReturnType( (DeclaredType) typeMirror, candidate );
}
return null;
@@ -895,7 +914,7 @@ private List nullSafeTypeElementListConversion(Function
* Matching occurs on:
*
* - The generic type parameter type of the collection should match the adder method argument
@@ -923,7 +942,6 @@ private String getPropertyName(ExecutableElement element) {
*
* @param collectionProperty property type (assumed collection) to find the adder method for
* @param pluralPropertyName the property name (assumed plural)
- *
* @return corresponding adder method for getter when present
*/
private Accessor getAdderForType(Type collectionProperty, String pluralPropertyName) {
@@ -962,9 +980,8 @@ else if ( collectionProperty.isStreamType() ) {
* Returns all accessor candidates that start with "add" and have exactly one argument
* whose type matches the collection or stream property's type argument.
*
- * @param property the collection or stream property
+ * @param property the collection or stream property
* @param superclass the superclass to use for type argument lookup
- *
* @return accessor candidates
*/
private List getAccessorCandidates(Type property, Class> superclass) {
@@ -1023,7 +1040,7 @@ private List getAdders() {
* Alternative accessors could be a getter for a collection. By means of the
* {@link java.util.Collection#addAll(java.util.Collection) } this getter can still
* be used as targetAccessor. JAXB XJC tool generates such constructs.
- *
+ *
* This method can be extended when new cases come along.
*
* @return an unmodifiable list of alternative target accessors.
@@ -1109,7 +1126,6 @@ private boolean isSubType(TypeMirror candidate, Class> clazz) {
* the other type. Returns {@code 1}, if the other type is a direct super type of this type, and so on.
*
* @param assignableOther the other type
- *
* @return the length of the shortest path in the type hierarchy between this type and the specified other type
*/
public int distanceTo(Type assignableOther) {
@@ -1138,7 +1154,7 @@ private int distanceTo(TypeMirror base, TypeMirror targetType) {
}
/**
- * @param type the type declaring the method
+ * @param type the type declaring the method
* @param method the method to check
* @return Whether this type can access the given method declared on the given type.
*/
@@ -1159,7 +1175,7 @@ else if ( !method.getModifiers().contains( Modifier.PUBLIC ) ) {
/**
* @return A valid Java expression most suitable for representing null - useful for dealing with primitives from
- * FTL.
+ * FTL.
*/
public String getNull() {
if ( !isPrimitive() || isArrayType() ) {
@@ -1173,7 +1189,7 @@ public String getNull() {
}
if ( "char".equals( getName() ) ) {
//"'\u0000'" would have been better, but depends on platform encoding
- return "0";
+ return "0";
}
if ( "double".equals( getName() ) ) {
return "0.0d";
@@ -1200,6 +1216,9 @@ public String getSensibleDefault() {
else if ( "String".equals( getName() ) ) {
return "\"\"";
}
+ else if ( isOptionalType() ) {
+ return "Optional.empty()";
+ }
else {
if ( isNative() ) {
// must be boxed, since primitive is already checked
@@ -1216,8 +1235,8 @@ public int hashCode() {
// are in another jar than the mapper. So the qualfiedName is a better candidate.
final int prime = 31;
int result = 1;
- result = prime * result + ((name == null) ? 0 : name.hashCode());
- result = prime * result + ((packageName == null) ? 0 : packageName.hashCode());
+ result = prime * result + ( ( name == null ) ? 0 : name.hashCode() );
+ result = prime * result + ( ( packageName == null ) ? 0 : packageName.hashCode() );
return result;
}
@@ -1235,7 +1254,7 @@ public boolean equals(Object obj) {
Type other = (Type) obj;
if ( this.isWildCardBoundByTypeVar() && other.isWildCardBoundByTypeVar() ) {
- return ( this.hasExtendsBound() == this.hasExtendsBound()
+ return ( this.hasExtendsBound() == this.hasExtendsBound()
|| this.hasSuperBound() == this.hasSuperBound() )
&& typeUtils.isSameType( getTypeBound().getTypeMirror(), other.getTypeBound().getTypeMirror() );
}
@@ -1276,7 +1295,6 @@ public String describe() {
}
/**
- *
* @return an identification that can be used as part in a forged method name.
*/
public String getIdentification() {
@@ -1296,6 +1314,7 @@ public String getIdentification() {
*
- {@code >}, returns Object
* - {@code , returns Number}
*
+ *
* @return the bound for this parameter
*/
public Type getTypeBound() {
@@ -1345,6 +1364,20 @@ public boolean hasAccessibleConstructor() {
return hasAccessibleConstructor;
}
+ public boolean hasAccessibleDefaultConstructor() {
+ if ( hasAccessibleDefaultConstructor == null ) {
+ hasAccessibleDefaultConstructor = false;
+ List constructors = ElementFilter.constructorsIn( typeElement.getEnclosedElements() );
+ for ( ExecutableElement constructor : constructors ) {
+ if ( constructor.isDefault() && !constructor.getModifiers().contains( Modifier.PRIVATE ) ) {
+ hasAccessibleDefaultConstructor = true;
+ break;
+ }
+ }
+ }
+ return hasAccessibleDefaultConstructor;
+ }
+
/**
* Returns the direct supertypes of a type. The interface types, if any,
* will appear last in the list.
@@ -1383,6 +1416,7 @@ public List determineTypeArguments(Class> superclass) {
/**
* All primitive types and their corresponding boxed types are considered native.
+ *
* @return true when native.
*/
public boolean isNative() {
@@ -1412,12 +1446,11 @@ public boolean isLiteral() {
* }
*
*
- * @param declared the type
+ * @param declared the type
* @param parameterized the parameterized type
- *
* @return - the same type when this is not a type var in the broadest sense (T, T[], or ? extends T)
- * - the matching parameter in the parameterized type when this is a type var when found
- * - null in all other cases
+ * - the matching parameter in the parameterized type when this is a type var when found
+ * - null in all other cases
*/
public ResolvedPair resolveParameterToType(Type declared, Type parameterized) {
if ( isTypeVar() || isArrayTypeVar() || isWildCardBoundByTypeVar() ) {
@@ -1476,7 +1509,7 @@ public ResolvedPair resolveParameterToType(Type declared, Type parameterized) {
* result: Map
* }
*
- *
+ *
* Mismatch result examples:
*
* {@code
@@ -1492,12 +1525,11 @@ public ResolvedPair resolveParameterToType(Type declared, Type parameterized) {
* }
*
*
- * @param declared the type
+ * @param declared the type
* @param parameterized the parameterized type
- *
* @return - the result of {@link #resolveParameterToType(Type, Type)} when this type itself is a type var.
- * - the type but then with the matching type parameters replaced.
- * - the same type when this type does not contain matching type parameters.
+ * - the type but then with the matching type parameters replaced.
+ * - the same type when this type does not contain matching type parameters.
*/
public Type resolveGenericTypeParameters(Type declared, Type parameterized) {
if ( isTypeVar() || isArrayTypeVar() || isWildCardBoundByTypeVar() ) {
@@ -1522,7 +1554,7 @@ public boolean isWildCardBoundByTypeVar() {
}
public boolean isArrayTypeVar() {
- return isArrayType() && getComponentType().isTypeVar();
+ return isArrayType() && getComponentType().isTypeVar();
}
private static class TypeVarMatcher extends SimpleTypeVisitor8 {
@@ -1533,7 +1565,7 @@ private static class TypeVarMatcher extends SimpleTypeVisitor8 results = new ArrayList<>( );
+ List results = new ArrayList<>();
if ( parameterized.getTypeArguments().isEmpty() ) {
return super.DEFAULT_VALUE;
}
@@ -1623,7 +1655,7 @@ else if ( types.isSameType( types.erasure( parameterized ), types.erasure( decla
continue;
}
ResolvedPair result = visitDeclared( parameterized, declaredSuperType );
- if ( result != super.DEFAULT_VALUE ) {
+ if ( result != super.DEFAULT_VALUE ) {
results.add( result );
}
}
@@ -1633,7 +1665,7 @@ else if ( types.isSameType( types.erasure( parameterized ), types.erasure( decla
continue;
}
ResolvedPair result = visitDeclared( (DeclaredType) parameterizedSuper, declared );
- if ( result != super.DEFAULT_VALUE ) {
+ if ( result != super.DEFAULT_VALUE ) {
results.add( result );
}
}
@@ -1649,7 +1681,7 @@ else if ( types.isSameType( types.erasure( parameterized ), types.erasure( decla
private boolean isJavaLangObject(TypeMirror type) {
if ( type instanceof DeclaredType ) {
return ( (TypeElement) ( (DeclaredType) type ).asElement() ).getQualifiedName()
- .contentEquals( Object.class.getName() );
+ .contentEquals( Object.class.getName() );
}
return false;
}
@@ -1722,7 +1754,7 @@ else if ( isPrimitive() ) {
/**
* It strips all the {@code []} from the {@code className}.
- *
+ *
* E.g.
*
* trimSimpleClassName("String[][][]") -> "String"
@@ -1730,7 +1762,6 @@ else if ( isPrimitive() ) {
*
*
* @param className that needs to be trimmed
- *
* @return the trimmed {@code className}, or {@code null} if the {@code className} was {@code null}
*/
private String trimSimpleClassName(String className) {
@@ -1793,9 +1824,9 @@ public boolean isSealed() {
/**
* return the list of permitted TypeMirrors for the java 17+ sealed class
*/
- @SuppressWarnings( "unchecked" )
+ @SuppressWarnings("unchecked")
public List extends TypeMirror> getPermittedSubclasses() {
- if (SEALED_PERMITTED_SUBCLASSES_METHOD == null) {
+ if ( SEALED_PERMITTED_SUBCLASSES_METHOD == null ) {
return emptyList();
}
try {
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java b/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java
index f28e0c687e..97d6212c64 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/common/TypeFactory.java
@@ -16,6 +16,7 @@
import java.util.Map;
import java.util.NavigableMap;
import java.util.NavigableSet;
+import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
@@ -92,6 +93,7 @@ public class TypeFactory {
private final TypeMirror collectionType;
private final TypeMirror mapType;
private final TypeMirror streamType;
+ private final TypeMirror optionalType;
private final Map implementationTypes = new HashMap<>();
private final Map toBeImportedTypes = new HashMap<>();
@@ -113,6 +115,7 @@ public TypeFactory(ElementUtils elementUtils, TypeUtils typeUtils, FormattingMes
mapType = typeUtils.erasure( elementUtils.getTypeElement( Map.class.getCanonicalName() ).asType() );
TypeElement streamTypeElement = elementUtils.getTypeElement( JavaStreamConstants.STREAM_FQN );
streamType = streamTypeElement == null ? null : typeUtils.erasure( streamTypeElement.asType() );
+ optionalType = typeUtils.erasure( elementUtils.getTypeElement( Optional.class.getCanonicalName() ).asType() );
implementationTypes.put( Iterable.class.getName(), withInitialCapacity( getType( ArrayList.class ) ) );
implementationTypes.put( Collection.class.getName(), withInitialCapacity( getType( ArrayList.class ) ) );
@@ -221,6 +224,7 @@ private Type getType(TypeMirror mirror, boolean isLiteral, Boolean alwaysImport)
boolean isCollectionType = typeUtils.isSubtypeErased( mirror, collectionType );
boolean isMapType = typeUtils.isSubtypeErased( mirror, mapType );
boolean isStreamType = streamType != null && typeUtils.isSubtypeErased( mirror, streamType );
+ boolean isOptionalType = typeUtils.isSubtypeErased( mirror, optionalType );
boolean isEnumType;
boolean isInterface;
@@ -330,6 +334,7 @@ else if ( mirror.getKind() == TypeKind.TYPEVAR ) {
isCollectionType,
isMapType,
isStreamType,
+ isOptionalType,
toBeImportedTypes,
notToBeImportedTypes,
toBeImported,
@@ -556,6 +561,7 @@ private ImplementationType getImplementationType(TypeMirror mirror) {
implementationType.isCollectionType(),
implementationType.isMapType(),
implementationType.isStreamType(),
+ implementationType.isOptionalType(),
toBeImportedTypes,
notToBeImportedTypes,
null,
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.java b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.java
new file mode 100644
index 0000000000..26430c2385
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.java
@@ -0,0 +1,71 @@
+/*
+ * 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.internal.model.presence;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+import org.mapstruct.ap.internal.model.common.ModelElement;
+import org.mapstruct.ap.internal.model.common.PresenceCheck;
+import org.mapstruct.ap.internal.model.common.Type;
+
+/**
+ * Presence checker for {@link java.util.Optional} types.
+ *
+ * @author Ken Wang
+ */
+public class OptionalPresenceCheck extends ModelElement implements PresenceCheck {
+
+ private final String sourceReference;
+ private final boolean negate;
+
+ public OptionalPresenceCheck(String sourceReference) {
+ this.sourceReference = sourceReference;
+ this.negate = false;
+ }
+
+ public OptionalPresenceCheck(String sourceReference, boolean negate) {
+ this.sourceReference = sourceReference;
+ this.negate = negate;
+ }
+
+ public String getSourceReference() {
+ return sourceReference;
+ }
+
+ @Override
+ public Set getImportTypes() {
+ return Collections.emptySet();
+ }
+
+ public boolean isNegate() {
+ return negate;
+ }
+
+ @Override
+ public PresenceCheck negate() {
+ return new OptionalPresenceCheck( sourceReference, !negate );
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ OptionalPresenceCheck that = (OptionalPresenceCheck) o;
+ return Objects.equals( sourceReference, that.sourceReference );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( sourceReference );
+ }
+
+}
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/Strings.java b/processor/src/main/java/org/mapstruct/ap/internal/util/Strings.java
index 88e0bdf6f6..54a034c357 100644
--- a/processor/src/main/java/org/mapstruct/ap/internal/util/Strings.java
+++ b/processor/src/main/java/org/mapstruct/ap/internal/util/Strings.java
@@ -148,6 +148,7 @@ public static String getSafeVariableName(String name, String... existingVariable
* any Java keyword; starting with a lower-case letter
*/
public static String getSafeVariableName(String name, Collection existingVariableNames) {
+ name = name.equals( "?" ) ? "optionalValue" : name;
name = decapitalize( sanitizeIdentifierName( name ) );
name = joinAndCamelize( extractParts( name ) );
diff --git a/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/OptionalValueAccessor.java b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/OptionalValueAccessor.java
new file mode 100644
index 0000000000..37ed2b88e5
--- /dev/null
+++ b/processor/src/main/java/org/mapstruct/ap/internal/util/accessor/OptionalValueAccessor.java
@@ -0,0 +1,60 @@
+/*
+ * 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.internal.util.accessor;
+
+import java.util.Collections;
+import java.util.Set;
+import javax.lang.model.element.Element;
+import javax.lang.model.element.Modifier;
+import javax.lang.model.type.TypeMirror;
+
+/**
+ * An {@link Accessor} for retrieving the value inside an {@link java.util.Optional}.
+ *
+ * @author Ken Wang
+ */
+public class OptionalValueAccessor implements ReadAccessor {
+
+ private final TypeMirror valueTypeMirror;
+ private final String simpleName;
+ private final Element element;
+
+ public OptionalValueAccessor(Element element, TypeMirror valueTypeMirror, String simpleName) {
+ this.element = element;
+ this.valueTypeMirror = valueTypeMirror;
+ this.simpleName = simpleName;
+ }
+
+ @Override
+ public TypeMirror getAccessedType() {
+ return valueTypeMirror;
+ }
+
+ @Override
+ public String getSimpleName() {
+ return this.simpleName;
+ }
+
+ @Override
+ public Set getModifiers() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Element getElement() {
+ return this.element;
+ }
+
+ @Override
+ public AccessorType getAccessorType() {
+ return AccessorType.GETTER;
+ }
+
+ @Override
+ public String getReadValueSource() {
+ return "orElse( null )";
+ }
+}
diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl
index 8fb5bfdef0..1481052e77 100644
--- a/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl
+++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/BeanMappingMethod.ftl
@@ -117,11 +117,12 @@
#if>
#list>
<#else>
- <#if mapNullToDefault>if ( <@includeModel object=getPresenceCheckByParameter(sourceParameters[0]) /> ) {#if>
+ <#assign propertyMappings = propertyMappingsByParameter(sourceParameters[0])/>
+ <#if mapNullToDefault && (propertyMappings?size > 0)>if ( <@includeModel object=getPresenceCheckByParameter(sourceParameters[0]) /> ) {#if>
<#list propertyMappingsByParameter(sourceParameters[0]) as propertyMapping>
<@includeModel object=propertyMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping defaultValueAssignment=propertyMapping.defaultValueAssignment/>
#list>
- <#if mapNullToDefault>}#if>
+ <#if mapNullToDefault && (propertyMappings?size > 0)>}#if>
#if>
<#list constantMappings as constantMapping>
<@includeModel object=constantMapping targetBeanName=resultName existingInstanceMapping=existingInstanceMapping/>
diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/InitDefaultValue.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/InitDefaultValue.ftl
new file mode 100644
index 0000000000..1adc810517
--- /dev/null
+++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/InitDefaultValue.ftl
@@ -0,0 +1,32 @@
+<#--
+
+ Copyright MapStruct Authors.
+
+ Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.InitDefaultValue" -->
+<#if factoryMethod??>
+ <@includeModel object=factoryMethod targetType=targetType/>
+<#else>
+ <@constructTargetObject/>
+#if>
+<#--
+ macro: constructTargetObject
+
+ purpose: Either call the constructor of the target object directly or of the implementing type.
+ If the target type is an Object but does not have a public default constructor, null is returned.
+-->
+<#macro constructTargetObject><@compress single_line=true>
+ <#if targetType.implementationType?? && targetType.implementationType.hasAccessibleDefaultConstructor()>
+ new <@includeModel object=targetType.implementationType/>()
+ <#elseif targetType.arrayType>
+ new <@includeModel object=targetType.componentType/>[0]
+ <#elseif targetType.sensibleDefault??>
+ ${targetType.sensibleDefault}
+ <#elseif targetType.hasAccessibleDefaultConstructor()>
+ new <@includeModel object=targetType/>()
+ <#else>
+ null
+ #if>
+@compress>#macro>
\ No newline at end of file
diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/NestedPropertyMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/NestedPropertyMappingMethod.ftl
index 8eabe23a16..7a54a6d029 100644
--- a/processor/src/main/resources/org/mapstruct/ap/internal/model/NestedPropertyMappingMethod.ftl
+++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/NestedPropertyMappingMethod.ftl
@@ -21,7 +21,11 @@
<#if !entry.presenceChecker?? >
<#if !entry.type.primitive>
if ( ${entry.name} == null ) {
- return ${returnType.null};
+ <#if returnType.isOptionalType()>
+ return Optional.empty();
+ <#else>
+ return ${returnType.null};
+ #if>
}
#if>
#if>
diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/OptionalMappingMethod.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/OptionalMappingMethod.ftl
new file mode 100644
index 0000000000..401f40fee5
--- /dev/null
+++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/OptionalMappingMethod.ftl
@@ -0,0 +1,48 @@
+<#--
+
+ Copyright MapStruct Authors.
+
+ Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.OptionalMappingMethod" -->
+<#if overridden>@Override#if>
+<#lt>${accessibility.keyword} <@includeModel object=returnType/> ${name}(<#list parameters as param><@includeModel object=param/><#if param_has_next>, #if>#list>)<@throws/> {
+ <#list beforeMappingReferencesWithoutMappingTarget as callback>
+ <@includeModel object=callback targetBeanName=resultName targetType=resultType/>
+ <#if !callback_has_next>
+
+ #if>
+ #list>
+ if ( <@includeModel object=sourceParameterPresenceCheck.negate() /> ) {
+ <#if resultType.optionalType || mapNullToDefault>
+ return <@includeModel object=initDefaultValueForResultType/>;
+ <#else>
+ return<#if returnType.name != "void"> null#if>;
+ #if>
+ }
+
+ <#if sourceParameter.type.optionalType>
+ <@includeModel object=returnType/> ${resultName} = ${sourceParameter.name}.map( ${loopVariableName} -> <@includeModel object=elementAssignment/> )<#if !returnType.optionalType>.orElse( <#if mapNullToDefault><@includeModel object=initDefaultValueForResultType/><#else>null#if> )#if>;
+ <#else>
+ <@includeModel object=sourceElementType/> ${loopVariableName} = ${sourceParameter.name};
+ <@includeModel object=returnType/> ${resultName} = Optional.ofNullable( <@includeModel object=elementAssignment/> );
+ #if>
+
+ <#list afterMappingReferences as callback>
+ <#if callback_index = 0>
+
+ #if>
+ <@includeModel object=callback targetBeanName=resultName targetType=resultType/>
+ #list>
+
+ return ${resultName};
+}
+<#macro throws>
+ <#if (thrownTypes?size > 0)><#lt> throws #if><@compress single_line=true>
+ <#list thrownTypes as exceptionType>
+ <@includeModel object=exceptionType/>
+ <#if exceptionType_has_next>, #if><#t>
+ #list>
+ @compress>
+#macro>
\ No newline at end of file
diff --git a/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.ftl b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.ftl
new file mode 100644
index 0000000000..36a763ca5f
--- /dev/null
+++ b/processor/src/main/resources/org/mapstruct/ap/internal/model/presence/OptionalPresenceCheck.ftl
@@ -0,0 +1,9 @@
+<#--
+
+ Copyright MapStruct Authors.
+
+ Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
+
+-->
+<#-- @ftlvariable name="" type="org.mapstruct.ap.internal.model.presence.OptionalPresenceCheck" -->
+${sourceReference}<#if isNegate()>.isEmpty()<#else>.isPresent()#if>
\ No newline at end of file
diff --git a/processor/src/test/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactoryTest.java b/processor/src/test/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactoryTest.java
index 646f898063..2194c04d99 100755
--- a/processor/src/test/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactoryTest.java
+++ b/processor/src/test/java/org/mapstruct/ap/internal/model/common/DateFormatValidatorFactoryTest.java
@@ -174,6 +174,7 @@ private Type typeWithFQN(String fullQualifiedName) {
false,
false,
false,
+ false,
new HashMap<>( ),
new HashMap<>( ),
false,
diff --git a/processor/src/test/java/org/mapstruct/ap/internal/model/common/DefaultConversionContextTest.java b/processor/src/test/java/org/mapstruct/ap/internal/model/common/DefaultConversionContextTest.java
index ce2f895e0f..91b1c87f5a 100755
--- a/processor/src/test/java/org/mapstruct/ap/internal/model/common/DefaultConversionContextTest.java
+++ b/processor/src/test/java/org/mapstruct/ap/internal/model/common/DefaultConversionContextTest.java
@@ -122,6 +122,7 @@ private Type typeWithFQN(String fullQualifiedName) {
false,
false,
false,
+ false,
new HashMap<>( ),
new HashMap<>( ),
false,
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/OptionalBeforeAfterMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/OptionalBeforeAfterMapper.java
new file mode 100644
index 0000000000..f8c9a15af1
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/OptionalBeforeAfterMapper.java
@@ -0,0 +1,76 @@
+/*
+ * 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.optionalmapping.beforeafter;
+
+import java.util.Optional;
+
+import org.mapstruct.AfterMapping;
+import org.mapstruct.BeforeMapping;
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.TargetType;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface OptionalBeforeAfterMapper {
+
+ OptionalBeforeAfterMapper INSTANCE = Mappers.getMapper( OptionalBeforeAfterMapper.class );
+
+ Target toTarget(Source source);
+
+ @BeforeMapping
+ default void beforeDeepOptionalSourceWithNoTargetType(Optional source) {
+ }
+
+ @BeforeMapping
+ default void beforeDeepOptionalSourceWithNonOptionalTargetType(@TargetType Class targetType,
+ Optional source) {
+ }
+
+ @AfterMapping
+ default void afterDeepOptionalSourceWithNoTarget(Optional source) {
+
+ }
+
+ @AfterMapping
+ default void afterDeepOptionalSourceWithNonOptionalTarget(@MappingTarget Target.SubType target,
+ Optional source) {
+ }
+
+ @AfterMapping
+ default void afterDeepOptionalSourceWithOptionalTarget(@MappingTarget Optional target,
+ Optional source) {
+ }
+
+ @AfterMapping
+ default void afterDeepNonOptionalSourceOptionalTarget(@MappingTarget Optional target,
+ Source.SubType source) {
+ }
+
+ @BeforeMapping
+ default void beforeShallowOptionalSourceWithNoTargetType(Optional source) {
+ }
+
+ @BeforeMapping
+ default void beforeShallowOptionalSourceWithNonOptionalTargetType(@TargetType Class targetType,
+ Optional source) {
+ }
+
+ @AfterMapping
+ default void afterShallowOptionalSourceWithNoTarget(Optional source) {
+
+ }
+
+ @AfterMapping
+ default void afterShallowOptionalSourceWithNonOptionalTarget(@MappingTarget String target,
+ Optional source) {
+ }
+
+ @AfterMapping
+ default void afterShallowNonOptionalSourceOptionalTarget(@MappingTarget Optional target, String source) {
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/OptionalBeforeAfterTest.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/OptionalBeforeAfterTest.java
new file mode 100644
index 0000000000..6a6b043313
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/OptionalBeforeAfterTest.java
@@ -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.optionalmapping.beforeafter;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+@WithClasses({
+ OptionalBeforeAfterMapper.class, Source.class, Target.class
+})
+public class OptionalBeforeAfterTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource()
+ .addComparisonToFixtureFor( OptionalBeforeAfterMapper.class );
+
+ @ProcessorTest
+ public void dummyTest() {
+
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/Source.java
new file mode 100644
index 0000000000..4d87be0e04
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/Source.java
@@ -0,0 +1,87 @@
+/*
+ * 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.optionalmapping.beforeafter;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Source {
+
+ private final Optional deepOptionalToOptional;
+ private final Optional deepOptionalToNonOptional;
+ private final SubType deepNonOptionalToOptional;
+
+ private final Optional shallowOptionalToOptional;
+ private final Optional shallowOptionalToNonOptional;
+ private final String shallowNonOptionalToOptional;
+
+ public Source(Optional deepOptionalToOptional, Optional deepOptionalToNonOptional,
+ SubType deepNonOptionalToOptional, Optional shallowOptionalToOptional,
+ Optional shallowOptionalToNonOptional, String shallowNonOptionalToOptional) {
+ this.deepOptionalToOptional = deepOptionalToOptional;
+ this.deepOptionalToNonOptional = deepOptionalToNonOptional;
+ this.deepNonOptionalToOptional = deepNonOptionalToOptional;
+ this.shallowOptionalToOptional = shallowOptionalToOptional;
+ this.shallowOptionalToNonOptional = shallowOptionalToNonOptional;
+ this.shallowNonOptionalToOptional = shallowNonOptionalToOptional;
+ }
+
+ public Optional getDeepOptionalToOptional() {
+ return deepOptionalToOptional;
+ }
+
+ public Optional getDeepOptionalToNonOptional() {
+ return deepOptionalToNonOptional;
+ }
+
+ public SubType getDeepNonOptionalToOptional() {
+ return deepNonOptionalToOptional;
+ }
+
+ public Optional getShallowOptionalToOptional() {
+ return shallowOptionalToOptional;
+ }
+
+ public Optional getShallowOptionalToNonOptional() {
+ return shallowOptionalToNonOptional;
+ }
+
+ public String getShallowNonOptionalToOptional() {
+ return shallowNonOptionalToOptional;
+ }
+
+ public static class SubType {
+
+ private final String value;
+
+ public SubType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ SubType subType = (SubType) o;
+ return Objects.equals( value, subType.value );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( value );
+ }
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/Target.java
new file mode 100644
index 0000000000..a93b4b31e7
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/beforeafter/Target.java
@@ -0,0 +1,86 @@
+/*
+ * 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.optionalmapping.beforeafter;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Target {
+
+ private final Optional deepOptionalToOptional;
+ private final SubType deepOptionalToNonOptional;
+ private final Optional deepNonOptionalToOptional;
+
+ private final Optional shallowOptionalToOptional;
+ private final String shallowOptionalToNonOptional;
+ private final Optional shallowNonOptionalToOptional;
+
+ public Target(Optional deepOptionalToOptional, SubType deepOptionalToNonOptional,
+ Optional deepNonOptionalToOptional, Optional shallowOptionalToOptional,
+ String shallowOptionalToNonOptional, Optional shallowNonOptionalToOptional) {
+ this.deepOptionalToOptional = deepOptionalToOptional;
+ this.deepOptionalToNonOptional = deepOptionalToNonOptional;
+ this.deepNonOptionalToOptional = deepNonOptionalToOptional;
+ this.shallowOptionalToOptional = shallowOptionalToOptional;
+ this.shallowOptionalToNonOptional = shallowOptionalToNonOptional;
+ this.shallowNonOptionalToOptional = shallowNonOptionalToOptional;
+ }
+
+ public Optional getDeepOptionalToOptional() {
+ return deepOptionalToOptional;
+ }
+
+ public SubType getDeepOptionalToNonOptional() {
+ return deepOptionalToNonOptional;
+ }
+
+ public Optional getDeepNonOptionalToOptional() {
+ return deepNonOptionalToOptional;
+ }
+
+ public Optional getShallowOptionalToOptional() {
+ return shallowOptionalToOptional;
+ }
+
+ public String getShallowOptionalToNonOptional() {
+ return shallowOptionalToNonOptional;
+ }
+
+ public Optional getShallowNonOptionalToOptional() {
+ return shallowNonOptionalToOptional;
+ }
+
+ public static class SubType {
+
+ private final String value;
+
+ public SubType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ SubType subType = (SubType) o;
+ return Objects.equals( value, subType.value );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( value );
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/OptionalDifferentTypesMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/OptionalDifferentTypesMapper.java
new file mode 100644
index 0000000000..88f4d2e966
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/OptionalDifferentTypesMapper.java
@@ -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.optionalmapping.differenttypes;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface OptionalDifferentTypesMapper {
+
+ OptionalDifferentTypesMapper INSTANCE = Mappers.getMapper(
+ OptionalDifferentTypesMapper.class );
+
+ Target toTarget(Source source);
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/OptionalDifferentTypesTest.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/OptionalDifferentTypesTest.java
new file mode 100644
index 0000000000..aae100ce72
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/OptionalDifferentTypesTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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.optionalmapping.differenttypes;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WithClasses({
+ OptionalDifferentTypesMapper.class, Source.class, Target.class
+})
+public class OptionalDifferentTypesTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource()
+ .addComparisonToFixtureFor( OptionalDifferentTypesMapper.class );
+
+ @ProcessorTest
+ public void constructorOptionalToOptionalWhenPresent() {
+ Source source = new Source( Optional.of( new Source.SubType( "some value" ) ), null, null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToOptional() ).contains( new Target.SubType( "some value" ) );
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToOptionalWhenEmpty() {
+ Source source = new Source( Optional.empty(), null, null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToNonOptionalWhenPresent() {
+ Source source = new Source( null, Optional.of( new Source.SubType( "some value" ) ), null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToNonOptional() ).isEqualTo( new Target.SubType( "some value" ) );
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToNonOptionalWhenEmpty() {
+ Source source = new Source( null, Optional.empty(), null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToNonOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void constructorNonOptionalToOptionalWhenNotNull() {
+ Source source = new Source( null, null, new Source.SubType( "some value" ) );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorNonOptionalToOptional() ).contains( new Target.SubType( "some value" ) );
+ }
+
+ @ProcessorTest
+ public void constructorNonOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorNonOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void optionalToOptionalWhenPresent() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToOptional( Optional.of( new Source.SubType( "some value" ) ) );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).contains( new Target.SubType( "some value" ) );
+ }
+
+ @ProcessorTest
+ public void optionalToOptionalWhenEmpty() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToOptional( Optional.empty() );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void optionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToOptional( null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenPresent() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToNonOptional( Optional.of( new Source.SubType( "some value" ) ) );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isEqualTo( new Target.SubType( "some value" ) );
+ }
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenEmpty() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToNonOptional( Optional.empty() );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToNonOptional( null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nonOptionalToOptionalWhenNotNull() {
+ Source source = new Source( null, null, null );
+ source.setNonOptionalToOptional( new Source.SubType( "some value" ) );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToOptional() ).contains( new Target.SubType( "some value" ) );
+ }
+
+ @ProcessorTest
+ public void nonOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.setNonOptionalToOptional( null );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToOptionalWhenPresent() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToOptional = Optional.of( new Source.SubType( "some value" ) );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToOptional ).contains( new Target.SubType( "some value" ) );
+ }
+
+ @ProcessorTest
+ public void publicOptionalToOptionalWhenEmpty() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToOptional = Optional.empty();
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToOptional ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToOptional = null;
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToOptional ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToNonOptionalWhenPresent() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToNonOptional = Optional.of( new Source.SubType( "some value" ) );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToNonOptional ).isEqualTo( new Target.SubType( "some value" ) );
+ }
+
+ @ProcessorTest
+ public void publicOptionalToNonOptionalWhenEmpty() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToNonOptional = Optional.empty();
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToNonOptional ).isNull();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToNonOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToNonOptional = null;
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToNonOptional ).isNull();
+ }
+
+ @ProcessorTest
+ public void publicNonOptionalToOptionalWhenNotNull() {
+ Source source = new Source( null, null, null );
+ source.publicNonOptionalToOptional = new Source.SubType( "some value" );
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicNonOptionalToOptional ).contains( new Target.SubType( "some value" ) );
+ }
+
+ @ProcessorTest
+ public void publicNonOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.publicNonOptionalToOptional = null;
+
+ Target target = OptionalDifferentTypesMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicNonOptionalToOptional ).isEmpty();
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/Source.java
new file mode 100644
index 0000000000..0c9c92811d
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/Source.java
@@ -0,0 +1,102 @@
+/*
+ * 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.optionalmapping.differenttypes;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Source {
+
+ private final Optional constructorOptionalToOptional;
+ private final Optional constructorOptionalToNonOptional;
+ private final SubType constructorNonOptionalToOptional;
+
+ private Optional optionalToOptional;
+ private Optional optionalToNonOptional;
+ private SubType nonOptionalToOptional;
+
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToNonOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public SubType publicNonOptionalToOptional;
+
+ public Source(Optional constructorOptionalToOptional, Optional constructorOptionalToNonOptional,
+ SubType constructorNonOptionalToOptional) {
+ this.constructorOptionalToOptional = constructorOptionalToOptional;
+ this.constructorOptionalToNonOptional = constructorOptionalToNonOptional;
+ this.constructorNonOptionalToOptional = constructorNonOptionalToOptional;
+ }
+
+ public Optional getConstructorOptionalToOptional() {
+ return constructorOptionalToOptional;
+ }
+
+ public Optional getConstructorOptionalToNonOptional() {
+ return constructorOptionalToNonOptional;
+ }
+
+ public SubType getConstructorNonOptionalToOptional() {
+ return constructorNonOptionalToOptional;
+ }
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public Optional getOptionalToNonOptional() {
+ return optionalToNonOptional;
+ }
+
+ public void setOptionalToNonOptional(Optional optionalToNonOptional) {
+ this.optionalToNonOptional = optionalToNonOptional;
+ }
+
+ public SubType getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(SubType nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+
+ public static class SubType {
+
+ private final String value;
+
+ public SubType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ SubType subType = (SubType) o;
+ return Objects.equals( value, subType.value );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( value );
+ }
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/Target.java
new file mode 100644
index 0000000000..7784e8ee52
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/differenttypes/Target.java
@@ -0,0 +1,101 @@
+/*
+ * 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.optionalmapping.differenttypes;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Target {
+
+ private final Optional constructorOptionalToOptional;
+ private final SubType constructorOptionalToNonOptional;
+ private final Optional constructorNonOptionalToOptional;
+
+ private Optional optionalToOptional;
+ private SubType optionalToNonOptional;
+ private Optional nonOptionalToOptional;
+
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public SubType publicOptionalToNonOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicNonOptionalToOptional;
+
+ public Target(Optional constructorOptionalToOptional, SubType constructorOptionalToNonOptional,
+ Optional constructorNonOptionalToOptional) {
+ this.constructorOptionalToOptional = constructorOptionalToOptional;
+ this.constructorOptionalToNonOptional = constructorOptionalToNonOptional;
+ this.constructorNonOptionalToOptional = constructorNonOptionalToOptional;
+ }
+
+ public Optional getConstructorOptionalToOptional() {
+ return constructorOptionalToOptional;
+ }
+
+ public SubType getConstructorOptionalToNonOptional() {
+ return constructorOptionalToNonOptional;
+ }
+
+ public Optional getConstructorNonOptionalToOptional() {
+ return constructorNonOptionalToOptional;
+ }
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public SubType getOptionalToNonOptional() {
+ return optionalToNonOptional;
+ }
+
+ public void setOptionalToNonOptional(SubType optionalToNonOptional) {
+ this.optionalToNonOptional = optionalToNonOptional;
+ }
+
+ public Optional getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(Optional nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+
+ public static class SubType {
+
+ private final String value;
+
+ public SubType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ Target.SubType subType = (Target.SubType) o;
+ return Objects.equals( value, subType.value );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( value );
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/OptionalNestedMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/OptionalNestedMapper.java
new file mode 100644
index 0000000000..59ae1b1ee2
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/OptionalNestedMapper.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.optionalmapping.nested;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.Mapping;
+import org.mapstruct.factory.Mappers;
+
+@Mapper
+public interface OptionalNestedMapper {
+
+ OptionalNestedMapper INSTANCE = Mappers.getMapper( OptionalNestedMapper.class );
+
+ @Mapping(source = "optionalToNonOptional?.value?", target = "optionalToNonOptional")
+ @Mapping(source = "optionalToOptional?.value", target = "optionalToOptional")
+ @Mapping(source = "nonOptionalToNonOptional?.value", target = "nonOptionalToNonOptional")
+ @Mapping(source = "nonOptionalToOptional?.value", target = "nonOptionalToOptional")
+ Target toTarget(Source source);
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/OptionalNestedTest.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/OptionalNestedTest.java
new file mode 100644
index 0000000000..f9c0edc0cc
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/OptionalNestedTest.java
@@ -0,0 +1,188 @@
+/*
+ * 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.optionalmapping.nested;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WithClasses({
+ OptionalNestedMapper.class, Source.class, Target.class
+})
+public class OptionalNestedTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource()
+ .addComparisonToFixtureFor( OptionalNestedMapper.class );
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenEmpty() {
+ Source source = new Source();
+ source.setOptionalToNonOptional( Optional.empty() );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenNull() {
+ Source source = new Source();
+ source.setOptionalToNonOptional( null );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nestedOptionalToNonOptionalWhenEmpty() {
+ Source source = new Source();
+ source.setOptionalToNonOptional( Optional.of( new Source.NestedOptional( Optional.empty() ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nestedOptionalToNonOptionalWhenNull() {
+ Source source = new Source();
+ source.setOptionalToNonOptional( Optional.of( new Source.NestedOptional( null ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nestedOptionalToNonOptionalWhenNotNull() {
+ Source source = new Source();
+ source.setOptionalToNonOptional( Optional.of( new Source.NestedOptional( Optional.of( "some value" ) ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isEqualTo( "some value" );
+ }
+
+ @ProcessorTest
+ public void optionalToOptionalWhenEmpty() {
+ Source source = new Source();
+ source.setOptionalToOptional( Optional.empty() );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void optionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.setOptionalToOptional( null );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void nestedOptionalToOptionalWhenEmpty() {
+ Source source = new Source();
+ source.setOptionalToOptional( Optional.of( new Source.NestedOptional( Optional.empty() ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void nestedOptionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.setOptionalToOptional( Optional.of( new Source.NestedOptional( null ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nestedOptionalToOptionalWhenNotNull() {
+ Source source = new Source();
+ source.setOptionalToOptional( Optional.of( new Source.NestedOptional( Optional.of( "some value" ) ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).contains( "some value" );
+ }
+
+ @ProcessorTest
+ public void nonOptionalToNonOptionalWhenEmpty() {
+ Source source = new Source();
+ source.setNonOptionalToNonOptional( Optional.empty() );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nonOptionalToNonOptionalWhenNull() {
+ Source source = new Source();
+ source.setNonOptionalToNonOptional( null );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nestedNonOptionalToNonOptionalWhenNull() {
+ Source source = new Source();
+ source.setNonOptionalToNonOptional( Optional.of( new Source.NestedNonOptional( null ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nestedNonOptionalToNonOptionalWhenNotNull() {
+ Source source = new Source();
+ source.setNonOptionalToNonOptional( Optional.of( new Source.NestedNonOptional( "some value" ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToNonOptional() ).isEqualTo( "some value" );
+ }
+
+ @ProcessorTest
+ public void nonOptionalToOptionalWhenEmpty() {
+ Source source = new Source();
+ source.setNonOptionalToOptional( Optional.empty() );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void nonOptionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.setNonOptionalToOptional( null );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void nestedNonOptionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.setNonOptionalToOptional( Optional.of( new Source.NestedNonOptional( null ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void nestedNonOptionalToOptionalWhenNotNull() {
+ Source source = new Source();
+ source.setNonOptionalToOptional( Optional.of( new Source.NestedNonOptional( "some value" ) ) );
+
+ Target target = OptionalNestedMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToOptional() ).contains( "some value" );
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/Source.java
new file mode 100644
index 0000000000..efafb1fb6b
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/Source.java
@@ -0,0 +1,76 @@
+/*
+ * 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.optionalmapping.nested;
+
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Source {
+
+ private Optional optionalToNonOptional;
+ private Optional optionalToOptional;
+ private Optional nonOptionalToNonOptional;
+ private Optional nonOptionalToOptional;
+
+ public Optional getOptionalToNonOptional() {
+ return optionalToNonOptional;
+ }
+
+ public void setOptionalToNonOptional(Optional optionalToNonOptional) {
+ this.optionalToNonOptional = optionalToNonOptional;
+ }
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public Optional getNonOptionalToNonOptional() {
+ return nonOptionalToNonOptional;
+ }
+
+ public void setNonOptionalToNonOptional(Optional nonOptionalToNonOptional) {
+ this.nonOptionalToNonOptional = nonOptionalToNonOptional;
+ }
+
+ public Optional getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(Optional nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+
+ public static class NestedOptional {
+
+ private final Optional value;
+
+ public NestedOptional(Optional value) {
+ this.value = value;
+ }
+
+ public Optional getValue() {
+ return value;
+ }
+ }
+
+ public static class NestedNonOptional {
+
+ private final String value;
+
+ public NestedNonOptional(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/Target.java
new file mode 100644
index 0000000000..6f60833dc3
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nested/Target.java
@@ -0,0 +1,48 @@
+/*
+ * 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.optionalmapping.nested;
+
+import java.util.Optional;
+
+public class Target {
+
+ private String optionalToNonOptional;
+ private Optional optionalToOptional;
+ private String nonOptionalToNonOptional;
+ private Optional nonOptionalToOptional;
+
+ public String getOptionalToNonOptional() {
+ return optionalToNonOptional;
+ }
+
+ public void setOptionalToNonOptional(String optionalToNonOptional) {
+ this.optionalToNonOptional = optionalToNonOptional;
+ }
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public String getNonOptionalToNonOptional() {
+ return nonOptionalToNonOptional;
+ }
+
+ public void setNonOptionalToNonOptional(String nonOptionalToNonOptional) {
+ this.nonOptionalToNonOptional = nonOptionalToNonOptional;
+ }
+
+ public Optional getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(Optional nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/OptionalNullCheckAlwaysMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/OptionalNullCheckAlwaysMapper.java
new file mode 100644
index 0000000000..9f170da90d
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/OptionalNullCheckAlwaysMapper.java
@@ -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.optionalmapping.nullcheckalways;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.NullValueCheckStrategy;
+import org.mapstruct.factory.Mappers;
+
+@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS)
+public interface OptionalNullCheckAlwaysMapper {
+
+ OptionalNullCheckAlwaysMapper INSTANCE = Mappers.getMapper( OptionalNullCheckAlwaysMapper.class );
+
+ Target toTarget(Source source);
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/OptionalNullCheckAlwaysTest.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/OptionalNullCheckAlwaysTest.java
new file mode 100644
index 0000000000..ce404409f0
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/OptionalNullCheckAlwaysTest.java
@@ -0,0 +1,51 @@
+/*
+ * 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.optionalmapping.nullcheckalways;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WithClasses({
+ OptionalNullCheckAlwaysMapper.class, Source.class, Target.class
+})
+public class OptionalNullCheckAlwaysTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource()
+ .addComparisonToFixtureFor( OptionalNullCheckAlwaysMapper.class );
+
+ @ProcessorTest
+ public void optionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.setOptionalToOptional( null );
+
+ Target target = OptionalNullCheckAlwaysMapper.INSTANCE.toTarget( source );
+ assertThat( target.isOptionalToOptionalCalled() ).isFalse();
+ }
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenNull() {
+ Source source = new Source();
+ source.setOptionalToNonOptional( null );
+
+ Target target = OptionalNullCheckAlwaysMapper.INSTANCE.toTarget( source );
+ assertThat( target.isOptionalToNonOptionalCalled() ).isFalse();
+ }
+
+ @ProcessorTest
+ public void nonOptionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.setNonOptionalToOptional( null );
+
+ Target target = OptionalNullCheckAlwaysMapper.INSTANCE.toTarget( source );
+ assertThat( target.isNonOptionalToOptionalCalled() ).isFalse();
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/Source.java
new file mode 100644
index 0000000000..d6c06cce3a
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/Source.java
@@ -0,0 +1,40 @@
+/*
+ * 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.optionalmapping.nullcheckalways;
+
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Source {
+
+ private Optional optionalToOptional;
+ private Optional optionalToNonOptional;
+ private String nonOptionalToOptional;
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public Optional getOptionalToNonOptional() {
+ return optionalToNonOptional;
+ }
+
+ public void setOptionalToNonOptional(Optional optionalToNonOptional) {
+ this.optionalToNonOptional = optionalToNonOptional;
+ }
+
+ public String getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(String nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/Target.java
new file mode 100644
index 0000000000..451e407dde
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullcheckalways/Target.java
@@ -0,0 +1,40 @@
+/*
+ * 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.optionalmapping.nullcheckalways;
+
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Target {
+
+ private boolean optionalToOptionalCalled;
+ private boolean optionalToNonOptionalCalled;
+ private boolean nonOptionalToOptionalCalled;
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptionalCalled = true;
+ }
+
+ public void setOptionalToNonOptional(String optionalToNonOptional) {
+ this.optionalToNonOptionalCalled = true;
+ }
+
+ public void setNonOptionalToOptional(Optional nonOptionalToOptional) {
+ this.nonOptionalToOptionalCalled = true;
+ }
+
+ public boolean isOptionalToOptionalCalled() {
+ return optionalToOptionalCalled;
+ }
+
+ public boolean isOptionalToNonOptionalCalled() {
+ return optionalToNonOptionalCalled;
+ }
+
+ public boolean isNonOptionalToOptionalCalled() {
+ return nonOptionalToOptionalCalled;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/NullValuePropertyToDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/NullValuePropertyToDefaultMapper.java
new file mode 100644
index 0000000000..1784550c52
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/NullValuePropertyToDefaultMapper.java
@@ -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.optionalmapping.nullvaluepropertytodefault;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.MappingTarget;
+import org.mapstruct.NullValuePropertyMappingStrategy;
+import org.mapstruct.factory.Mappers;
+
+@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.SET_TO_DEFAULT)
+public interface NullValuePropertyToDefaultMapper {
+
+ NullValuePropertyToDefaultMapper INSTANCE = Mappers.getMapper(
+ NullValuePropertyToDefaultMapper.class );
+
+ void mapTarget(Source source, @MappingTarget Target target);
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/NullValuePropertyToDefaultTest.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/NullValuePropertyToDefaultTest.java
new file mode 100644
index 0000000000..5e2a23678b
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/NullValuePropertyToDefaultTest.java
@@ -0,0 +1,66 @@
+/*
+ * 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.optionalmapping.nullvaluepropertytodefault;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WithClasses({
+ NullValuePropertyToDefaultMapper.class,
+ Source.class,
+ Target.class
+})
+public class NullValuePropertyToDefaultTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource()
+ .addComparisonToFixtureFor( NullValuePropertyToDefaultMapper.class );
+
+ @ProcessorTest
+ public void optionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.setOptionalToOptional( null );
+
+ Target target = new Target();
+ NullValuePropertyToDefaultMapper.INSTANCE.mapTarget( source, target );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void nonOptionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.setNonOptionalToOptional( null );
+
+ Target target = new Target();
+ NullValuePropertyToDefaultMapper.INSTANCE.mapTarget( source, target );
+ assertThat( target.getNonOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.publicOptionalToOptional = null;
+
+ Target target = new Target();
+ NullValuePropertyToDefaultMapper.INSTANCE.mapTarget( source, target );
+ assertThat( target.publicOptionalToOptional ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void publicNonOptionalToOptionalWhenNull() {
+ Source source = new Source();
+ source.publicNonOptionalToOptional = null;
+
+ Target target = new Target();
+ NullValuePropertyToDefaultMapper.INSTANCE.mapTarget( source, target );
+ assertThat( target.publicNonOptionalToOptional ).isEmpty();
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/Source.java
new file mode 100644
index 0000000000..01a6efb1de
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/Source.java
@@ -0,0 +1,68 @@
+/*
+ * 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.optionalmapping.nullvaluepropertytodefault;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Source {
+
+ private Optional optionalToOptional;
+ private SubType nonOptionalToOptional;
+
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public SubType publicNonOptionalToOptional;
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public SubType getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(SubType nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+
+ public static class SubType {
+
+ private final String value;
+
+ public SubType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ SubType subType = (SubType) o;
+ return Objects.equals( value, subType.value );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( value );
+ }
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/Target.java
new file mode 100644
index 0000000000..e374e6940d
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/Target.java
@@ -0,0 +1,67 @@
+/*
+ * 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.optionalmapping.nullvaluepropertytodefault;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Target {
+
+ private Optional optionalToOptional;
+ private Optional nonOptionalToOptional;
+
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicNonOptionalToOptional;
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public Optional getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(Optional nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+
+ public static class SubType {
+
+ private final String value;
+
+ public SubType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ SubType subType = (SubType) o;
+ return Objects.equals( value, subType.value );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( value );
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/NullValueToDefaultMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/NullValueToDefaultMapper.java
new file mode 100644
index 0000000000..8cfd09dd6f
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/NullValueToDefaultMapper.java
@@ -0,0 +1,20 @@
+/*
+ * 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.optionalmapping.nullvaluetodefault;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.NullValueMappingStrategy;
+import org.mapstruct.factory.Mappers;
+
+@Mapper(nullValueMappingStrategy = NullValueMappingStrategy.RETURN_DEFAULT)
+public interface NullValueToDefaultMapper {
+
+ NullValueToDefaultMapper INSTANCE = Mappers.getMapper(
+ NullValueToDefaultMapper.class );
+
+ Target toTarget(Source source);
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/NullValueToDefaultTest.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/NullValueToDefaultTest.java
new file mode 100644
index 0000000000..03e4b11de9
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/NullValueToDefaultTest.java
@@ -0,0 +1,104 @@
+/*
+ * 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.optionalmapping.nullvaluetodefault;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WithClasses({
+ NullValueToDefaultMapper.class,
+ Source.class,
+ Target.class
+})
+public class NullValueToDefaultTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource()
+ .addComparisonToFixtureFor( NullValueToDefaultMapper.class );
+
+ @ProcessorTest
+ public void constructorOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+
+ Target target = NullValueToDefaultMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToNonOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+
+ Target target = NullValueToDefaultMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void constructorNonOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+
+ Target target = NullValueToDefaultMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorNonOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void optionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToOptional( null );
+
+ Target target = NullValueToDefaultMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToNonOptional( null );
+
+ Target target = NullValueToDefaultMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nonOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.setNonOptionalToOptional( null );
+
+ Target target = NullValueToDefaultMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToOptional = null;
+
+ Target target = NullValueToDefaultMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToOptional ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToNonOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToNonOptional = null;
+
+ Target target = NullValueToDefaultMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToNonOptional ).isNull();
+ }
+
+ @ProcessorTest
+ public void publicNonOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.publicNonOptionalToOptional = null;
+
+ Target target = NullValueToDefaultMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicNonOptionalToOptional ).isEmpty();
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/Source.java
new file mode 100644
index 0000000000..48881cbdf2
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/Source.java
@@ -0,0 +1,102 @@
+/*
+ * 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.optionalmapping.nullvaluetodefault;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Source {
+
+ private final Optional constructorOptionalToOptional;
+ private final Optional constructorOptionalToNonOptional;
+ private final SubType constructorNonOptionalToOptional;
+
+ private Optional optionalToOptional;
+ private Optional optionalToNonOptional;
+ private SubType nonOptionalToOptional;
+
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToNonOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public SubType publicNonOptionalToOptional;
+
+ public Source(Optional constructorOptionalToOptional, Optional constructorOptionalToNonOptional,
+ SubType constructorNonOptionalToOptional) {
+ this.constructorOptionalToOptional = constructorOptionalToOptional;
+ this.constructorOptionalToNonOptional = constructorOptionalToNonOptional;
+ this.constructorNonOptionalToOptional = constructorNonOptionalToOptional;
+ }
+
+ public Optional getConstructorOptionalToOptional() {
+ return constructorOptionalToOptional;
+ }
+
+ public Optional getConstructorOptionalToNonOptional() {
+ return constructorOptionalToNonOptional;
+ }
+
+ public SubType getConstructorNonOptionalToOptional() {
+ return constructorNonOptionalToOptional;
+ }
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public Optional getOptionalToNonOptional() {
+ return optionalToNonOptional;
+ }
+
+ public void setOptionalToNonOptional(Optional optionalToNonOptional) {
+ this.optionalToNonOptional = optionalToNonOptional;
+ }
+
+ public SubType getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(SubType nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+
+ public static class SubType {
+
+ private final String value;
+
+ public SubType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ SubType subType = (SubType) o;
+ return Objects.equals( value, subType.value );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( value );
+ }
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/Target.java
new file mode 100644
index 0000000000..bc7fd34540
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/Target.java
@@ -0,0 +1,101 @@
+/*
+ * 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.optionalmapping.nullvaluetodefault;
+
+import java.util.Objects;
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Target {
+
+ private final Optional constructorOptionalToOptional;
+ private final SubType constructorOptionalToNonOptional;
+ private final Optional constructorNonOptionalToOptional;
+
+ private Optional optionalToOptional;
+ private SubType optionalToNonOptional;
+ private Optional nonOptionalToOptional;
+
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public SubType publicOptionalToNonOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicNonOptionalToOptional;
+
+ public Target(Optional constructorOptionalToOptional, SubType constructorOptionalToNonOptional,
+ Optional constructorNonOptionalToOptional) {
+ this.constructorOptionalToOptional = constructorOptionalToOptional;
+ this.constructorOptionalToNonOptional = constructorOptionalToNonOptional;
+ this.constructorNonOptionalToOptional = constructorNonOptionalToOptional;
+ }
+
+ public Optional getConstructorOptionalToOptional() {
+ return constructorOptionalToOptional;
+ }
+
+ public SubType getConstructorOptionalToNonOptional() {
+ return constructorOptionalToNonOptional;
+ }
+
+ public Optional getConstructorNonOptionalToOptional() {
+ return constructorNonOptionalToOptional;
+ }
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public SubType getOptionalToNonOptional() {
+ return optionalToNonOptional;
+ }
+
+ public void setOptionalToNonOptional(SubType optionalToNonOptional) {
+ this.optionalToNonOptional = optionalToNonOptional;
+ }
+
+ public Optional getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(Optional nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+
+ public static class SubType {
+
+ private final String value;
+
+ public SubType(String value) {
+ this.value = value;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if ( this == o ) {
+ return true;
+ }
+ if ( o == null || getClass() != o.getClass() ) {
+ return false;
+ }
+ SubType subType = (SubType) o;
+ return Objects.equals( value, subType.value );
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash( value );
+ }
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/OptionalSameTypeMapper.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/OptionalSameTypeMapper.java
new file mode 100644
index 0000000000..6a34d231bd
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/OptionalSameTypeMapper.java
@@ -0,0 +1,20 @@
+/*
+ * 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.optionalmapping.sametype;
+
+import org.mapstruct.Mapper;
+import org.mapstruct.NullValueCheckStrategy;
+import org.mapstruct.factory.Mappers;
+
+@Mapper(nullValueCheckStrategy = NullValueCheckStrategy.ON_IMPLICIT_CONVERSION)
+public interface OptionalSameTypeMapper {
+
+ OptionalSameTypeMapper INSTANCE = Mappers.getMapper(
+ OptionalSameTypeMapper.class );
+
+ Target toTarget(Source source);
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/OptionalSameTypeTest.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/OptionalSameTypeTest.java
new file mode 100644
index 0000000000..686e23bf68
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/OptionalSameTypeTest.java
@@ -0,0 +1,252 @@
+/*
+ * 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.optionalmapping.sametype;
+
+import java.util.Optional;
+
+import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mapstruct.ap.testutil.ProcessorTest;
+import org.mapstruct.ap.testutil.WithClasses;
+import org.mapstruct.ap.testutil.runner.GeneratedSource;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WithClasses({
+ OptionalSameTypeMapper.class, Source.class, Target.class
+})
+public class OptionalSameTypeTest {
+
+ @RegisterExtension
+ final GeneratedSource generatedSource = new GeneratedSource()
+ .addComparisonToFixtureFor( OptionalSameTypeMapper.class );
+
+ @ProcessorTest
+ public void constructorOptionalToOptionalWhenPresent() {
+ Source source = new Source( Optional.of( "some value" ), null, null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToOptional() ).contains( "some value" );
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToOptionalWhenEmpty() {
+ Source source = new Source( Optional.empty(), null, null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToOptional() ).isEmpty();
+ }
+
+ // TODO Contentious
+ // Should null Optional map to null Optional, or explicitly to empty?
+ // This one is a bit stranger than the "different types" case.
+ // Because here, both the source and target are the same type:
+ // Optional -> Optional, therefore no nested mapping is required.
+ // By default, the mapper just generates `target.prop = source.prop`.
+ @ProcessorTest
+ public void constructorOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToNonOptionalWhenPresent() {
+ Source source = new Source( null, Optional.of( "some value" ), null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToNonOptional() ).isEqualTo( "some value" );
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToNonOptionalWhenEmpty() {
+ Source source = new Source( null, Optional.empty(), null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void constructorOptionalToNonOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void constructorNonOptionalToOptionalWhenNotNull() {
+ Source source = new Source( null, null, "some value" );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorNonOptionalToOptional() ).contains( "some value" );
+ }
+
+ @ProcessorTest
+ public void constructorNonOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getConstructorNonOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void optionalToOptionalWhenPresent() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToOptional( Optional.of( "some value" ) );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).contains( "some value" );
+ }
+
+ @ProcessorTest
+ public void optionalToOptionalWhenEmpty() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToOptional( Optional.empty() );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isEmpty();
+ }
+
+ // TODO Contentious
+ // Should null Optional map to null Optional, or explicitly to empty?
+ // This one is a bit stranger than the "different types" case.
+ // Because here, both the source and target are the same type:
+ // Optional -> Optional, therefore no nested mapping is required.
+ // By default, the mapper just generates `target.prop = source.prop`.
+ @ProcessorTest
+ public void optionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToOptional( null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenPresent() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToNonOptional( Optional.of( "some value" ) );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isEqualTo( "some value" );
+ }
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenEmpty() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToNonOptional( Optional.empty() );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void optionalToNonOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.setOptionalToNonOptional( null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getOptionalToNonOptional() ).isNull();
+ }
+
+ @ProcessorTest
+ public void nonOptionalToOptionalWhenNotNull() {
+ Source source = new Source( null, null, null );
+ source.setNonOptionalToOptional( "some value" );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToOptional() ).contains( "some value" );
+ }
+
+ @ProcessorTest
+ public void nonOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.setNonOptionalToOptional( null );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.getNonOptionalToOptional() ).isEmpty();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToOptionalWhenPresent() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToOptional = Optional.of( "some value" );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToOptional ).contains( "some value" );
+ }
+
+ @ProcessorTest
+ public void publicOptionalToOptionalWhenEmpty() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToOptional = Optional.empty();
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToOptional ).isEmpty();
+ }
+
+ // TODO Contentious
+ // Should null Optional map to null Optional, or explicitly to empty?
+ // This one is a bit stranger than the "different types" case.
+ // Because here, both the source and target are the same type:
+ // Optional -> Optional, therefore no nested mapping is required.
+ // By default, the mapper just generates `target.prop = source.prop`.
+ @ProcessorTest
+ public void publicOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToOptional = null;
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToOptional ).isNull();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToNonOptionalWhenPresent() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToNonOptional = Optional.of( "some value" );
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToNonOptional ).isEqualTo( "some value" );
+ }
+
+ @ProcessorTest
+ public void publicOptionalToNonOptionalWhenEmpty() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToNonOptional = Optional.empty();
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToNonOptional ).isNull();
+ }
+
+ @ProcessorTest
+ public void publicOptionalToNonOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.publicOptionalToNonOptional = null;
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicOptionalToNonOptional ).isNull();
+ }
+
+ @ProcessorTest
+ public void publicNonOptionalToOptionalWhenNotNull() {
+ Source source = new Source( null, null, null );
+ source.publicNonOptionalToOptional = "some value";
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicNonOptionalToOptional ).contains( "some value" );
+ }
+
+ @ProcessorTest
+ public void publicNonOptionalToOptionalWhenNull() {
+ Source source = new Source( null, null, null );
+ source.publicNonOptionalToOptional = null;
+
+ Target target = OptionalSameTypeMapper.INSTANCE.toTarget( source );
+ assertThat( target.publicNonOptionalToOptional ).isEmpty();
+ }
+
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/Source.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/Source.java
new file mode 100644
index 0000000000..6d136056d3
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/Source.java
@@ -0,0 +1,70 @@
+/*
+ * 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.optionalmapping.sametype;
+
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Source {
+
+ private final Optional constructorOptionalToOptional;
+ private final Optional constructorOptionalToNonOptional;
+ private final String constructorNonOptionalToOptional;
+
+ private Optional optionalToOptional;
+ private Optional optionalToNonOptional;
+ private String nonOptionalToOptional;
+
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToNonOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public String publicNonOptionalToOptional;
+
+ public Source(Optional constructorOptionalToOptional, Optional constructorOptionalToNonOptional,
+ String constructorNonOptionalToOptional) {
+ this.constructorOptionalToOptional = constructorOptionalToOptional;
+ this.constructorOptionalToNonOptional = constructorOptionalToNonOptional;
+ this.constructorNonOptionalToOptional = constructorNonOptionalToOptional;
+ }
+
+ public Optional getConstructorOptionalToOptional() {
+ return constructorOptionalToOptional;
+ }
+
+ public Optional getConstructorOptionalToNonOptional() {
+ return constructorOptionalToNonOptional;
+ }
+
+ public String getConstructorNonOptionalToOptional() {
+ return constructorNonOptionalToOptional;
+ }
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public Optional getOptionalToNonOptional() {
+ return optionalToNonOptional;
+ }
+
+ public void setOptionalToNonOptional(Optional optionalToNonOptional) {
+ this.optionalToNonOptional = optionalToNonOptional;
+ }
+
+ public String getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(String nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/Target.java b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/Target.java
new file mode 100644
index 0000000000..d1210aff05
--- /dev/null
+++ b/processor/src/test/java/org/mapstruct/ap/test/optionalmapping/sametype/Target.java
@@ -0,0 +1,70 @@
+/*
+ * 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.optionalmapping.sametype;
+
+import java.util.Optional;
+
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public class Target {
+
+ private final Optional constructorOptionalToOptional;
+ private final String constructorOptionalToNonOptional;
+ private final Optional constructorNonOptionalToOptional;
+
+ private Optional optionalToOptional;
+ private String optionalToNonOptional;
+ private Optional nonOptionalToOptional;
+
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicOptionalToOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public String publicOptionalToNonOptional;
+ @SuppressWarnings( "VisibilityModifier" )
+ public Optional publicNonOptionalToOptional;
+
+ public Target(Optional constructorOptionalToOptional, String constructorOptionalToNonOptional,
+ Optional constructorNonOptionalToOptional) {
+ this.constructorOptionalToOptional = constructorOptionalToOptional;
+ this.constructorOptionalToNonOptional = constructorOptionalToNonOptional;
+ this.constructorNonOptionalToOptional = constructorNonOptionalToOptional;
+ }
+
+ public Optional getConstructorOptionalToOptional() {
+ return constructorOptionalToOptional;
+ }
+
+ public String getConstructorOptionalToNonOptional() {
+ return constructorOptionalToNonOptional;
+ }
+
+ public Optional getConstructorNonOptionalToOptional() {
+ return constructorNonOptionalToOptional;
+ }
+
+ public Optional getOptionalToOptional() {
+ return optionalToOptional;
+ }
+
+ public void setOptionalToOptional(Optional optionalToOptional) {
+ this.optionalToOptional = optionalToOptional;
+ }
+
+ public String getOptionalToNonOptional() {
+ return optionalToNonOptional;
+ }
+
+ public void setOptionalToNonOptional(String optionalToNonOptional) {
+ this.optionalToNonOptional = optionalToNonOptional;
+ }
+
+ public Optional getNonOptionalToOptional() {
+ return nonOptionalToOptional;
+ }
+
+ public void setNonOptionalToOptional(Optional nonOptionalToOptional) {
+ this.nonOptionalToOptional = nonOptionalToOptional;
+ }
+}
diff --git a/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java b/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java
index e6eabd9f84..3ceb58fbc9 100644
--- a/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java
+++ b/processor/src/test/java/org/mapstruct/ap/testutil/assertions/JavaFileAssert.java
@@ -33,7 +33,7 @@ public class JavaFileAssert extends FileAssert {
private static final String FIRST_LINE_LICENSE_REGEX = ".*Copyright MapStruct Authors.*";
private static final String GENERATED_DATE_REGEX = "\\s+date = " +
- "\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\+\\d{4}\",";
+ "\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}[+-]\\d{4}\",";
private static final String GENERATED_COMMENTS_REGEX = "\\s+comments = \"version: , compiler: .*, environment: " +
".*\"";
private static final String IMPORT_GENERATED_ANNOTATION_REGEX = "import javax\\.annotation\\.(processing\\.)?" +
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/beforeafter/OptionalBeforeAfterMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/beforeafter/OptionalBeforeAfterMapperImpl.java
new file mode 100644
index 0000000000..8a3b57b4a2
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/beforeafter/OptionalBeforeAfterMapperImpl.java
@@ -0,0 +1,129 @@
+/*
+ * 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.optionalmapping.beforeafter;
+
+import java.util.Optional;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2023-11-20T22:01:31-0500",
+ comments = "version: , compiler: javac, environment: Java 11.0.19 (Homebrew)"
+)
+public class OptionalBeforeAfterMapperImpl implements OptionalBeforeAfterMapper {
+
+ @Override
+ public Target toTarget(Source source) {
+ if ( source == null ) {
+ return null;
+ }
+
+ Optional deepOptionalToOptional = null;
+ Target.SubType deepOptionalToNonOptional = null;
+ Optional deepNonOptionalToOptional = null;
+ Optional shallowOptionalToOptional = null;
+ String shallowOptionalToNonOptional = null;
+ Optional shallowNonOptionalToOptional = null;
+
+ deepOptionalToOptional = subTypeOptionalToSubTypeOptional( source.getDeepOptionalToOptional() );
+ deepOptionalToNonOptional = subTypeOptionalToSubType( source.getDeepOptionalToNonOptional() );
+ deepNonOptionalToOptional = subTypeToSubTypeOptional( source.getDeepNonOptionalToOptional() );
+ shallowOptionalToOptional = source.getShallowOptionalToOptional();
+ shallowOptionalToNonOptional = stringOptionalToString( source.getShallowOptionalToNonOptional() );
+ shallowNonOptionalToOptional = stringToStringOptional( source.getShallowNonOptionalToOptional() );
+
+ Target target = new Target( deepOptionalToOptional, deepOptionalToNonOptional, deepNonOptionalToOptional, shallowOptionalToOptional, shallowOptionalToNonOptional, shallowNonOptionalToOptional );
+
+ return target;
+ }
+
+ protected Target.SubType subTypeToSubType(Source.SubType subType) {
+ if ( subType == null ) {
+ return null;
+ }
+
+ String value = null;
+
+ value = subType.getValue();
+
+ Target.SubType subType1 = new Target.SubType( value );
+
+ return subType1;
+ }
+
+ protected Optional subTypeOptionalToSubTypeOptional(Optional optional) {
+ beforeDeepOptionalSourceWithNoTargetType( optional );
+
+ if ( optional == null ) {
+ return Optional.empty();
+ }
+
+ Optional optional1 = optional.map( subType -> subTypeToSubType( subType ) );
+
+ afterDeepOptionalSourceWithNoTarget( optional );
+ afterDeepOptionalSourceWithOptionalTarget( optional1, optional );
+
+ return optional1;
+ }
+
+ protected Target.SubType subTypeOptionalToSubType(Optional optional) {
+ beforeDeepOptionalSourceWithNoTargetType( optional );
+ beforeDeepOptionalSourceWithNonOptionalTargetType( Target.SubType.class, optional );
+
+ if ( optional == null ) {
+ return null;
+ }
+
+ Target.SubType subType1 = optional.map( subType -> subTypeToSubType( subType ) ).orElse( null );
+
+ afterDeepOptionalSourceWithNoTarget( optional );
+ afterDeepOptionalSourceWithNonOptionalTarget( subType1, optional );
+
+ return subType1;
+ }
+
+ protected Optional subTypeToSubTypeOptional(Source.SubType subType) {
+ if ( subType == null ) {
+ return Optional.empty();
+ }
+
+ Source.SubType subType1 = subType;
+ Optional optional = Optional.ofNullable( subTypeToSubType( subType1 ) );
+
+ afterDeepNonOptionalSourceOptionalTarget( optional, subType );
+
+ return optional;
+ }
+
+ protected String stringOptionalToString(Optional optional) {
+ beforeShallowOptionalSourceWithNoTargetType( optional );
+ beforeShallowOptionalSourceWithNonOptionalTargetType( String.class, optional );
+
+ if ( optional == null ) {
+ return null;
+ }
+
+ String string1 = optional.map( string -> string ).orElse( null );
+
+ afterShallowOptionalSourceWithNoTarget( optional );
+ afterShallowOptionalSourceWithNonOptionalTarget( string1, optional );
+
+ return string1;
+ }
+
+ protected Optional stringToStringOptional(String string) {
+ if ( string == null ) {
+ return Optional.empty();
+ }
+
+ String string1 = string;
+ Optional optional = Optional.ofNullable( string1 );
+
+ afterShallowNonOptionalSourceOptionalTarget( optional, string );
+
+ return optional;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/differenttypes/OptionalDifferentTypesMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/differenttypes/OptionalDifferentTypesMapperImpl.java
new file mode 100644
index 0000000000..a5f6a4b0f1
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/differenttypes/OptionalDifferentTypesMapperImpl.java
@@ -0,0 +1,88 @@
+/*
+ * 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.optionalmapping.differenttypes;
+
+import java.util.Optional;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2023-11-20T21:40:58-0500",
+ comments = "version: , compiler: javac, environment: Java 11.0.19 (Homebrew)"
+)
+public class OptionalDifferentTypesMapperImpl implements OptionalDifferentTypesMapper {
+
+ @Override
+ public Target toTarget(Source source) {
+ if ( source == null ) {
+ return null;
+ }
+
+ Optional constructorOptionalToOptional = null;
+ Target.SubType constructorOptionalToNonOptional = null;
+ Optional constructorNonOptionalToOptional = null;
+
+ constructorOptionalToOptional = subTypeOptionalToSubTypeOptional( source.getConstructorOptionalToOptional() );
+ constructorOptionalToNonOptional = subTypeOptionalToSubType( source.getConstructorOptionalToNonOptional() );
+ constructorNonOptionalToOptional = subTypeToSubTypeOptional( source.getConstructorNonOptionalToOptional() );
+
+ Target target = new Target( constructorOptionalToOptional, constructorOptionalToNonOptional, constructorNonOptionalToOptional );
+
+ target.setOptionalToOptional( subTypeOptionalToSubTypeOptional( source.getOptionalToOptional() ) );
+ target.setOptionalToNonOptional( subTypeOptionalToSubType( source.getOptionalToNonOptional() ) );
+ target.setNonOptionalToOptional( subTypeToSubTypeOptional( source.getNonOptionalToOptional() ) );
+ target.publicOptionalToOptional = subTypeOptionalToSubTypeOptional( source.publicOptionalToOptional );
+ target.publicOptionalToNonOptional = subTypeOptionalToSubType( source.publicOptionalToNonOptional );
+ target.publicNonOptionalToOptional = subTypeToSubTypeOptional( source.publicNonOptionalToOptional );
+
+ return target;
+ }
+
+ protected Target.SubType subTypeToSubType(Source.SubType subType) {
+ if ( subType == null ) {
+ return null;
+ }
+
+ String value = null;
+
+ value = subType.getValue();
+
+ Target.SubType subType1 = new Target.SubType( value );
+
+ return subType1;
+ }
+
+ protected Optional subTypeOptionalToSubTypeOptional(Optional optional) {
+ if ( optional == null ) {
+ return Optional.empty();
+ }
+
+ Optional optional1 = optional.map( subType -> subTypeToSubType( subType ) );
+
+ return optional1;
+ }
+
+ protected Target.SubType subTypeOptionalToSubType(Optional optional) {
+ if ( optional == null ) {
+ return null;
+ }
+
+ Target.SubType subType1 = optional.map( subType -> subTypeToSubType( subType ) ).orElse( null );
+
+ return subType1;
+ }
+
+ protected Optional subTypeToSubTypeOptional(Source.SubType subType) {
+ if ( subType == null ) {
+ return Optional.empty();
+ }
+
+ Source.SubType subType1 = subType;
+ Optional optional = Optional.ofNullable( subTypeToSubType( subType1 ) );
+
+ return optional;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nested/OptionalNestedMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nested/OptionalNestedMapperImpl.java
new file mode 100644
index 0000000000..1bb3a85dbc
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nested/OptionalNestedMapperImpl.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.optionalmapping.nested;
+
+import java.util.Optional;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2023-11-20T21:40:54-0500",
+ comments = "version: , compiler: javac, environment: Java 11.0.19 (Homebrew)"
+)
+public class OptionalNestedMapperImpl implements OptionalNestedMapper {
+
+ @Override
+ public Target toTarget(Source source) {
+ if ( source == null ) {
+ return null;
+ }
+
+ Target target = new Target();
+
+ target.setOptionalToNonOptional( sourceOptionalToNonOptional_Value_( source ) );
+ target.setOptionalToOptional( sourceOptionalToOptional_Value( source ) );
+ target.setNonOptionalToNonOptional( sourceNonOptionalToNonOptional_Value( source ) );
+ target.setNonOptionalToOptional( stringToStringOptional( sourceNonOptionalToOptional_Value( source ) ) );
+
+ return target;
+ }
+
+ private String sourceOptionalToNonOptional_Value_(Source source) {
+ Optional optionalToNonOptional = source.getOptionalToNonOptional();
+ if ( optionalToNonOptional == null ) {
+ return null;
+ }
+ Source.NestedOptional optionalValue = optionalToNonOptional.orElse( null );
+ if ( optionalValue == null ) {
+ return null;
+ }
+ Optional value = optionalValue.getValue();
+ if ( value == null ) {
+ return null;
+ }
+ return value.orElse( null );
+ }
+
+ private Optional sourceOptionalToOptional_Value(Source source) {
+ Optional optionalToOptional = source.getOptionalToOptional();
+ if ( optionalToOptional == null ) {
+ return Optional.empty();
+ }
+ Source.NestedOptional optionalValue = optionalToOptional.orElse( null );
+ if ( optionalValue == null ) {
+ return Optional.empty();
+ }
+ return optionalValue.getValue();
+ }
+
+ private String sourceNonOptionalToNonOptional_Value(Source source) {
+ Optional nonOptionalToNonOptional = source.getNonOptionalToNonOptional();
+ if ( nonOptionalToNonOptional == null ) {
+ return null;
+ }
+ Source.NestedNonOptional optionalValue = nonOptionalToNonOptional.orElse( null );
+ if ( optionalValue == null ) {
+ return null;
+ }
+ return optionalValue.getValue();
+ }
+
+ private String sourceNonOptionalToOptional_Value(Source source) {
+ Optional nonOptionalToOptional = source.getNonOptionalToOptional();
+ if ( nonOptionalToOptional == null ) {
+ return null;
+ }
+ Source.NestedNonOptional optionalValue = nonOptionalToOptional.orElse( null );
+ if ( optionalValue == null ) {
+ return null;
+ }
+ return optionalValue.getValue();
+ }
+
+ protected Optional stringToStringOptional(String string) {
+ if ( string == null ) {
+ return Optional.empty();
+ }
+
+ String string1 = string;
+ Optional optional = Optional.ofNullable( string1 );
+
+ return optional;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nullcheckalways/OptionalNullCheckAlwaysMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nullcheckalways/OptionalNullCheckAlwaysMapperImpl.java
new file mode 100644
index 0000000000..8d923e47e1
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nullcheckalways/OptionalNullCheckAlwaysMapperImpl.java
@@ -0,0 +1,59 @@
+/*
+ * 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.optionalmapping.nullcheckalways;
+
+import java.util.Optional;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2023-11-20T21:40:55-0500",
+ comments = "version: , compiler: javac, environment: Java 11.0.19 (Homebrew)"
+)
+public class OptionalNullCheckAlwaysMapperImpl implements OptionalNullCheckAlwaysMapper {
+
+ @Override
+ public Target toTarget(Source source) {
+ if ( source == null ) {
+ return null;
+ }
+
+ Target target = new Target();
+
+ if ( source.getOptionalToOptional() != null ) {
+ target.setOptionalToOptional( source.getOptionalToOptional() );
+ }
+ if ( source.getOptionalToNonOptional() != null ) {
+ target.setOptionalToNonOptional( stringOptionalToString( source.getOptionalToNonOptional() ) );
+ }
+ if ( source.getNonOptionalToOptional() != null ) {
+ target.setNonOptionalToOptional( stringToStringOptional( source.getNonOptionalToOptional() ) );
+ }
+
+ return target;
+ }
+
+ protected String stringOptionalToString(Optional optional) {
+ if ( optional == null ) {
+ return null;
+ }
+
+ String string1 = optional.map( string -> string ).orElse( null );
+
+ return string1;
+ }
+
+ protected Optional stringToStringOptional(String string) {
+ if ( string == null ) {
+ return Optional.empty();
+ }
+
+ String string1 = string;
+ Optional optional = Optional.ofNullable( string1 );
+
+ return optional;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/NullValuePropertyToDefaultMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/NullValuePropertyToDefaultMapperImpl.java
new file mode 100644
index 0000000000..8bb4bb5c51
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nullvaluepropertytodefault/NullValuePropertyToDefaultMapperImpl.java
@@ -0,0 +1,84 @@
+/*
+ * 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.optionalmapping.nullvaluepropertytodefault;
+
+import java.util.Optional;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2023-11-20T21:40:59-0500",
+ comments = "version: , compiler: javac, environment: Java 11.0.19 (Homebrew)"
+)
+public class NullValuePropertyToDefaultMapperImpl implements NullValuePropertyToDefaultMapper {
+
+ @Override
+ public void mapTarget(Source source, Target target) {
+ if ( source == null ) {
+ return;
+ }
+
+ if ( source.getOptionalToOptional() != null ) {
+ target.setOptionalToOptional( subTypeOptionalToSubTypeOptional( source.getOptionalToOptional() ) );
+ }
+ else {
+ target.setOptionalToOptional( Optional.empty() );
+ }
+ if ( source.getNonOptionalToOptional() != null ) {
+ target.setNonOptionalToOptional( subTypeToSubTypeOptional( source.getNonOptionalToOptional() ) );
+ }
+ else {
+ target.setNonOptionalToOptional( Optional.empty() );
+ }
+ if ( source.publicOptionalToOptional != null ) {
+ target.publicOptionalToOptional = subTypeOptionalToSubTypeOptional( source.publicOptionalToOptional );
+ }
+ else {
+ target.publicOptionalToOptional = Optional.empty();
+ }
+ if ( source.publicNonOptionalToOptional != null ) {
+ target.publicNonOptionalToOptional = subTypeToSubTypeOptional( source.publicNonOptionalToOptional );
+ }
+ else {
+ target.publicNonOptionalToOptional = Optional.empty();
+ }
+ }
+
+ protected Target.SubType subTypeToSubType(Source.SubType subType) {
+ if ( subType == null ) {
+ return null;
+ }
+
+ String value = null;
+
+ value = subType.getValue();
+
+ Target.SubType subType1 = new Target.SubType( value );
+
+ return subType1;
+ }
+
+ protected Optional subTypeOptionalToSubTypeOptional(Optional optional) {
+ if ( optional == null ) {
+ return Optional.empty();
+ }
+
+ Optional optional1 = optional.map( subType -> subTypeToSubType( subType ) );
+
+ return optional1;
+ }
+
+ protected Optional subTypeToSubTypeOptional(Source.SubType subType) {
+ if ( subType == null ) {
+ return Optional.empty();
+ }
+
+ Source.SubType subType1 = subType;
+ Optional optional = Optional.ofNullable( subTypeToSubType( subType1 ) );
+
+ return optional;
+ }
+}
diff --git a/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/NullValueToDefaultMapperImpl.java b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/NullValueToDefaultMapperImpl.java
new file mode 100644
index 0000000000..3097b3d21d
--- /dev/null
+++ b/processor/src/test/resources/fixtures/org/mapstruct/ap/test/optionalmapping/nullvaluetodefault/NullValueToDefaultMapperImpl.java
@@ -0,0 +1,86 @@
+/*
+ * 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.optionalmapping.nullvaluetodefault;
+
+import java.util.Optional;
+import javax.annotation.processing.Generated;
+
+@Generated(
+ value = "org.mapstruct.ap.MappingProcessor",
+ date = "2023-11-20T21:40:57-0500",
+ comments = "version: , compiler: javac, environment: Java 11.0.19 (Homebrew)"
+)
+public class NullValueToDefaultMapperImpl implements NullValueToDefaultMapper {
+
+ @Override
+ public Target toTarget(Source source) {
+
+ Optional