/*
 * Copyright 2004-2008 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
 * 
 *      https://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.springframework.binding.mapping.impl;

import org.springframework.binding.convert.ConversionExecutionException;
import org.springframework.binding.convert.ConversionExecutor;
import org.springframework.binding.expression.EvaluationException;
import org.springframework.binding.expression.Expression;
import org.springframework.binding.mapping.Mapping;
import org.springframework.util.Assert;

/**
 * A single mapping definition, encapsulating the information necessary to map the result of evaluating an expression on
 * a source object to a property on a target object, optionally applying a type conversion during the mapping process.
 * 
 * @author Keith Donald
 * @author Scott Andrews
 */
public class DefaultMapping implements Mapping {

	/**
	 * The source expression to evaluate against a source object to map from.
	 */
	private final Expression sourceExpression;

	/**
	 * The target expression to set on a target object to map to.
	 */
	private final Expression targetExpression;

	/**
	 * Whether or not this is a required mapping; if true, the source expression must return a non-null value.
	 */
	private boolean required;

	/**
	 * A specific type conversion routine to apply during the mapping process.
	 */
	private ConversionExecutor typeConverter;

	/**
	 * Creates a new mapping.
	 * @param sourceExpression the source expression
	 * @param targetExpression the target expression
	 */
	public DefaultMapping(Expression sourceExpression, Expression targetExpression) {
		Assert.notNull(sourceExpression, "The source expression is required");
		Assert.notNull(targetExpression, "The target expression is required");
		this.sourceExpression = sourceExpression;
		this.targetExpression = targetExpression;
	}

	// implementing mapping

	public Expression getSourceExpression() {
		return sourceExpression;
	}

	public Expression getTargetExpression() {
		return targetExpression;
	}

	public boolean isRequired() {
		return required;
	}

	// optional impl getters/setters

	/**
	 * Returns the type conversion executor to use during mapping execution. May be null.
	 */
	public ConversionExecutor getTypeConverter() {
		return typeConverter;
	}

	/**
	 * Sets a specific type conversion executor to use during mapping execution.
	 * @param typeConverter the type converter
	 */
	public void setTypeConverter(ConversionExecutor typeConverter) {
		this.typeConverter = typeConverter;
	}

	/**
	 * Indicates if this mapping is a required mapping. Default is false.
	 * @param required required status
	 */
	public void setRequired(boolean required) {
		this.required = required;
	}

	/**
	 * Execute this mapping.
	 * @param context the mapping context
	 */
	public void map(DefaultMappingContext context) {
		context.setCurrentMapping(this);
		Object sourceValue;
		try {
			sourceValue = sourceExpression.getValue(context.getSource());
		} catch (EvaluationException e) {
			context.setSourceAccessError(e);
			return;
		}
		if (required && (sourceValue == null || isEmptyString(sourceValue))) {
			context.setRequiredErrorResult(sourceValue);
			return;
		}
		Object targetValue = sourceValue;
		if (sourceValue != null) {
			if (typeConverter != null) {
				try {
					targetValue = typeConverter.execute(sourceValue);
				} catch (ConversionExecutionException e) {
					context.setTypeConversionErrorResult(sourceValue, e);
					return;
				}
			}
		}
		try {
			targetExpression.setValue(context.getTarget(), targetValue);
			context.setSuccessResult(sourceValue, targetValue);
		} catch (EvaluationException e) {
			context.setTargetAccessError(sourceValue, e);
		}
	}

	private boolean isEmptyString(Object sourceValue) {
		if (sourceValue instanceof CharSequence) {
			return ((CharSequence) sourceValue).length() == 0;
		} else {
			return false;
		}
	}

	public boolean equals(Object o) {
		if (!(o instanceof DefaultMapping)) {
			return false;
		}
		DefaultMapping other = (DefaultMapping) o;
		return sourceExpression.equals(other.sourceExpression) && targetExpression.equals(other.targetExpression);
	}

	public int hashCode() {
		return sourceExpression.hashCode() + targetExpression.hashCode();
	}

	public String toString() {
		return sourceExpression + " -> " + targetExpression;
	}
}