/*
 * 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.commons.chain2.base;

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 java.util.Map;

/**
 * <p>Look up a specified {@link Command} (which could also be a
 * {@link org.apache.commons.chain2.Chain})
 * in a {@link Catalog}, and delegate execution to it.  If the delegated-to
 * {@link Command} is also a {@link Filter}, its <code>postprocess()</code>
 * method will also be invoked at the appropriate time.</p>
 *
 * <p>The name of the {@link Command} can be specified either directly (via
 * the <code>name</code> property) or indirectly (via the <code>nameKey</code>
 * property).  Exactly one of these must be set.</p>
 *
 * <p>If the <code>optional</code> property is set to <code>true</code>,
 * failure to find the specified command in the specified catalog will be
 * silently ignored.  Otherwise, a lookup failure will trigger an
 * <code>IllegalArgumentException</code>.</p>
 *
 * @version $Id$
 *
 * @param <K> Context key type
 * @param <V> Context value type
 * @param <C> Type of the context associated with this command
 */
public class LookupCommand<K, V, C extends Map<K, V>> implements Filter<K, V, C> {

    private CatalogFactory<K, V, C> catalogFactory = null;
    private String catalogName = null;
    private String name = null;
    private String nameKey = null;
    private boolean optional = false;
    private boolean ignoreExecuteResult = false;
    private boolean ignorePostprocessResult = false;

    // -------------------------------------------------------------- Constructors

    /**
     * Create an instance, setting its <code>catalogFactory</code> property to the
     * value of <code>CatalogFactory.getInstance()</code>.
     *
     * @since Chain 1.1
     */
    public LookupCommand() {
        this(CatalogFactoryBase.<K, V, C>getInstance());
    }

    /**
     * Create an instance and initialize the <code>catalogFactory</code> property
     * to given <code>factory</code>/
     *
     * @param factory The Catalog Factory.
     *
     * @since Chain 1.1
     */
    public LookupCommand(final CatalogFactory<K, V, C> factory) {
        this.catalogFactory = factory;
    }

    // -------------------------------------------------------------- Properties

    /**
     * <p>Set the {@link CatalogFactoryBase} from which lookups will be
     * performed.</p>
     *
     * @param factory The Catalog Factory.
     *
     * @since Chain 1.1
     */
    public void setCatalogFactory(final CatalogFactory<K, V, C> factory) {
        this.catalogFactory = factory;
    }

    /**
     * Return the {@link CatalogFactoryBase} from which lookups will be performed.
     * @return The Catalog factory.
     *
     * @since Chain 1.1
     */
    public CatalogFactory<K, V, C> getCatalogFactory() {
        return this.catalogFactory;
    }

    /**
     * <p>Return the name of the {@link Catalog} to be searched, or
     * <code>null</code> to search the default {@link Catalog}.</p>
     * @return The Catalog name.
     */
    public String getCatalogName() {
        return this.catalogName;
    }

    /**
     * <p>Set the name of the {@link Catalog} to be searched, or
     * <code>null</code> to search the default {@link Catalog}.</p>
     *
     * @param val The new {@link Catalog} name or <code>null</code>
     */
    public void setCatalogName(final String val) {
        this.catalogName = val;
    }

    /**
     * <p>Return the name of the {@link Command} that we will look up and
     * delegate execution to.</p>
     * @return The name of the Command.
     */
    public String getName() {
        return this.name;
    }

    /**
     * <p>Set the name of the {@link Command} that we will look up and
     * delegate execution to.</p>
     *
     * @param val The new command name
     */
    public void setName(final String val) {
        this.name = val;
    }

    /**
     * <p>Return the context attribute key under which the {@link Command}
     * name is stored.</p>
     * @return The context key of the Command.
     */
    public String getNameKey() {
        return this.nameKey;
    }

    /**
     * <p>Set the context attribute key under which the {@link Command}
     * name is stored.</p>
     *
     * @param val The new context attribute key
     */
    public void setNameKey(final String val) {
        this.nameKey = val;
    }

    /**
     * <p>Return <code>true</code> if locating the specified command
     * is optional.</p>
     * @return <code>true</code> if the Command is optional.
     */
    public boolean isOptional() {
        return this.optional;
    }

    /**
     * <p>Set the optional flag for finding the specified command.</p>
     *
     * @param val The new optional flag
     */
    public void setOptional(final boolean val) {
        this.optional = val;
    }

    /**
     * <p>Return <code>true</code> if this command should ignore
     * the return value from executing the looked-up command.
     * Defaults to <code>false</code>, which means that the return result
     * of executing this lookup will be whatever is returned from that
     * command.</p>
     * @return <code>true</code> if result of the looked up Command
     * should be ignored.
     *
     * @since Chain 1.1
     */
    public boolean isIgnoreExecuteResult() {
        return this.ignoreExecuteResult;
    }

    /**
     * <p>Set the rules for whether or not this class will ignore or
     * pass through the value returned from executing the looked up
     * command.</p>
     * <p>If you are looking up a chain which may be "aborted" and
     * you do not want this class to stop chain processing, then this
     * value should be set to <code>true</code></p>
     * @param val <code>true</code> if result of the
     * looked up Command should be ignored.
     *
     * @since Chain 1.1
     */
    public void setIgnoreExecuteResult(final boolean val) {
        this.ignoreExecuteResult = val;
    }

    /**
     * <p>Return <code>true</code> if this command is a Filter and
     * should ignore the return value from executing the looked-up Filter's
     * <code>postprocess()</code> method.
     * Defaults to <code>false</code>, which means that the return result
     * of executing this lookup will be whatever is returned from that
     * Filter.</p>
     * @return <code>true</code> if result of the looked up Filter's
     * <code>postprocess()</code> method should be ignored.
     *
     * @since Chain 1.1
     */
    public boolean isIgnorePostprocessResult() {
        return this.ignorePostprocessResult;
    }

    /**
     * <p>Set the rules for whether or not this class will ignore or
     * pass through the value returned from executing the looked up
     * Filter's <code>postprocess()</code> method.</p>
     * <p>If you are looking up a Filter which may be "aborted" and
     * you do not want this class to stop chain processing, then this
     * value should be set to <code>true</code></p>
     * @param val <code>true</code> if result of the
     * looked up Filter's <code>postprocess()</code> method should be ignored.
     *
     * @since Chain 1.1
     */
    public void setIgnorePostprocessResult(final boolean val) {
        this.ignorePostprocessResult = val;
    }

    // ---------------------------------------------------------- Filter Methods

    /**
     * <p>Look up the specified command, and (if found) execute it.
     * Unless <code>ignoreExecuteResult</code> is set to <code>true</code>,
     * return the result of executing the found command.  If no command
     * is found, return {@link Processing#CONTINUE}, unless the <code>optional</code>
     * property is <code>false</code>, in which case an <code>IllegalArgumentException</code>
     * will be thrown.
     * </p>
     *
     * @param context The context for this request
     * @exception IllegalArgumentException if no such {@link Command}
     *  can be found and the <code>optional</code> property is set
     *  to <code>false</code>
     * @return the result of executing the looked-up command, or
     * <code>CONTINUE</code> if no command is found or if the command
     * is found but the <code>ignoreExecuteResult</code> property of this
     * instance is <code>true</code>
     * @throws org.apache.commons.chain2.ChainException
     * if and error occurs in the looked-up Command.
     */
    @Override
    public Processing execute(final C context) {
        Command<K, V, C> command = getCommand(context);
        if (command != null) {
            Processing result = command.execute(context);
            if (isIgnoreExecuteResult()) {
                return Processing.CONTINUE;
            }
            return result;
        }
        return Processing.CONTINUE;
    }

    /**
     * <p>If the executed command was itself a {@link Filter}, call the
     * <code>postprocess()</code> method of that {@link Filter} as well.</p>
     *
     * @param context The context for this request
     * @param exception Any <code>Exception</code> thrown by command execution
     * @return the result of executing the <code>postprocess</code> method
     * of the looked-up command, unless <code>ignorePostprocessResult</code> is
     * <code>true</code>.  If no command is found, return <code>false</code>,
     * unless the <code>optional</code> property is <code>false</code>, in which
     * case <code>IllegalArgumentException</code> will be thrown.
     */
    @Override
    public boolean postprocess(final C context, final Exception exception) {
        Command<K, V, C> command = getCommand(context);
        if (command instanceof Filter) {
            boolean result = ((Filter<K, V, C>) command).postprocess(context, exception);
            return !isIgnorePostprocessResult() && result;
        }
        return false;
    }

    // --------------------------------------------------------- Private Methods

    /**
     * <p>Return the {@link Catalog} to look up the {@link Command} in.</p>
     *
     * @param context {@link Context} for this request
     * @return The catalog.
     * @exception IllegalArgumentException if no {@link Catalog}
     *  can be found
     *
     * @since Chain 1.2
     */
    protected Catalog<K, V, C> getCatalog(final C context) {
        CatalogFactory<K, V, C> lookupFactory = getCatalogFactory();
        if (lookupFactory == null) {
            lookupFactory = CatalogFactoryBase.getInstance();
        }

        String ctlg = getCatalogName();
        Catalog<K, V, C> catalog =
                ctlg == null ? lookupFactory.getCatalog() : lookupFactory.getCatalog(ctlg);
        if (catalog == null) {
            throw new IllegalArgumentException(
                    String.format("Cannot find %s", getCatalogNameString(ctlg)));
        }
        return catalog;
    }

    /**
     * <p>Return the {@link Command} instance to be delegated to.</p>
     *
     * @param context {@link Context} for this request
     * @return The looked-up Command.
     * @exception IllegalArgumentException if no such {@link Command}
     *  can be found and the <code>optional</code> property is set
     *  to <code>false</code>
     */
    protected Command<K, V, C> getCommand(final C context) {

        String cmd = getCommandName(context);
        if (cmd == null) {
            throw new IllegalArgumentException("No command name");
        }

        Catalog<K, V, C> catalog = getCatalog(context);
        Command<K, V, C> command = catalog.getCommand(cmd);
        if (command == null && !isOptional()) {
            throw new IllegalArgumentException(String.format("Cannot find command '%s' in %s",
                            cmd, getCatalogNameString(getCatalogName())));
        }
        return command;
    }

    private String getCatalogNameString(final String ctlg) {
        return ctlg == null ? "default catalog" : String.format("catalog '%s'", ctlg);
    }

    /**
     * <p>Return the name of the {@link Command} instance to be delegated to.</p>
     *
     * @param context {@link Context} for this request
     * @return The name of the {@link Command} instance
     *
     * @since Chain 1.2
     */
    protected String getCommandName(final C context) {
        String ret = getName();
        if (ret == null) {
            ret = (String) context.get(getNameKey());
        }
        return ret;
    }

}
