/*
 * This file is part of Nuts Framework.
 * Copyright (C) 2009 Nuts Develop Team.
 *
 * Nuts Framework is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License any later version.
 *
 * Nuts Framework is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.exts.xwork2.interceptor;

import java.util.HashMap;
import java.util.Map;

import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ValidationAware;
import com.opensymphony.xwork2.conversion.impl.XWorkConverter;
import com.opensymphony.xwork2.interceptor.AbstractInterceptor;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.util.ValueStack;

/**
 * <!-- START SNIPPET: description --> ConversionErrorInterceptor adds conversion errors from the
 * ActionContext to the Action's field errors.
 * 
 * <p/>
 * This interceptor adds any error found in the {@link ActionContext}'s conversionErrors map as a
 * field error (provided that the action implements {@link ValidationAware}). In addition, any field
 * that contains a validation error has its original value saved such that any subsequent requests
 * for that value return the original value rather than the value in the action. This is important
 * because if the value "abc" is submitted and can't be converted to an int, we want to display the
 * original string ("abc") again rather than the int value (likely 0, which would make very little
 * sense to the user).
 * 
 * 
 * <!-- END SNIPPET: description -->
 * 
 * <p/>
 * <u>Interceptor parameters:</u>
 * 
 * <!-- START SNIPPET: parameters -->
 * 
 * <ul>
 * 
 * <li>ignoreOnErrorAdded: do not add field error message if an error message already exists. (default: true)</li>
 * 
 * </ul>
 * 
 * <!-- END SNIPPET: parameters -->
 * 
 * <p/>
 * 
 * <p/>
 * <u>Example code:</u>
 * 
 * <pre>
 * &lt;!-- START SNIPPET: example --&gt;
 * &lt;action name=&quot;someAction&quot; class=&quot;com.examples.SomeAction&quot;&gt;
 *     &lt;interceptor-ref name=&quot;params&quot;/&gt;
 *     &lt;interceptor-ref name=&quot;conversionError&quot;/&gt;
 *     &lt;result name=&quot;success&quot;&gt;good_result.ftl&lt;/result&gt;
 * &lt;/action&gt;
 * &lt;!-- END SNIPPET: example --&gt;
 * </pre>
 * 
 */
@SuppressWarnings("serial")
public class ConversionErrorInterceptor extends AbstractInterceptor {

	/**
	 * ORIGINAL_PROPERTY_OVERRIDE = "original.property.override";
	 */
	public static final String ORIGINAL_PROPERTY_OVERRIDE = "original.property.override";

	protected boolean ignoreOnErrorAdded = true;

	/**
	 * @param ignoreIfErrorExists the ignoreIfErrorExists to set
	 */
	public void setIgnoreIfErrorExists(String ignoreIfErrorExists) {
		this.ignoreOnErrorAdded = Boolean.parseBoolean(ignoreIfErrorExists);
	}

	/**
	 * Allows the Interceptor to do some processing on the request before and/or after the rest of
	 * the processing of the request by the {@link ActionInvocation} or to short-circuit the
	 * processing and just return a String return code.
	 * 
	 * @param invocation the action invocation
	 * @return the return code, either returned from {@link ActionInvocation#invoke()}, or from the
	 *         interceptor itself.
	 * @throws Exception any system-level error, as defined in
	 *             {@link com.opensymphony.xwork2.Action#execute()}.
	 */
	@Override
	public String intercept(ActionInvocation invocation) throws Exception {
		ActionContext invocationContext = invocation.getInvocationContext();
		Map<String, Object> conversionErrors = invocationContext.getConversionErrors();
		ValueStack stack = invocationContext.getValueStack();

		Map<Object, Object> fakie = new HashMap<Object, Object>();

		for (Map.Entry<String, Object> entry : conversionErrors.entrySet()) {
			String propertyName = entry.getKey();
			Object value = entry.getValue();

			if (shouldAddError(propertyName, value)) {
				Object action = invocation.getAction();
				if (action instanceof ValidationAware) {
					ValidationAware va = (ValidationAware) action;
	
					if (!(ignoreOnErrorAdded && va.getFieldErrors().containsKey(propertyName))) {
						String message = XWorkConverter.getConversionErrorMessage(propertyName, stack);
						va.addFieldError(propertyName, message);
					}
				}
			}

			fakie.put(propertyName, getOverrideExpr(invocation, value));
		}

		if (!fakie.isEmpty()) {
			// if there were some errors, put the original (fake) values in place right before the
			// result
			stack.getContext().put(ORIGINAL_PROPERTY_OVERRIDE, fakie);
			invocation.addPreResultListener(new PreResultListener() {
				@SuppressWarnings("unchecked")
				public void beforeResult(ActionInvocation invocation, String resultCode) {
					Map<Object, Object> fakie = (Map<Object, Object>) invocation
							.getInvocationContext().get(ORIGINAL_PROPERTY_OVERRIDE);

					if (fakie != null) {
						invocation.getStack().setExprOverrides(fakie);
					}
				}
			});
		}
		return invocation.invoke();
	}

    protected Object getOverrideExpr(ActionInvocation invocation, Object value) {
        ValueStack stack = invocation.getStack();

        try {
            stack.push(value);

            return "'" + stack.findValue("top", String.class) + "'";
        } finally {
            stack.pop();
        }
    }

    /**
     * Returns <tt>false</tt> if the value is null, "", or {""} (array of size 1 with a blank element). Returns
     * <tt>true</tt> otherwise.
     *
     * @param propertyName the name of the property to check.
     * @param value        the value to error check.
     * @return <tt>false</tt>  if the value is null, "", or {""}, <tt>true</tt> otherwise.
     */
    protected boolean shouldAddError(String propertyName, Object value) {
        if (value == null) {
            return false;
        }

        if ("".equals(value)) {
            return false;
        }

        if (value instanceof String[]) {
            String[] array = (String[]) value;

            if (array.length == 0) {
                return false;
            }

            if (array.length > 1) {
                return true;
            }

            String str = array[0];

            if ("".equals(str)) {
                return false;
            }
        }

        return true;
    }
}
