/*
 * $Id: WrappingLookupCommand.java 471754 2006-11-06 14:55:09Z husted $
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.struts.chain.commands.generic;

import org.apache.commons.beanutils.ConstructorUtils;
import org.apache.commons.chain2.Catalog;
import org.apache.commons.chain2.CatalogFactory;
import org.apache.commons.chain2.Command;
import org.apache.commons.chain2.Context;
import org.apache.commons.chain2.Filter;
import org.apache.commons.chain2.Processing;
import org.apache.commons.chain2.impl.CatalogFactoryBase;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts.chain.commands.util.ClassUtils;

/**
 * <p>Variant on chain LookupCommand which can optionally wrap the context it
 * passes to the looked up command in an alternative class.</p>
 */
public class WrappingLookupCommand implements Filter<String, Object, Context<String, Object>> {

    /**
     * Provide Commons Logging instance for this class.
     */
    private static final Log LOG = LogFactory.getLog(WrappingLookupCommand.class);

    // ------------------------------------------------------ Instance Variables

    /**
     * <p>Field for property.</p>
     */
    private String catalogName;

    /**
     * <p>Field for property.</p>
     */
    private String name;

    /**
     * <p>Field for property.</p>
     */
    private String nameKey;

    /**
     * <p>Field for property.</p>
     */
    private String wrapperClassName = null;

    /**
     * <p>Field for property.</p>
     */
    private boolean optional;

    /**
     * <p>Zero-argument constructor.</p>
     */
    public WrappingLookupCommand() {
        this.catalogName = null;
        this.name = null;
        this.nameKey = null;
        this.optional = false;
    }

    /**
     * <p>Return CatalogName property.  </p>
     *
     * @return Value of CatalogName property.
     */
    public String getCatalogName() {
        return this.catalogName;
    }

    /**
     * <p>Set CatalogName property.</p>
     *
     * @param val New value for CatalogName
     */
    public void setCatalogName(final String val) {
        this.catalogName = val;
    }

    /**
     * <p>Retrieve Name property.</p>
     *
     * @return Value of Name property
     */
    public String getName() {
        return this.name;
    }

    /**
     * <p>Set Name property.</p>
     *
     * @param val New value for Name
     */
    public void setName(final String val) {
        this.name = val;
    }

    /**
     * <p>Return NameKey property.</p>
     *
     * @return Value of NameKey property.
     */
    public String getNameKey() {
        return this.nameKey;
    }

    /**
     * <p>Set NameKey property.</p>
     *
     * @param val New value for NameKey
     */
    public void setNameKey(final String val) {
        this.nameKey = val;
    }

    /**
     * <p>Test Optional property.</p>
     *
     * @return TRUE if Optional is TRUE.
     */
    public boolean isOptional() {
        return this.optional;
    }

    /**
     * <p>Set Optional property.</p>
     *
     * @param val New value for Optional
     */
    public void setOptional(final boolean val) {
        this.optional = val;
    }

    /**
     * <p>Return the WrapperClass property.</p>
     *
     * @return The WrapperClass property
     */
    public String getWrapperClassName() {
        return this.wrapperClassName;
    }

    /**
     * <p>Set WrapperClassName property. </p>
     *
     * @param val The name of a WrapperClass
     */
    public void setWrapperClassName(final String val) {
        this.wrapperClassName = val;
    }

    /**
     * <p>Invoke the Command for a Context, returning TRUE if processing
     * should halt.</p>
     *
     * @param context Our ActionContext
     * @return TRUE if processing should halt
     *
     */
    @Override
    public Processing execute(final Context<String, Object> context) {
        if (LOG.isTraceEnabled()) {
            LOG.trace("execute [" + this + "]");
        }

        Command<String, Object, Context<String, Object>> command = getCommand(context);

        if (command != null) {
            try {
                return command.execute(getContext(context));
            } catch (final ReflectiveOperationException ex) {
                LOG.error("Error wrapping context in postprocess", ex);
            }
        }
        return Processing.CONTINUE;

    }

    /**
     * <p>Process the Exception for any Command that is a filter.</p>
     *
     * @param context   Our ActionContext
     * @param exception The Exception thrown by another Command in a Chain
     * @return TRUE if there is a Filter to process
     */
    @Override
    public boolean postprocess(final Context<String, Object> context, final Exception exception) {
        Command<String, Object, Context<String, Object>> command = getCommand(context);
        if (command instanceof Filter) {
            try {
                return ((Filter<String, Object, Context<String, Object>>) command).postprocess(
                        getContext(context), exception);
            } catch (final ReflectiveOperationException ex) {
                LOG.error("Error wrapping context in postprocess", ex);
            }
        }

        return false;
    }

    /**
     * <p>Return the Command to process for this Context.</p>
     *
     * @param context The Context we are processing
     * @return The Command to process for this Context
     */
    protected Command<String, Object, Context<String, Object>> getCommand(
            final Context<String, Object> context) {

        CatalogFactory<String, Object, Context<String, Object>> catalogFactory =
                CatalogFactoryBase.getInstance();

        String cn = getCatalogName();

        Catalog<String, Object, Context<String, Object>> catalog;
        if (cn == null) {
            catalog = catalogFactory.getCatalog();
            // for debugging purposes
            cn = "{default}";
        } else {
            catalog = catalogFactory.getCatalog(cn);
        }

        if (catalog == null) {
            throw new IllegalArgumentException("Cannot find catalog '" + cn + "'");
        }

        String propName = getName();
        if (propName == null) {
            propName = (String) context.get(getNameKey());
        }

        if (propName != null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("Lookup command " + propName + " in catalog " + cn);
            }

            Command<String, Object, Context<String, Object>> command = catalog.getCommand(propName);

            if (LOG.isDebugEnabled()) {
                LOG.debug("Found command " + command + ";" + " optional: " + isOptional());
            }

            if ((command == null) && !isOptional()) {
                throw new IllegalArgumentException("Cannot find command " + "'"
                    + propName + "' in catalog '" + cn + "'");
            }

            return command;
        }

        throw new IllegalArgumentException("No command name");
    }

    /**
     * <p>If the wrapperClassName property is not null, return a Context of
     * the type specified by wrapperClassName, instantiated using a single-arg
     * constructor which takes the context passed as an argument to this
     * method.</p>
     *
     * <p>This method throws an exception if the wrapperClass cannot be found,
     * or if there are any errors instantiating the wrapping context.</p>
     *
     * @param context Context we are processing
     * @return Context wrapper
     * @throws ReflectiveOperationException    On failed instantiation
     */
    protected Context<String, Object> getContext(final Context<String, Object> context)
            throws ReflectiveOperationException {

        if (this.wrapperClassName == null) {
            if (LOG.isDebugEnabled()) {
                LOG.debug("No defined wrapper class; " + "returning original context.");
            }
            return context;
        }

        if (LOG.isDebugEnabled()) {
            LOG.debug("Looking for wrapper class: " + this.wrapperClassName);
        }

        Class<? extends Context<String, Object>> wrapperClass =
                    ClassUtils.getApplicationClass(this.wrapperClassName);

        if (LOG.isDebugEnabled()) {
            LOG.debug("Instantiating wrapper class");
        }

        return ConstructorUtils.invokeConstructor(wrapperClass, new Object[] {context});
    }
}
