/*
 * 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.components;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;

import javax.faces.component.UICommand;
import javax.faces.context.FacesContext;
import javax.faces.el.MethodBinding;
import javax.faces.el.ValueBinding;

import org.apache.myfaces.orchestra.conversation.ConversationUtils;
import org.apache.myfaces.orchestra.conversation.jsf._JsfConversationUtils;
import org.apache.myfaces.orchestra.conversation.jsf.lib._EndConversationMethodBindingFacade;
import org.apache.myfaces.shared_orchestra.util.StringUtils;

/**
 * Can be used to end a manual-scope conversation, and optionally handles exceptions thrown
 * by action methods.
 * <p>
 * When nested within a UICommand component (eg a commandLink or commandButton) the specified
 * conversation will be ended after the method invoked by the parent component is executed.
 * <pre>
 * &lt;h:commandLink action="#{backing.saveAction}"&gt;
 *     &lt;orchestra:endConversation name="conversation1" onOutcome="success" /&gt;
 * &lt;/h:commandLink&gt;
 * </pre>
 * <p>
 * The "name" attribute is mandatory, and specifies which conversation is to be ended.
 * The optional attributes are:
 * <ul>
 * <li>onOutcome</li>
 * <li>errorOutcome</li>
 * </ul>
 * 
 * <h2>onOutcome</h2>
 * 
 * This is a string or comma-separated list of strings. After invoking the action
 * associated with the nearest ancestor UICommand component, the following rules
 * are executed:
 * <ul>
 *   <li>If there is no ancestor UICommand component then end the conversation, else</li>
 *   <li>If the action returned null, then 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>
 * 
 * Note in particular that when this component has no enclosing UICommand component, then
 * the specified conversation is always terminated. This is often useful on the "confirmation"
 * page of a wizard-style page sequence.
 * 
 * <h2>errorOutcome</h2>
 * 
 * In case of an exception being thrown by the action method, use the given outcome as the
 * new outcome so normal navigation to a specified page can occur instead of showing the
 * default errorPage. This value is checked against the onOutcome list to determine whether
 * the specified conversation should be terminated when an exception occurs. If an exception
 * occurs, but no errorOutcome is specified then the conversation is never terminated.
 */
public class UIEndConversation extends AbstractConversationComponent
{
    public static final String COMPONENT_TYPE = "org.apache.myfaces.orchestra.EndConversation";

    private String onOutcome;
    private String errorOutcome;

    private boolean inited = false;

    public void encodeBegin(FacesContext context) throws IOException
    {
        super.encodeBegin(context);

        UICommand command = _JsfConversationUtils.findParentCommand(this);
        if (command != null)
        {
            // This component has a UICommand ancestor. Replace its "action" MethodBinding 
            // with a proxy.
            if (!inited)
            {
                MethodBinding original = command.getAction();
                command.setAction(new _EndConversationMethodBindingFacade(
                    getName(),
                    getOnOutcomes(),
                    original,
                    getErrorOutcome()));
                inited = true;
            }
        }
        else
        {
            // This component has no UICommand ancestor. Always end the conversation.
            ConversationUtils.invalidateIfExists(getName());
        }
    }

    private Collection getOnOutcomes()
    {
        String onOutcome = getOnOutcome();
        if (onOutcome == null || onOutcome.trim().length() < 1)
        {
            return null;
        }

        return Arrays.asList(StringUtils.trim(StringUtils.splitShortString(onOutcome, ',')));
    }

    public void restoreState(FacesContext context, Object state)
    {
        Object[] states = (Object[]) state;
        super.restoreState(context, states[0]);
        inited = ((Boolean) states[1]).booleanValue();
        onOutcome = (String) states[2];
        errorOutcome = (String) states[3];
    }

    public Object saveState(FacesContext context)
    {
        return new Object[]
            {
                super.saveState(context),
                inited ? Boolean.TRUE : Boolean.FALSE,
                onOutcome,
                errorOutcome
            };
    }

    public String getOnOutcome()
    {
        if (onOutcome != null)
        {
            return onOutcome;
        }
        ValueBinding vb = getValueBinding("onOutcome");
        if (vb == null)
        {
            return null;
        }
        return (String) vb.getValue(getFacesContext());
    }

    public void setOnOutcome(String onOutcome)
    {
        this.onOutcome = onOutcome;
    }

    public String getErrorOutcome()
    {
        if (errorOutcome != null)
        {
            return errorOutcome;
        }
        ValueBinding vb = getValueBinding("errorOutcome");
        if (vb == null)
        {
            return null;
        }
        return (String) vb.getValue(getFacesContext());
    }

    public void setErrorOutcome(String errorOutcome)
    {
        this.errorOutcome = errorOutcome;
    }
}
