Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#674 Support for native Java 8 Optional mapping #3183

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
3 changes: 2 additions & 1 deletion copyright.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public enum NullValueMappingStrategy {
* case.</li>
* <li>For iterable mapping methods an empty collection will be returned.</li>
* <li>For map mapping methods an empty map will be returned.</li>
* <li>For optional mapping methods an empty optional will be returned.</li>
* </ul>
*/
RETURN_DEFAULT;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public enum NullValuePropertyMappingStrategy {
* <p>
* This means:
* <ol>
* <li>For {@code Optional} MapStruct generates an {@code Optional.empty()}</li>
* <li>For {@code List} MapStruct generates an {@code ArrayList}</li>
* <li>For {@code Map} a {@code HashMap}</li>
* <li>For arrays an empty array</li>
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>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
*
* <p>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<Type> getImportTypes() {
Set<Type> types = new HashSet<>();
types.add( targetType );
return types;
}

public Type getTargetType() {
return targetType;
}

public MethodReference getFactoryMethod() {
return factoryMethod;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ public MethodReference getFactoryMethod() {
return this.factoryMethod;
}

public InitDefaultValue getInitDefaultValueForResultType() {
return new InitDefaultValue( this.getResultType(), this.getFactoryMethod() );
}

public List<Annotation> getAnnotations() {
return annotations;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Builder, OptionalMappingMethod> {

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<String> existingVariables,
Assignment assignment, MethodReference factoryMethod, boolean mapNullToDefault, String loopVariableName,
List<LifecycleCallbackMethodReference> beforeMappingMethods,
List<LifecycleCallbackMethodReference> afterMappingMethods, SelectionParameters selectionParameters) {
return new OptionalMappingMethod(
method,
getMethodAnnotations(),
existingVariables,
assignment,
factoryMethod,
mapNullToDefault,
loopVariableName,
beforeMappingMethods,
afterMappingMethods,
selectionParameters
);
}
}

private OptionalMappingMethod(Method method,
List<Annotation> annotations,
Collection<String> existingVariables,
Assignment parameterAssignment,
MethodReference factoryMethod,
boolean mapNullToDefault,
String loopVariableName,
List<LifecycleCallbackMethodReference> beforeMappingReferences,
List<LifecycleCallbackMethodReference> afterMappingReferences,
SelectionParameters selectionParameters) {
super(
method,
annotations,
existingVariables,
parameterAssignment,
factoryMethod,
mapNullToDefault,
loopVariableName,
beforeMappingReferences,
afterMappingReferences,
selectionParameters
);
}

@Override
public Set<Type> getImportTypes() {
Set<Type> 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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
Expand Down Expand Up @@ -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, "?" );
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using ? here to represent an Optional type

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to see how this will look like in the error. I think that it would be something like customer?.address?.value if customer and address are optional. I think that looks nice. Perhaps an example as a comment here to make it easy to understand 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have to admit, I'm not certain how to trigger an error that will print out this forge history. Would you have any guidance on the simplest way to do so? Once I can see what it looks like, I can add the comment.


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() );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
Loading
Loading