/*
 * 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.myfaces.orchestra.conversation.jsf.lib;

import java.util.Collection;

import javax.faces.FacesException;
import javax.faces.component.StateHolder;
import javax.faces.component.UIComponentBase;
import javax.faces.context.FacesContext;
import javax.faces.el.EvaluationException;
import javax.faces.el.MethodBinding;
import javax.faces.el.MethodNotFoundException;

import org.apache.myfaces.orchestra.conversation.ConversationManager;
import org.apache.myfaces.orchestra.conversation.ConversationUtils;

/**
 * A facade for the original method binding to deal with end conversation conditions.
 * <p>
 * This class implements MethodBinding, ie represents an EL expression string that specifies
 * a method to call. It is expected to be used when invoking action methods when the current
 * conversation should be closed upon certain results of the action.
 * <p>
 * This facade also enhances error-handling for action methods. If the invoked method throws
 * an exception of any kind, and an errorOutcome value has been specified then the errorOutcome
 * is returned instead of allowing the exception to propagate. The exception that occurred is
 * reported to the ConversationMessager object associated with the conversation, so it can
 * choose whether and how to present the error to the user.
 */
public class _EndConversationMethodBindingFacade extends MethodBinding implements StateHolder
{
    private MethodBinding original;
    private String conversationName;
    private Collection onOutcomes;
    private String errorOutcome;

    private boolean _transient = false;

    public _EndConversationMethodBindingFacade()
    {
    }

    /**
     * Constructor.
     *
     * @param conversation is the name of the conversation to conditionally be closed.
     *
     * @param onOutcomes is a collection of navigation strings that may be returned from the
     * invoked method. One of the following rules is then used to determine whether the conversation
     * is ended or not:
     * <ul>
     * <li>If there was no action to invoke, end the conversation, else</li>
     * <li>If the action returned null, do not end the conversation, else</li>
     * <li>If the onOutcomes list is null or empty then end the conversation, else</li>
     * <li>If the returned value is in the onOutcomes list, then end the conversation, else</li>
     * <li>do not end the conversation.</li>
     * </ul>
     *
     * @param original is the EL expression to be invoked.
     *
     * @param errorOutcome is a JSF navigation string to be returned if the action method
     * throws an exception of any kind. This navigation value is checked against the onOutcomes
     * values just as if the action method had actually returned this value. When not specified,
     * then on exception the current conversation is not ended.
     */
    public _EndConversationMethodBindingFacade(
        String conversation,
        Collection onOutcomes,
        MethodBinding original,
        String errorOutcome)
    {
        this.original = original;
        this.conversationName = conversation;
        this.onOutcomes = onOutcomes;
        this.errorOutcome = errorOutcome;
    }

    public String getConversationName()
    {
        return conversationName;
    }

    public String getExpressionString()
    {
        if (original == null)
        {
            return null;
        }
        return original.getExpressionString();
    }

    public Class getType(FacesContext context) throws MethodNotFoundException
    {
        if (original == null)
        {
            return null;
        }
        return original.getType(context);
    }

    public Object invoke(FacesContext context, Object[] values) throws EvaluationException, MethodNotFoundException
    {
        Object returnValue = null;
        //noinspection CatchGenericClass
        try
        {
            if (original != null)
            {
                returnValue = original.invoke(context, values);
            }
        }
        catch (Throwable t)
        {
            ConversationManager conversationManager = ConversationManager.getInstance();

            if (errorOutcome != null)
            {
                // Suppress the exception, and act as if errorOutcome had been returned. 

                conversationManager.getMessager().setConversationException(t);
                returnValue = errorOutcome;
            }
            else
            {
                // When no errorOutcomes are specified, then there is nothing to check against
                // the onOutcomes list. 
                //
                // Note that in this case the conversation is NEVER ended. It is debatable what
                // the correct behaviour should be here. If this action wrapper was not present
                // then the conversation would not be terminated, so an instance with no
                // errorOutcomes specified is consistent with that. In any case, the user can
                // easily get the alternate behaviour by simply specifying an errorOutcome and
                // adding that to the onOutcomes list.

                returnValue = null; // do not end conversation
                throw new FacesException(t);
            }
        }
        finally
        {
            boolean endConversation;
            if (original == null)
            {
                endConversation = true;
            }
            else if (returnValue == null)
            {
                endConversation = false;
            }
            else if (onOutcomes == null || onOutcomes.isEmpty())
            {
                endConversation = true;
            }
            else
            {
                endConversation = onOutcomes.contains(returnValue);
            }

            if (endConversation)
            {
                ConversationUtils.invalidateIfExists(conversationName);
            }
        }
        return returnValue;
    }

    /** Required by StateHolder interface. */
    public void setTransient(boolean newTransientValue)
    {
        _transient = newTransientValue;
    }

    /** Required by StateHolder interface. */
    public boolean isTransient()
    {
        return _transient;
    }

    public void restoreState(FacesContext context, Object states)
    {
        Object[] state = (Object[]) states;

        original = (MethodBinding) UIComponentBase.restoreAttachedState(context, state[0]);
        conversationName = (String) state[1];
        onOutcomes = (Collection) state[2];
        errorOutcome = (String) state[3];
    }

    public Object saveState(FacesContext context)
    {
        return new Object[]
            {
                UIComponentBase.saveAttachedState(context, original),
                conversationName,
                onOutcomes,
                errorOutcome
            };
    }
}
