/** * Copyright 2012-2017 Gunnar Morling (http://www.gunnarmorling.de/) * and/or other contributors as indicated by the @authors tag. See the * copyright.txt file in the distribution for a full listing of all * contributors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.mapstruct.ap.internal.model.source; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import javax.lang.model.type.DeclaredType; import org.mapstruct.ap.internal.model.common.Parameter; import org.mapstruct.ap.internal.model.common.Type; import org.mapstruct.ap.internal.model.common.TypeFactory; import org.mapstruct.ap.internal.prism.CollectionMappingStrategyPrism; import org.mapstruct.ap.internal.util.Executables; import org.mapstruct.ap.internal.util.FormattingMessager; import org.mapstruct.ap.internal.util.Message; import org.mapstruct.ap.internal.util.accessor.Accessor; /** * This class describes the target side of a property mapping. * <p> * It contains the target parameter, and all individual (nested) property entries. So consider the following mapping * method: * * <pre> * @Mapping(source = "in.propA.propB" target = "propC") * TypeB mappingMethod(TypeA in); * </pre> * * Then: * <ul> * <li>{@code parameter} will describe {@code in}</li> * <li>{@code propertyEntries[0]} will describe {@code propA}</li> * <li>{@code propertyEntries[1]} will describe {@code propB}</li> * </ul> * * After building, {@link #isValid()} will return true when when no problems are detected during building. * * @author Sjaak Derksen */ public class TargetReference { private final Parameter parameter; private final List<PropertyEntry> propertyEntries; private final boolean isValid; /** * Builds a {@link TargetReference} from an {@code @Mappping}. */ public static class BuilderFromTargetMapping { private Mapping mapping; private SourceMethod method; private FormattingMessager messager; private TypeFactory typeFactory; private boolean isReverse; /** * Needed when we are building from reverse mapping. It is needed, so we can remove the first level if it is * needed. * E.g. If we have a mapping like: * <code> * {@literal @}Mapping( target = "letterSignature", source = "dto.signature" ) * </code> * * When it is reversed it will look like: * <code> * {@literal @}Mapping( target = "dto.signature", source = "letterSignature" ) * </code> * * The {@code dto} needs to be considered as a possibility for a target name only if a Target Reference for * a reverse is created. */ private Parameter reverseSourceParameter; /** * During {@link #getTargetEntries(Type, String[])} an error can occur. However, we are invoking * that multiple times because the first entry can also be the name of the parameter. Therefore we keep * the error message until the end when we can report it. */ private Message errorMessage; public BuilderFromTargetMapping messager(FormattingMessager messager) { this.messager = messager; return this; } public BuilderFromTargetMapping mapping(Mapping mapping) { this.mapping = mapping; return this; } public BuilderFromTargetMapping method(SourceMethod method) { this.method = method; return this; } public BuilderFromTargetMapping typeFactory(TypeFactory typeFactory) { this.typeFactory = typeFactory; return this; } public BuilderFromTargetMapping isReverse(boolean isReverse) { this.isReverse = isReverse; return this; } public BuilderFromTargetMapping reverseSourceParameter(Parameter reverseSourceParameter) { this.reverseSourceParameter = reverseSourceParameter; return this; } public TargetReference build() { String targetName = mapping.getTargetName(); if ( targetName == null ) { return null; } String[] segments = targetName.split( "\\." ); Parameter parameter = method.getMappingTargetParameter(); boolean foundEntryMatch; Type resultType = method.getResultType(); // there can be 4 situations // 1. Return type // 2. A reverse target reference where the source parameter name is used in the original mapping // 3. @MappingTarget, with // 4. or without parameter name. String[] targetPropertyNames = segments; List<PropertyEntry> entries = getTargetEntries( resultType, targetPropertyNames ); foundEntryMatch = (entries.size() == targetPropertyNames.length); if ( !foundEntryMatch && segments.length > 1 && matchesSourceOrTargetParameter( segments[0], parameter, reverseSourceParameter, isReverse ) ) { targetPropertyNames = Arrays.copyOfRange( segments, 1, segments.length ); entries = getTargetEntries( resultType, targetPropertyNames ); foundEntryMatch = (entries.size() == targetPropertyNames.length); } if ( !foundEntryMatch && errorMessage != null) { // This is called only for reporting errors reportMappingError( errorMessage, mapping.getTargetName() ); } // foundEntryMatch = isValid, errors are handled here, and the BeanMapping uses that to ignore // the creation of mapping for invalid TargetReferences return new TargetReference( parameter, entries, foundEntryMatch ); } private List<PropertyEntry> getTargetEntries(Type type, String[] entryNames) { // initialize CollectionMappingStrategyPrism cms = method.getMapperConfiguration().getCollectionMappingStrategy(); List<PropertyEntry> targetEntries = new ArrayList<PropertyEntry>(); Type nextType = type; // iterate, establish for each entry the target write accessors. Other than setter is only allowed for // last entry for ( int i = 0; i < entryNames.length; i++ ) { Accessor targetReadAccessor = nextType.getPropertyReadAccessors().get( entryNames[i] ); Accessor targetWriteAccessor = nextType.getPropertyWriteAccessors( cms ).get( entryNames[i] ); boolean isLast = i == entryNames.length - 1; boolean isNotLast = i < entryNames.length - 1; if ( isWriteAccessorNotValidWhenNotLast( targetWriteAccessor, isNotLast ) || isWriteAccessorNotValidWhenLast( targetWriteAccessor, targetReadAccessor, mapping, isLast ) || ( isNotLast && targetReadAccessor == null ) ) { // there should always be a write accessor (except for the last when the mapping is ignored and // there is a read accessor) and there should be read accessor mandatory for all but the last setErrorMessage( targetWriteAccessor, targetReadAccessor ); break; } if ( isLast || ( Executables.isSetterMethod( targetWriteAccessor ) || Executables.isFieldAccessor( targetWriteAccessor ) ) ) { // only intermediate nested properties when they are a true setter or field accessor // the last may be other readAccessor (setter / getter / adder). nextType = findNextType( nextType, targetWriteAccessor, targetReadAccessor ); // check if an entry alread exists, otherwise create String[] fullName = Arrays.copyOfRange( entryNames, 0, i + 1 ); PropertyEntry propertyEntry = PropertyEntry.forTargetReference( fullName, targetReadAccessor, targetWriteAccessor, nextType ); targetEntries.add( propertyEntry ); } } return targetEntries; } /** * Finds the next type based on the initial type. * * @param initial for which a next type should be found * @param targetWriteAccessor the write accessor * @param targetReadAccessor the read accessor * @return the next type that should be used for finding a property entry */ private Type findNextType(Type initial, Accessor targetWriteAccessor, Accessor targetReadAccessor) { Type nextType; Accessor toUse = targetWriteAccessor != null ? targetWriteAccessor : targetReadAccessor; if ( Executables.isGetterMethod( toUse ) || Executables.isFieldAccessor( toUse ) ) { nextType = typeFactory.getReturnType( (DeclaredType) initial.getTypeMirror(), toUse ); } else { nextType = typeFactory.getSingleParameter( (DeclaredType) initial.getTypeMirror(), toUse ).getType(); } return nextType; } private void setErrorMessage(Accessor targetWriteAccessor, Accessor targetReadAccessor) { if ( targetWriteAccessor == null && targetReadAccessor == null ) { errorMessage = Message.BEANMAPPING_UNKNOWN_PROPERTY_IN_RESULTTYPE; } else if ( targetWriteAccessor == null ) { errorMessage = Message.BEANMAPPING_PROPERTY_HAS_NO_WRITE_ACCESSOR_IN_RESULTTYPE; } } private void reportMappingError(Message msg, Object... objects) { messager.printMessage( method.getExecutable(), mapping.getMirror(), mapping.getSourceAnnotationValue(), msg, objects ); } /** * A write accessor is not valid if it is {@code null} and it is not last. i.e. for nested target mappings * there must be a write accessor for all entries except the last one. * * @param accessor that needs to be checked * @param isNotLast whether or not this is the last write accessor in the entry chain * * @return {@code true} if the accessor is not valid, {@code false} otherwise */ private static boolean isWriteAccessorNotValidWhenNotLast(Accessor accessor, boolean isNotLast) { return accessor == null && isNotLast; } /** * For a last accessor to be valid, a read accessor should exist and the mapping should be ignored. All other * cases represent an invalid write accessor. This method will evaluate to {@code true} if the following is * {@code true}: * <ul> * <li>{@code writeAccessor} is {@code null}</li> * <li>It is for the last entry</li> * <li>A read accessor does not exist, or the mapping is not ignored</li> * </ul> * * @param writeAccessor that needs to be checked * @param readAccessor that is used * @param mapping that is used * @param isLast whether or not this is the last write accessor in the entry chain * * @return {@code true} if the write accessor is not valid, {@code false} otherwise. See description for more * information */ private static boolean isWriteAccessorNotValidWhenLast(Accessor writeAccessor, Accessor readAccessor, Mapping mapping, boolean isLast) { return writeAccessor == null && isLast && ( readAccessor == null || !mapping.isIgnored() ); } /** * Validates that the {@code segment} is the same as the {@code targetParameter} or the {@code * reverseSourceParameter} names * * @param segment that needs to be checked * @param targetParameter the target parameter if it exists * @param reverseSourceParameter the reverse source parameter if it exists * @param isReverse whether a reverse {@link TargetReference} is being built * * @return {@code true} if the segment matches the name of the {@code targetParameter} or the name of the * {@code reverseSourceParameter} when this is a reverse {@link TargetReference} is being built, {@code * false} otherwise */ private static boolean matchesSourceOrTargetParameter(String segment, Parameter targetParameter, Parameter reverseSourceParameter, boolean isReverse) { boolean matchesTargetParameter = targetParameter != null && targetParameter.getName().equals( segment ); return matchesTargetParameter || isReverse && reverseSourceParameter != null && reverseSourceParameter.getName().equals( segment ); } } private TargetReference(Parameter sourceParameter, List<PropertyEntry> sourcePropertyEntries, boolean isValid) { this.parameter = sourceParameter; this.propertyEntries = sourcePropertyEntries; this.isValid = isValid; } public Parameter getParameter() { return parameter; } public List<PropertyEntry> getPropertyEntries() { return propertyEntries; } public boolean isValid() { return isValid; } public List<String> getElementNames() { List<String> elementNames = new ArrayList<String>(); if ( parameter != null ) { // only relevant for source properties elementNames.add( parameter.getName() ); } for ( PropertyEntry propertyEntry : propertyEntries ) { elementNames.add( propertyEntry.getName() ); } return elementNames; } public TargetReference pop() { if ( propertyEntries.size() > 1 ) { List<PropertyEntry> newPropertyEntries = new ArrayList<PropertyEntry>( propertyEntries.size() - 1 ); for ( PropertyEntry propertyEntry : propertyEntries ) { PropertyEntry newPropertyEntry = propertyEntry.pop(); if ( newPropertyEntry != null ) { newPropertyEntries.add( newPropertyEntry ); } } return new TargetReference( null, newPropertyEntries, isValid ); } else { return null; } } }