All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.modelmapper.internal.ExplicitMappingBuilder Maven / Gradle / Ivy

There is a newer version: 3.2.1
Show newest version
/*
 * Copyright 2011-2014 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 org.modelmapper.Condition;
import org.modelmapper.ConfigurationException;
import org.modelmapper.Converter;
import org.modelmapper.PropertyMap;
import org.modelmapper.Provider;
import org.modelmapper.builder.ConditionExpression;
import org.modelmapper.internal.ExplicitMappingVisitor.VisitedMapping;
import org.modelmapper.internal.PropertyInfoImpl.FieldPropertyInfo;
import org.modelmapper.internal.PropertyInfoImpl.MethodAccessor;
import org.modelmapper.internal.PropertyInfoImpl.ValueReaderPropertyInfo;
import org.modelmapper.internal.PropertyInfoImpl.ValueWriterPropertyInfo;
import org.modelmapper.internal.util.Assert;
import org.modelmapper.internal.util.Members;
import org.modelmapper.internal.util.Types;
import org.modelmapper.spi.PropertyType;
import org.modelmapper.spi.ValueReader;
import org.modelmapper.spi.ValueWriter;
import org.objectweb.asm.ClassReader;

import java.io.IOException;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Builds explicit property mappings.
 *
 * @author Jonathan Halterman
 */
public class ExplicitMappingBuilder implements ConditionExpression {
  private static final Pattern DOT_PATTERN = Pattern.compile("\\.");
  private static Method PROPERTY_MAP_CONFIGURE;

  private final Class sourceType;
  private final Class destinationType;
  private final InheritingConfiguration configuration;
  public volatile S source;
  public volatile D destination;
  private final Errors proxyErrors = new Errors();
  private final Errors errors = new Errors();
  private List visitedMappings;
  private final Map proxyInterceptors = new IdentityHashMap();
  private final Set propertyMappings = new HashSet();

  /** Per mapping state */
  private int currentMappingIndex;
  private VisitedMapping currentMapping;
  private MappingOptions options = new MappingOptions();
  private List sourceAccessors;
  private List destinationMutators;
  private Object sourceConstant;

  static {
    PROPERTY_MAP_CONFIGURE = Members.methodFor(PropertyMap.class, "configure",
        ExplicitMappingBuilder.class);
    PROPERTY_MAP_CONFIGURE.setAccessible(true);
  }

  static class MappingOptions {
    Condition condition;
    Converter converter;
    Provider provider;
    int skipType;
    boolean mapFromSource;
  }

  static  Collection build(Class sourceType, Class destinationType,
      InheritingConfiguration configuration, PropertyMap propertyMap) {
    return new ExplicitMappingBuilder(sourceType, destinationType, configuration)
        .build(propertyMap);
  }

  ExplicitMappingBuilder(Class sourceType, Class destinationType,
      InheritingConfiguration configuration) {
    this.sourceType = sourceType;
    this.destinationType = destinationType;
    this.configuration = configuration;
  }

  public D skip() {
    map();
    options.skipType = 1;
    return destination;
  }

  public void skip(Object destination) {
    map(destination);
    options.skipType = 2;
  }

  public void skip(Object source, Object destination) {
    map(source, destination);
    options.skipType = 3;
  }

  public D map() {
    saveLastMapping();
    getNextMapping();
    return destination;
  }

  public D map(Object subject) {
    saveLastMapping();
    getNextMapping();
    recordSourceValue(subject);
    return destination;
  }

  public void map(Object source, Object destination) {
    saveLastMapping();
    getNextMapping();
    recordSourceValue(source);
  }

  public  T source(String sourcePropertyPath) {
    if (sourcePropertyPath == null)
      errors.errorNullArgument("sourcePropertyPath");
    if (sourceAccessors != null)
      saveLastMapping();

    String[] propertyNames = DOT_PATTERN.split(sourcePropertyPath);
    sourceAccessors = new ArrayList(propertyNames.length);
    ValueReader valueReader = configuration.valueAccessStore.getFirstSupportedReader(sourceType);
    if (valueReader != null)
      for (String propertyName : propertyNames)
        sourceAccessors.add(ValueReaderPropertyInfo.create(valueReader, propertyName));
    else {
      Accessor accessor = null;
      for (String propertyName : propertyNames) {
        Class propertyType = accessor == null ? sourceType : accessor.getType();
        accessor = PropertyInfoRegistry.accessorFor(propertyType, propertyName, configuration);
        if (accessor == null) {
          errors.errorInvalidSourcePath(sourcePropertyPath, propertyType, propertyName);
          return null;
        }

        sourceAccessors.add(accessor);
      }
    }

    return null;
  }

  public Object destination(String destPropertyPath) {
    if (destPropertyPath == null)
      errors.errorNullArgument("destPropertyPath");

    String[] propertyNames = DOT_PATTERN.split(destPropertyPath);
    destinationMutators = new ArrayList(propertyNames.length);
    ValueWriter valueWriter = configuration.valueMutateStore.getFirstSupportedWriter(destinationType);
    if (valueWriter != null)
      for (String propertyName : propertyNames)
        destinationMutators.add(ValueWriterPropertyInfo.create(valueWriter, propertyName));
    else {
      Mutator mutator = null;
      for (String propertyName : propertyNames) {
        Class propertyType = mutator == null ? destinationType : mutator.getType();
        mutator = PropertyInfoRegistry.mutatorFor(propertyType, propertyName, configuration);
        if (mutator == null) {
          errors.errorInvalidDestinationPath(destPropertyPath, propertyType, propertyName);
          return null;
        }
        destinationMutators.add(mutator);
      }
    }
    return null;
  }

  public ConditionExpression using(Converter converter) {
    saveLastMapping();
    if (converter == null)
      errors.errorNullArgument("converter");
    Assert.state(options.converter == null, "using() can only be called once per mapping.");
    options.converter = converter;
    return this;
  }

  public ConditionExpression when(Condition condition) {
    saveLastMapping();
    if (condition == null)
      errors.errorNullArgument("condition");
    Assert.state(options.condition == null, "when() can only be called once per mapping.");
    options.condition = condition;
    return this;
  }

  public ConditionExpression with(Provider provider) {
    saveLastMapping();
    if (provider == null)
      errors.errorNullArgument("provider");
    Assert.state(options.provider == null, "withProvider() can only be called once per mapping.");
    options.provider = provider;
    return this;
  }

  /**
   * Builds and returns all property mappings defined in the {@code propertyMap}.
   */
  Collection build(PropertyMap propertyMap) {
    try {
      PROPERTY_MAP_CONFIGURE.invoke(propertyMap, this);
      saveLastMapping();
    } catch (IllegalAccessException e) {
      errors.errorAccessingConfigure(e);
    } catch (InvocationTargetException e) {
      Throwable cause = e.getCause();
      if (cause instanceof ConfigurationException)
        throw (ConfigurationException) cause;
      else
        errors.addMessage(cause, "Failed to configure mappings");
    } catch (NullPointerException e) {
      if (proxyErrors.hasErrors()) {
        throw proxyErrors.toException();
      }
      throw e;
    }

    errors.throwConfigurationExceptionIfErrorsExist();
    return propertyMappings;
  }

  /**
   * Visits the {@code propertyMap} and captures and validates mappings.
   */
  public void visitPropertyMap(PropertyMap propertyMap) {
    String propertyMapClassName = propertyMap.getClass().getName();

    try {
      ClassReader cr = new ClassReader(propertyMap.getClass().getClassLoader().getResourceAsStream(
          propertyMapClassName.replace('.', '/') + ".class"));
      ExplicitMappingVisitor visitor = new ExplicitMappingVisitor(errors, configuration,
          propertyMapClassName, destinationType.getName(), propertyMap.getClass().getClassLoader());
      cr.accept(visitor, ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
      visitedMappings = visitor.mappings;
    } catch (IOException e) {
      errors.errorReadingClass(e, propertyMapClassName);
    }

    errors.throwConfigurationExceptionIfErrorsExist();
    createProxies();
  }

  /**
   * Creates the source and destination proxy models.
   */
  private void createProxies() {
    source = createProxy(sourceType);
    destination = createProxy(destinationType);

    for (VisitedMapping mapping : visitedMappings) {
      createAccessorProxies(source, mapping.sourceAccessors);
      createAccessorProxies(destination, mapping.destinationAccessors);
    }
  }

  private void createAccessorProxies(Object proxy, List accessors) {
    for (Accessor accessor : accessors) {
      if (accessor instanceof MethodAccessor) {
        ExplicitMappingInterceptor interceptor = proxyInterceptors.get(proxy);
        String methodName = accessor.getMember().getName();
        proxy = interceptor.methodProxies.get(methodName);
        if (proxy == null) {
          proxy = createProxy(accessor.getType());
          interceptor.methodProxies.put(methodName, proxy);
        }
      } else if (accessor instanceof FieldPropertyInfo) {
        FieldPropertyInfo field = (FieldPropertyInfo) accessor;
        Object nextProxy = field.getValue(proxy);
        if (nextProxy == null) {
          nextProxy = createProxy(field.getType());
          field.setValue(proxy, nextProxy);
        }
        proxy = nextProxy;
      }
    }
  }

  public final class ExplicitMappingInterceptor implements InvocationHandler {
    private final Map methodProxies = new HashMap();

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) {
      if (args.length == 1) {
        sourceConstant = args[0];
        if (sourceConstant != null && sourceConstant == source)
          errors.missingSource();
      }
      return methodProxies.get(method.getName());
    }
  }

  private  T createProxy(Class type) {
    ExplicitMappingInterceptor interceptor = new ExplicitMappingInterceptor();

    try {
      T proxy = ProxyFactory.proxyFor(type, interceptor, proxyErrors, configuration.isUseOSGiClassLoaderBridging());
      proxyInterceptors.put(proxy, interceptor);
      return proxy;
    } catch (ErrorsException e) {
      return null;
    }
  }

  private void getNextMapping() {
    if (currentMappingIndex < visitedMappings.size())
      currentMapping = visitedMappings.get(currentMappingIndex++);
  }

  private void recordSourceValue(Object sourceValue) {
    if (sourceValue != null) {
      if (sourceValue == source)
        options.mapFromSource = true;
      else if (!Types.isProxied(sourceValue.getClass()))
        sourceConstant = sourceValue;
    }
  }

  /**
   * Validates the current mapping that was recorded via a MapExpression.
   */
  private void validateRecordedMapping() {
    if (currentMapping.destinationMutators == null || currentMapping.destinationMutators.isEmpty())
      errors.missingDestination();
    // If mapping a field without a source
    else if (options.skipType == 0
        && (currentMapping.sourceAccessors == null || currentMapping.sourceAccessors.isEmpty())
        && currentMapping.destinationMutators.get(currentMapping.destinationMutators.size() - 1)
            .getPropertyType()
            .equals(PropertyType.FIELD) && options.converter == null && !options.mapFromSource
        && sourceConstant == null)
      errors.missingSource();
    else if (options.skipType == 2 && options.condition != null)
      errors.conditionalSkipWithoutSource();
  }

  private void saveLastMapping() {
    if (currentMapping != null) {
      try {
        MappingImpl mapping;
        if (currentMapping.sourceAccessors.isEmpty())
          currentMapping.sourceAccessors = sourceAccessors;
        if (currentMapping.destinationMutators.isEmpty())
          currentMapping.destinationMutators = destinationMutators;

        validateRecordedMapping();
        if (!errors.hasErrors()) {
          if (options.mapFromSource)
            mapping = new SourceMappingImpl(sourceType, currentMapping.destinationMutators,
                options);
          else if (currentMapping.sourceAccessors == null)
            mapping = new ConstantMappingImpl(sourceConstant, currentMapping.destinationMutators,
                options);
          else
            mapping = new PropertyMappingImpl(currentMapping.sourceAccessors,
                currentMapping.destinationMutators, options);

          if (!propertyMappings.add(mapping))
            errors.duplicateMapping(mapping.getLastDestinationProperty());
        }
      } finally {
        currentMapping = null;
        options = new MappingOptions();
        sourceAccessors = null;
        sourceConstant = null;
      }
    }
  }
}