Package org.modelmapper.internal

Source Code of org.modelmapper.internal.ImplicitMappingBuilder

/*
* Copyright 2011 the original author or authors.
*
* 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.modelmapper.internal;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.modelmapper.Converter;
import org.modelmapper.TypeMap;
import org.modelmapper.internal.PropertyInfoImpl.ValueReaderPropertyInfo;
import org.modelmapper.internal.converter.ConverterStore;
import org.modelmapper.internal.util.Iterables;
import org.modelmapper.internal.util.Primitives;
import org.modelmapper.internal.util.Strings;
import org.modelmapper.internal.util.Types;
import org.modelmapper.spi.ConditionalConverter;
import org.modelmapper.spi.ConditionalConverter.MatchResult;
import org.modelmapper.spi.Mapping;
import org.modelmapper.spi.MatchingStrategy;
import org.modelmapper.spi.NameableType;
import org.modelmapper.spi.PropertyInfo;
import org.modelmapper.spi.PropertyMapping;

/**
* Builds and populates implicit property mappings for a TypeMap.
*
* @param <S> source type
* @param <D> destination type
*
* @author Jonathan Halterman
*/
class ImplicitMappingBuilder<S, D> {
  private final TypeMapImpl<S, D> typeMap;
  private final TypeInfo<S> sourceTypeInfo;
  private final TypeMapStore typeMapStore;
  private final InheritingConfiguration configuration;
  private final ConverterStore converterStore;
  private final MatchingStrategy matchingStrategy;

  /** Mutable state */
  private final Errors errors = new Errors();
  private final PropertyNameInfoImpl propertyNameInfo;
  private final Set<Class<?>> sourceTypes = new HashSet<Class<?>>();
  private final Set<Class<?>> destinationTypes = new HashSet<Class<?>>();
  private final List<PropertyMappingImpl> mappings = new ArrayList<PropertyMappingImpl>();
  /** Mappings for which the source accessor type was not verified by the supported converter. */
  private final List<PropertyMappingImpl> partiallyMatchedMappings = new ArrayList<PropertyMappingImpl>();
  /** Mappings whose source and destination paths match by name, but not by type. */
  private final Map<Accessor, PropertyMappingImpl> intermediateMappings = new HashMap<Accessor, PropertyMappingImpl>();
  /** Mappings which are to be merged in from a pre-existing TypeMap. */
  private final List<MappingImpl> mergedMappings = new ArrayList<MappingImpl>();

  ImplicitMappingBuilder(S source, TypeMapImpl<S, D> typeMap, TypeMapStore typeMapStore,
      ConverterStore converterStore) {
    this.typeMap = typeMap;
    this.converterStore = converterStore;
    this.typeMapStore = typeMapStore;
    this.configuration = typeMap.configuration;
    sourceTypeInfo = TypeInfoRegistry.typeInfoFor(source, typeMap.getSourceType(), configuration);
    matchingStrategy = configuration.getMatchingStrategy();
    propertyNameInfo = new PropertyNameInfoImpl(typeMap.getSourceType(), configuration);
  }

  void build() {
    matchDestination(TypeInfoRegistry.typeInfoFor(typeMap.getDestinationType(), configuration));
  }

  /**
   * Matches the {@code destinationTypeInfo}'s mutator hierarchy hierarchy to the
   * {@code sourceTypeInfo}'s accessor hierarchy.
   */
  private void matchDestination(TypeInfo<?> destinationTypeInfo) {
    destinationTypes.add(destinationTypeInfo.getType());

    for (Map.Entry<String, Mutator> entry : destinationTypeInfo.getMutators().entrySet()) {
      propertyNameInfo.pushDestination(entry.getKey(), entry.getValue());
      String destPath = Strings.join(propertyNameInfo.getDestinationProperties());
      Mutator mutator = entry.getValue();

      // Skip explicit mappings
      MappingImpl existingMapping = typeMap.mappingFor(destPath);
      if (existingMapping == null) {
        matchSource(sourceTypeInfo, mutator);
        propertyNameInfo.clearSource();
        sourceTypes.clear();
      }

      // Use partially matched mappings only if there is no fully matched mapping
      if (mappings.isEmpty())
        mappings.addAll(partiallyMatchedMappings);

      if (!mappings.isEmpty()) {
        PropertyMappingImpl mapping = null;
        if (mappings.size() == 1) {
          mapping = mappings.get(0);
        } else {
          mapping = disambiguateMappings();
          if (mapping == null && !configuration.isAmbiguityIgnored())
            errors.ambiguousDestination(mappings);
        }

        if (mapping != null) {
          typeMap.addMapping(mapping);

          // If the mapping is potentially circular, add intermediate mappings
          if (Iterables.isIterable(mapping.getLastDestinationProperty().getType())) {
            for (PropertyInfo sourceAccessor : mapping.sourceAccessors) {
              PropertyMappingImpl intermediateMapping = intermediateMappings.get(sourceAccessor);
              if (intermediateMapping != null
                  && !intermediateMapping.getPath().equals(mapping.getPath()))
                typeMap.addMapping(intermediateMapping);
            }
          }
        }

        mappings.clear();
        partiallyMatchedMappings.clear();
        intermediateMappings.clear();
      } else if (!mergedMappings.isEmpty()) {
        for (MappingImpl mapping : mergedMappings)
          typeMap.addMapping(mapping);
        mergedMappings.clear();
      } else if (isMatchable(mutator.getType()) && !destinationTypes.contains(mutator.getType())
          && !typeMap.isSkipped(destPath) && !isConvertable(existingMapping)) {
        matchDestination(TypeInfoRegistry.typeInfoFor(mutator.getType(), configuration));
      }

      propertyNameInfo.popDestination();
    }

    destinationTypes.remove(destinationTypeInfo.getType());
    errors.throwConfigurationExceptionIfErrorsExist();
  }

  /**
   * Matches a source accessor hierarchy to the {@code destinationMutator}, first by checking the
   * {@code typeMapStore} for any existing TypeMaps and merging the mappings if one exists, else by
   * running the {@code matchingStrategy} against all accessors for the {@code sourceTypeInfo}.
   */
  private void matchSource(TypeInfo<?> sourceTypeInfo, Mutator destinationMutator) {
    sourceTypes.add(sourceTypeInfo.getType());

    for (Map.Entry<String, Accessor> entry : sourceTypeInfo.getAccessors().entrySet()) {
      Accessor accessor = entry.getValue();
      propertyNameInfo.pushSource(entry.getKey(), entry.getValue());
      boolean doneMatching = false;

      if (matchingStrategy.matches(propertyNameInfo)) {
        if (destinationTypes.contains(destinationMutator.getType())) {
          mappings.add(new PropertyMappingImpl(propertyNameInfo.getSourceProperties(),
              propertyNameInfo.getDestinationProperties(), true));
        } else {
          TypeMap<?, ?> propertyTypeMap = typeMapStore.get(accessor.getType(),
              destinationMutator.getType(), null);
          PropertyMappingImpl mapping = null;

          // Check to create mapping(s) from existing TypeMap
          if (propertyTypeMap != null) {
            Converter<?, ?> propertyConverter = propertyTypeMap.getConverter();
            if (propertyConverter == null)
              mergeMappings(propertyTypeMap);
            else
              mappings.add(new PropertyMappingImpl(propertyNameInfo.getSourceProperties(),
                  propertyNameInfo.getDestinationProperties(), propertyTypeMap.getProvider(),
                  propertyConverter));
            doneMatching = matchingStrategy.isExact();
          } else {
            for (ConditionalConverter<?, ?> converter : converterStore.getConverters()) {
              MatchResult matchResult = converter.match(accessor.getType(),
                  destinationMutator.getType());

              if (!MatchResult.NONE.equals(matchResult)) {
                mapping = new PropertyMappingImpl(propertyNameInfo.getSourceProperties(),
                    propertyNameInfo.getDestinationProperties(), false);

                if (MatchResult.FULL.equals(matchResult)) {
                  mappings.add(mapping);
                  doneMatching = matchingStrategy.isExact();
                  break;
                } else if (!configuration.isFullTypeMatchingRequired()) {
                  partiallyMatchedMappings.add(mapping);
                  break;
                }
              }
            }
          }

          if (mapping == null)
            intermediateMappings.put(
                accessor,
                new PropertyMappingImpl(propertyNameInfo.getSourceProperties(),
                    propertyNameInfo.getDestinationProperties(), false));
        }
      }

      if (!doneMatching
          && isMatchable(accessor.getType())
          && (!sourceTypes.contains(accessor.getType()) || accessor instanceof ValueReaderPropertyInfo))
        matchSource(TypeInfoRegistry.typeInfoFor(accessor, configuration), destinationMutator);

      propertyNameInfo.popSource();

      if (doneMatching)
        break;
    }

    sourceTypes.remove(sourceTypeInfo.getType());
  }

  /**
   * Disambiguates the captured mappings by looking for the mapping with property tokens that most
   * closely match the destination. Match closeness is calculated as the total number of matched
   * source to destination tokens / the total number of source and destination tokens. Currently
   * this algorithm does not consider class name tokens.
   *
   * @return closest matching mapping, else {@code null} if one could not be determined
   */
  PropertyMappingImpl disambiguateMappings() {
    double maxMatchRatio = -1;
    // Whether multiple mappings have the same max ratio
    boolean multipleMax = false;
    PropertyMappingImpl closestMapping = null;

    for (PropertyMappingImpl mapping : mappings) {
      double matches = 0, totalSourceTokens = 0, totalDestTokens = 0;
      String[][] allSourceTokens = new String[mapping.getSourceProperties().size()][];
      // Tracks whether a source token has been matched
      boolean[][] sourceMatches = new boolean[allSourceTokens.length][];

      // Build source tokens
      for (int i = 0; i < mapping.getSourceProperties().size(); i++) {
        PropertyInfo source = mapping.getSourceProperties().get(i);
        NameableType nameableType = NameableType.forPropertyType(source.getPropertyType());
        allSourceTokens[i] = configuration.getSourceNameTokenizer().tokenize(source.getName(),
            nameableType);
        sourceMatches[i] = new boolean[allSourceTokens[i].length];
        totalSourceTokens += allSourceTokens[i].length;
      }

      for (int destIndex = 0; destIndex < mapping.getDestinationProperties().size(); destIndex++) {
        PropertyInfo dest = mapping.getDestinationProperties().get(destIndex);
        NameableType nameableType = NameableType.forPropertyType(dest.getPropertyType());
        String[] destTokens = configuration.getDestinationNameTokenizer().tokenize(dest.getName(),
            nameableType);
        totalDestTokens += destTokens.length;

        for (int destTokenIndex = 0; destTokenIndex < destTokens.length
            && matches < totalSourceTokens; destTokenIndex++) {
          String destToken = destTokens[destTokenIndex];
          boolean matched = false;

          for (int i = 0; i < allSourceTokens.length && !matched && matches < totalSourceTokens; i++) {
            String[] sourceTokens = allSourceTokens[i];

            for (int j = 0; j < sourceTokens.length && !matched && matches < totalSourceTokens; j++) {
              if (!sourceMatches[i][j] && sourceTokens[j].equalsIgnoreCase(destToken)) {
                matched = true;
                matches++;
                sourceMatches[i][j] = true;
              }
            }
          }
        }
      }

      double matchRatio = matches / (totalSourceTokens + totalDestTokens);
      if (matchRatio == maxMatchRatio)
        multipleMax = true;

      if (matchRatio > maxMatchRatio) {
        maxMatchRatio = matchRatio;
        closestMapping = mapping;
        multipleMax = false;
      }
    }

    return multipleMax ? null : closestMapping;
  }

  /**
   * Merges mappings from an existing TypeMap into the type map under construction.
   */
  private void mergeMappings(TypeMap<?, ?> destinationMap) {
    for (Mapping mapping : destinationMap.getMappings())
      mergedMappings.add(((MappingImpl) mapping).createMergedCopy(
          propertyNameInfo.getSourceProperties(), propertyNameInfo.getDestinationProperties()));
  }

  static boolean isMatchable(Class<?> type) {
    return type != Object.class && type != String.class && !Primitives.isPrimitive(type)
        && !Iterables.isIterable(type) && !Types.isGroovyType(type);
  }

  /**
   * Indicates whether the mapping represents a PropertyMapping that is convertible to the
   * destination type.
   */
  private boolean isConvertable(Mapping mapping) {
    return mapping != null
        && mapping.getProvider() == null
        && mapping instanceof PropertyMapping
        && converterStore.getFirstSupported(((PropertyMapping) mapping).getLastSourceProperty()
            .getType(), mapping.getLastDestinationProperty().getType()) != null;
  }
}
TOP

Related Classes of org.modelmapper.internal.ImplicitMappingBuilder

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.