Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import java.util.Set;
import javax.lang.model.element.AnnotationMirror;

import org.mapstruct.ap.internal.gem.BuilderGem;
import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem;
import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem;
import org.mapstruct.ap.internal.model.assignment.AdderWrapper;
import org.mapstruct.ap.internal.model.assignment.ArrayCopyWrapper;
import org.mapstruct.ap.internal.model.assignment.EnumConstantWrapper;
Expand All @@ -36,22 +39,20 @@
import org.mapstruct.ap.internal.model.source.Method;
import org.mapstruct.ap.internal.model.source.SelectionParameters;
import org.mapstruct.ap.internal.model.source.selector.SelectionCriteria;
import org.mapstruct.ap.internal.gem.BuilderGem;
import org.mapstruct.ap.internal.gem.NullValueCheckStrategyGem;
import org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem;
import org.mapstruct.ap.internal.util.Message;
import org.mapstruct.ap.internal.util.NativeTypes;
import org.mapstruct.ap.internal.util.Strings;
import org.mapstruct.ap.internal.util.ValueProvider;
import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.AccessorType;
import org.mapstruct.ap.internal.util.accessor.NestedExecutableElementAccessor;

import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_DEFAULT;
import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_NULL;
import static org.mapstruct.ap.internal.model.ForgedMethod.forElementMapping;
import static org.mapstruct.ap.internal.model.ForgedMethod.forParameterMapping;
import static org.mapstruct.ap.internal.model.ForgedMethod.forPropertyMapping;
import static org.mapstruct.ap.internal.model.common.Assignment.AssignmentType.DIRECT;
import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_DEFAULT;
import static org.mapstruct.ap.internal.gem.NullValuePropertyMappingStrategyGem.SET_TO_NULL;

/**
* Represents the mapping between a source and target property, e.g. from {@code String Source#foo} to
Expand Down Expand Up @@ -565,17 +566,36 @@ private String getSourcePresenceCheckerRef( SourceReference sourceReference ) {
// in the forged method?
PropertyEntry propertyEntry = sourceReference.getShallowestProperty();
if ( propertyEntry.getPresenceChecker() != null ) {
sourcePresenceChecker = sourceParam.getName()
+ "." + propertyEntry.getPresenceChecker().getSimpleName() + "()";
if ( propertyEntry.getPresenceChecker() instanceof NestedExecutableElementAccessor ) {
// nested presence check
sourcePresenceChecker = sourceParam.getName() + " != null && "
+ sourceParam.getName() + "." + propertyEntry.getReadAccessor().getSimpleName() + "()."
+ propertyEntry.getPresenceChecker().getSimpleName() + "()";
}
else {
sourcePresenceChecker = sourceParam.getName()
+ "." + propertyEntry.getPresenceChecker().getSimpleName() + "()";
}

String variableName = sourceParam.getName() + "."
+ propertyEntry.getReadAccessor().getSimpleName() + "()";
for (int i = 1; i < sourceReference.getPropertyEntries().size(); i++) {
for ( int i = 1; i < sourceReference.getPropertyEntries().size(); i++ ) {
PropertyEntry entry = sourceReference.getPropertyEntries().get( i );
if (entry.getPresenceChecker() != null && entry.getReadAccessor() != null) {
sourcePresenceChecker += " && " + variableName + " != null && "
+ variableName + "." + entry.getPresenceChecker().getSimpleName() + "()";
variableName = variableName + "." + entry.getReadAccessor().getSimpleName() + "()";
if ( entry.getPresenceChecker() != null && entry.getReadAccessor() != null ) {
// nested presence check
if ( entry.getPresenceChecker() instanceof NestedExecutableElementAccessor ) {
// XXX not tested
System.out.println( "nested presence check for multiple entries" );
sourcePresenceChecker = variableName + " != null && "
+ variableName + "." + entry.getReadAccessor().getSimpleName() + " != null &&"
+ variableName + "." + entry.getReadAccessor().getSimpleName() + "()."
+ entry.getPresenceChecker().getSimpleName() + "()";
}
else {
sourcePresenceChecker += " && " + variableName + " != null && "
+ variableName + "." + entry.getPresenceChecker().getSimpleName() + "()";
variableName = variableName + "." + entry.getReadAccessor().getSimpleName() + "()";
}
}
else {
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
Expand All @@ -35,12 +36,13 @@
import org.mapstruct.ap.internal.util.Fields;
import org.mapstruct.ap.internal.util.Filters;
import org.mapstruct.ap.internal.util.JavaStreamConstants;
import org.mapstruct.ap.internal.util.NativeTypes;
import org.mapstruct.ap.internal.util.Nouns;
import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.AccessorType;
import org.mapstruct.ap.internal.util.accessor.ExecutableElementAccessor;

import static org.mapstruct.ap.internal.util.Collections.first;
import org.mapstruct.ap.internal.util.NativeTypes;

/**
* Represents (a reference to) the type of a bean property, parameter etc. Types are managed per generated source file.
Expand Down Expand Up @@ -530,6 +532,15 @@ public Map<String, Accessor> getPropertyPresenceCheckers() {
if ( presenceCheckers == null ) {
List<Accessor> checkerList = filters.presenceCheckMethodsIn( getAllMethods() );
Map<String, Accessor> modifiableCheckers = new LinkedHashMap<>();
// adding custom nested presenceCheckers, if any
filters.presenceCheckMethodsFor( getAllMethods() ).entrySet().forEach( customChecker -> {
String propertyName = getPropertyName( new ExecutableElementAccessor(
customChecker.getKey(),
typeMirror,
AccessorType.GETTER
) );
modifiableCheckers.put( propertyName, customChecker.getValue() );
} );
for ( Accessor checker : checkerList ) {
modifiableCheckers.put( getPropertyName( checker ), checker );
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
package org.mapstruct.ap.internal.util;

import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
Expand Down Expand Up @@ -49,6 +50,40 @@ && isPublicNotStatic( executable )
&& accessorNamingStrategy.getMethodType( executable ) == MethodType.PRESENCE_CHECKER;
}

public ExecutableElement findPresenceCheckMethodFor(ExecutableElement executable) {
if ( isGetterMethod( executable ) && executable.getReturnType().getKind() == TypeKind.DECLARED ) {
DeclaredType returnType = (DeclaredType) executable.getReturnType();
// look for nested custom presence checker in property enclosed methods
String typeName = typeFullName( returnType );
String customPresenceChecker = accessorNamingStrategy.getPresenceCheckerMethodInType( typeName );
if ( customPresenceChecker != null ) {
for ( Element element : returnType.asElement().getEnclosedElements() ) {
if ( element instanceof ExecutableElement &&
element.getSimpleName().contentEquals( customPresenceChecker ) &&
isBooleanPublicMethod( (ExecutableElement) element ) ) {
return (ExecutableElement) element;
}
}
}

}
return null;
}

private String typeFullName(DeclaredType type) {
// FIXME: how to get type package name
Object packageName = type.asElement().getEnclosingElement().toString().replace( "package ", "" );
return packageName + "." + type.asElement().getSimpleName().toString();
}

private boolean isBooleanPublicMethod(ExecutableElement method) {
return isPublicNotStatic( method )
&& method.getParameters().isEmpty()
&& ( method.getReturnType().getKind() == TypeKind.BOOLEAN ||
"java.lang.Boolean"
.equals( getQualifiedName( method.getReturnType() ) ) );
}

public boolean isSetterMethod(ExecutableElement executable) {
return executable != null
&& isPublicNotStatic( executable )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -25,6 +26,7 @@
import org.mapstruct.ap.internal.util.accessor.Accessor;
import org.mapstruct.ap.internal.util.accessor.ExecutableElementAccessor;
import org.mapstruct.ap.internal.util.accessor.FieldElementAccessor;
import org.mapstruct.ap.internal.util.accessor.NestedExecutableElementAccessor;

import static org.mapstruct.ap.internal.util.Collections.first;
import static org.mapstruct.ap.internal.util.accessor.AccessorType.ADDER;
Expand Down Expand Up @@ -133,6 +135,20 @@ public List<Accessor> presenceCheckMethodsIn(List<ExecutableElement> elements) {
.collect( Collectors.toCollection( LinkedList::new ) );
}

public Map<ExecutableElement, Accessor> presenceCheckMethodsFor(List<ExecutableElement> elements) {
Map<ExecutableElement, Accessor> presenceCheckMethods = new HashMap<>();
for ( ExecutableElement element : elements ) {
ExecutableElement presenceChecker = accessorNaming.findPresenceCheckMethodFor( element );
if ( presenceChecker != null ) {
presenceCheckMethods.put(
element,
new NestedExecutableElementAccessor( presenceChecker, element.asType() )
);
}
}
return presenceCheckMethods;
}

public List<Accessor> setterMethodsIn(List<ExecutableElement> elements) {
return elements.stream()
.filter( accessorNaming::isSetterMethod )
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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 javax.lang.model.element.ExecutableElement;
import javax.lang.model.type.TypeMirror;

/**
* An {@link Accessor} that wraps an {@link ExecutableElement}.
*
* @author Filip Hrisafov
*/
public class NestedExecutableElementAccessor extends ExecutableElementAccessor {

public NestedExecutableElementAccessor(ExecutableElement element, TypeMirror accessedType) {
super( element, accessedType, AccessorType.PRESENCE_CHECKER );
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ default void init(MapStructProcessingEnvironment processingEnvironment) {
*/
MethodType getMethodType(ExecutableElement method);

/**
* Returns the simple name of a method in given type to be used as presence check.
*
* For instance for type {@code java.util.Optional} you may want to return 'isPresent'.
* @param typeName the java fully qualified type name
* @return the presence checker method simple name or {@code null}
*/
default String getPresenceCheckerMethodInType(String typeName) {
return null;
}

/**
* Returns the name of the property represented by the given getter or setter method.
* <p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,17 @@ public boolean isPresenceCheckMethod(ExecutableElement method) {
return methodName.startsWith( "has" ) && methodName.length() > 3;
}

/**
* Returns the simple name of a method in given type to be used as presence check.
*
* For instance for type {@code java.util.Optional} you may want to return 'isPresent'.
* @param typeName the java fully qualified type name
* @return the presence checker method simple name or {@code null}
*/
public String getPresenceCheckerMethodInType(String typeName) {
return null;
}

/**
* Analyzes the method (getter or setter) and derives the property name.
* See {@link #isGetterMethod(ExecutableElement)} {@link #isSetterMethod(ExecutableElement)}. The first three
Expand Down
Original file line number Diff line number Diff line change
@@ -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.conversion.optional;

import org.mapstruct.ap.spi.AccessorNamingStrategy;
import org.mapstruct.ap.spi.DefaultAccessorNamingStrategy;

public class CustomAccessorNamingStrategy extends DefaultAccessorNamingStrategy implements AccessorNamingStrategy {

@Override
public String getPresenceCheckerMethodInType(String typeName) {
if ( typeName.equals( "java.util.Optional" ) ) {
return "isPresent";
}
return super.getPresenceCheckerMethodInType( typeName );
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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.conversion.optional;

import java.util.Optional;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mapstruct.ap.internal.util.Collections;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.WithClasses;
import org.mapstruct.ap.testutil.WithServiceImplementation;
import org.mapstruct.ap.testutil.runner.AnnotationProcessorTestRunner;

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

@WithClasses({ OptionalMapper.class, SomeMapper.class, SourceWithOptional.class, TargetWithPlainValues.class })
@IssueKey("674")
@RunWith(AnnotationProcessorTestRunner.class)
@WithServiceImplementation(CustomAccessorNamingStrategy.class)
public class OptionalConversionTest {

@Test
public void shouldApplyOptionalConversions() {
final SourceWithOptional source = new SourceWithOptional();
source.setOptional( Optional.of( "USD" ) );
source.setOptionalsSet( Collections.asSet( Optional.of( "EUR" ), Optional.of( "CHF" ) ) );

final TargetWithPlainValues target = SomeMapper.INSTANCE.asPlain( source );

assertThat( target ).isNotNull();
assertThat( target.getOptional() ).isEqualTo( "USD" );
assertThat( target.getOptionalsSet() )
.isNotEmpty()
.containsExactlyInAnyOrder( "EUR", "CHF" );
}

@Test
public void shouldApplyReverseConversions() {
final TargetWithPlainValues target = new TargetWithPlainValues();
target.setOptional( "USD" );
target.setOptionalsSet( Collections.asSet( "JPY" ) );

final SourceWithOptional source = SomeMapper.INSTANCE.asOptional( target );

assertThat( source ).isNotNull();
assertThat( source.getOptional() ).isNotNull();
assertThat( source.getOptional().isPresent() ).isTrue();
assertThat( source.getOptional().get() ).isEqualTo( "USD" );
assertThat( source.getOptionalsSet() ).containsExactlyInAnyOrder( Optional.of( "JPY" ) );
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.conversion.optional;

import java.util.Optional;

import org.mapstruct.Mapper;

@Mapper
public interface OptionalMapper {

default <T> T fromOptional(final Optional<T> optional) {
return optional.orElse( null );
}

default <T> Optional<T> asOptional(final T optional) {
return optional != null ? Optional.of( optional ) : Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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.conversion.optional;

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

@Mapper(uses = OptionalMapper.class)
public interface SomeMapper {

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

TargetWithPlainValues asPlain(SourceWithOptional source);

void asPlain(SourceWithOptional source, @MappingTarget TargetWithPlainValues target);

SourceWithOptional asOptional(TargetWithPlainValues target);

void asOptional(TargetWithPlainValues target, @MappingTarget SourceWithOptional source);

}
Loading