/*
 * 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.
 *
 * $Header:$
 */
package org.apache.beehive.netui.tags.html;

import org.apache.beehive.netui.util.internal.InternalStringBuilder;

import org.apache.beehive.netui.pageflow.ProcessPopulate;
import org.apache.beehive.netui.pageflow.RequestParameterHandler;
import org.apache.beehive.netui.script.common.DataAccessProviderStack;
import org.apache.beehive.netui.script.common.IDataAccessProvider;
import org.apache.beehive.netui.tags.ByRef;
import org.apache.beehive.netui.tags.naming.FormDataNameInterceptor;
import org.apache.beehive.netui.tags.naming.IndexedNameInterceptor;
import org.apache.beehive.netui.tags.naming.PrefixNameInterceptor;
import org.apache.beehive.netui.tags.rendering.*;
import org.apache.beehive.netui.util.Bundle;
import org.apache.beehive.netui.util.iterator.ArrayIterator;
import org.apache.beehive.netui.util.iterator.IteratorFactory;
import org.apache.beehive.netui.util.logging.Logger;

import javax.servlet.ServletRequest;
import javax.servlet.jsp.JspException;
import java.util.*;


/**
 * Renders a select containing a set of SelectOptions.
 *
 * Select binds to an Iterator of Strings.
 *
 * If Select uses any Format tags, it must have those tags come before any nested
 * SelectOption tags.
 * @jsptagref.tagdescription Renders an HTML &lt;select> tag containing a set of selectable options.
 *
 * <p>The &lt;netui:select> tag can generate a set of
 * selectable options in two ways:
 *
 * <blockquote>
 * <ol>
 * <li>they can be dynamically generated by pointing the
 * &lt;netui:select> tag at a String[] object or
 * {@link java.util.HashMap java.util.HashMap}</li>
 * <li>they can be statically generated by providing a set of children
 * {@link SelectOption}
 * tags</li>
 * </ol>
 * </blockquote>
 *
 * <p><b>Dynamically Generated Options</b>
 *
 * <p>You can dynamically generate a set of selectable options by
 * pointing the &lt;netui:select> tag at a String[].
 *
 * <pre>    public String[] colors = {"red", "green", "blue", "orange", "pink", "aqua", "black", "brown", "tan"};
 *
 *    public String[] getColors()
 *    {
 *        return colors;
 *    }</pre>
 *
 * <p>To point the &lt;netui:select> tag at the String[] object use the
 * <code>optionsDataSource</code> attribute.</p>
 *
 * <pre>    &lt;netui:select dataSource="actionForm.selection"
 *                  optionsDataSource="${pageFlow.colors}"/></pre>
 *
 * Note that you can make the display value and the submitted value differ by pointing the
 * optionsDataSource attribute of the &lt;netui:select> tag at a HashMap object.
 * (Any object that implements the {@link java.util.Map java.util.Map} interface will work.)
 *
 * <pre>    public HashMap optionsMap = new HashMap();
 *
 *    protected HashMap getOptionsMap()
 *    {
 *        return optionsMap;
 *    }
 *
 *    protected void onCreate()
 *    {
 *        optionsMap.put("#ff3333", "red");
 *        optionsMap.put("#3333ff", "blue");
 *        optionsMap.put("#33ff33", "green");
 *    }</pre>
 *
 * <p>However, you cannot use a Map object if you choose to use the Select as a repeater
 * (setting the attribute repeater="true").</p>
 *
 * <p>Point the &lt;netui:select> at the Map object using the <code>optionsDataSource</code> attribute.</p>
 *
 * <pre>    &lt;netui:select dataSource="actionForm.selection"
 *                  optionsDataSource="${pageFlow.optionsMap}"/></pre>
 *
 * The following HTML will be generated.
 *
 * <pre>    &lt;select name="wlw-select_key:{actionForm.selection}">
 *        &lt;option value="#3333ff">blue&lt;/option>
 *        &lt;option value="#33ff33">green&lt;/option>
 *        &lt;option value="#ff3333">red&lt;/option>
 *    &lt;/select></pre>
 *
 * <p><b>Statically Generated Options</b></p>
 *
 * <p>To statically generate selecable options, place a set of &lt;netui:selectOption> tags inside
 * the &lt;netui:select> tag.
 *
 * <pre>    &lt;netui:select dataSource="actionForm.selection" size="5">
 *        &lt;netui:selectOption value="red" />
 *        &lt;netui:selectOption value="blue" />
 *        &lt;netui:selectOption value="green" />
 *        &lt;netui:selectOption value="yellow" />
 *        &lt;netui:selectOption value="orange" />
 *    &lt;/netui:select></pre>
 *
 * <p><b>Submitting Selections</b></p>
 *
 * <p>A &lt;netui:select> is submitted as a String or String[] object, depending on whether the
 * <code>multiple</code> attribute is set to true.  In the following example, the <code>dataSource</code>
 * attribute points at a String[] object.</p>
 *
 * <pre>    &lt;/netui:select dataSource="actionForm.selections"...</pre>
 *
 * <p>In this case, the &lt;netui:select> tag submits to a String[] field of a Form Bean.</p>
 *
 * <pre>    public static class SubmitForm extends FormData
 *    {
 *        private String[] selections;
 *
 *        public void setSelections(String[] selections)
 *        {
 *            this.selections = selections;
 *        }
 *
 *        public String[] getSelections()
 *        {
 *            return this.selections;
 *        }
 *    }</pre>
 *
 * <p><b>Use Select as a Repeater with Multiple Repeating Types</b></p>
 *
 * <p>Optionally, use the &lt;netui:select> tag as a repeater to render multiple options
 * from the <code>dataSource</code> and <code>defaultValue</code> attributes as well as
 * the <code>optionsDataSource</code>. The &lt;netui:select> element can dynamically generate
 * option elements for different repeating types of "option", "dataSource", "default",
 * (optionsDataSource, dataSource, and defaultValue attributes respectively) and "null".
 * The Select <code>repeatingOrder</code> attribute sets the order that repeating types
 * are generated. The <code>repeatingType</code> attribute on the &lt;netui:selectOption>
 * tag identifies each of the types to be rendered.</p>
 *
 * <p>Use JSTL boolean conditional tags with the &lt;netui:selectOption> elements
 * to help manage repeaters of different data types.
 * For example, the <code>dataSource</code> could point to a String[] while
 * the <code>optionsDataSource</code> points to an Object[] where each object has
 * name and value fields...</p>
 *
 * <pre>    &lt;netui:select dataSource="actionForm.selections"
 *                  optionsDataSource="${pageFlow.options}"
 *                  repeatingOrder="dataSource,option"
 *                  repeater="true" multiple="true">
 *        &lt;c:if test="${container.metadata.dataSourceStage}">
 *            &lt;netui:selectOption  repeatingType="dataSource" value="${container.item}">
 *                &lt;netui:span value="${container.item}" />
 *            &lt;/netui:selectOption>
 *        &lt;/c:if>
 *        &lt;c:if test="${container.metadata.optionStage}">
 *            &lt;netui:selectOption  repeatingType="option" value="${container.item.name}">
 *                &lt;netui:span value="${container.item.value}" />
 *            &lt;/netui:selectOption>
 *        &lt;/c:if>
 *    &lt;/netui:select>
 * </pre>
 * @example The following sample uses the <code>optionsDataSource</code> attribute to reference a
 * dynamically generated dropdown list.
 *
 * <pre>
 *    &lt;netui:select dataSource="actionForm.selectedOption"
 *                  optionsDataSource="${actionForm.itemOptions}" />
 * </pre>
 *
 * <p>Assume that the <code>optionsDataSource</code> attribute refers to
 * a <code>java.util.Map</code> object.
 * The Map object will be rendered as a series
 * of &lt;option> tags. HTML that is similar to the following will be
 * rendered in the browser:</p>
 *
 * <pre>    &lt;select name="wlw-select_key:{actionForm.itemOptions}">
 *        &lt;option value="633">Aurora Bridge&lt;/option>
 *        &lt;option value="631">FA-18 fighter jet&lt;/option>
 *        &lt;option value="635">Space Needle&lt;/option>
 *        &lt;option value="642">Thin Mints&lt;/option>
 * 	      ...
 *    &lt;/select></pre>
 * @netui:tag name="select" description="Defines a multiple-choice menu or drop-down list within a netui:form element."
 * @netui:attribute name="onSelect" hide="true" description=""
 */
public class Select extends HtmlOptionsDataSourceTag
        implements IDataAccessProvider, IFormattable
{
    // @todo: needs to create DRT tests for: verification of errors, verirication of data sources matching,
    // @todo: verification of formating inside a repeater.
    // @todo: on the null tag, we need to default the null value to NULL_VALUE
    // @todo: need to handle null, in the options
    // @todo: should handle no optionDataSource when repeating...

    private static final Logger logger = Logger.getInstance(Select.class);

    private SelectTag.State _state = new SelectTag.State();
    private OptionTag.State _optionState = new OptionTag.State();
    private InputHiddenTag.State _hiddenState = new InputHiddenTag.State();
    private boolean _formatterError = false;

    private static Object[] NULL_INSTANCE = {null};


    /**
     * This enum defines stages through the possible option values.
     */
    public static class RepeatingStages
    {
        private static final int INT_BEFORE = 0;
        private static final int INT_OPTION = 1;
        private static final int INT_DEFAULT = 2;
        private static final int INT_DATASOURCE = 3;
        private static final int INT_NULL = 4;
        private static final int INT_DONE = 5;

        static final RepeatingStages BEFORE = new RepeatingStages(INT_BEFORE);
        static final RepeatingStages OPTION = new RepeatingStages(INT_OPTION);
        static final RepeatingStages DEFAULT = new RepeatingStages(INT_DEFAULT);
        static final RepeatingStages DATASOURCE = new RepeatingStages(INT_DATASOURCE);
        static final RepeatingStages NULL = new RepeatingStages(INT_NULL);
        static final RepeatingStages DONE = new RepeatingStages(INT_DONE);
        
        /**
         * These are the publically exposed stages, <code>REPEATING_OPTION, REPEATING_DEFAULT,
         * REPEATING_DATASOURCE and REPEATING_NULL</code>.
         */
        public static final String REPEATING_OPTION = "option";
        public static final String REPEATING_DEFAULT = "default";
        public static final String REPEATING_DATASOURCE = "dataSource";
        public static final String REPEATING_NULL = "null";

        public int value;

        // prevent construction...
        private RepeatingStages(int val)
        {
            value = val;
        }
        
        int getValue()
        {
            return value;
        }

        /**
         * Returns the String value that can be used to order the selection.
         * @return The String Value.
         */
        public String toString()
        {
            switch (value) {
                case INT_OPTION:
                    return REPEATING_OPTION;
                case INT_DEFAULT:
                    return REPEATING_DEFAULT;
                case INT_DATASOURCE:
                    return REPEATING_DATASOURCE;
                case INT_NULL:
                    return REPEATING_NULL;
                default:
                    return "Unknown Stage";
            }
        }

        /**
         * Given a String value defined above, return the enum value for it.
         * @param value a String value matching one of the public Strings defined for the class.
         * @return the matching RepeatingStages or null.
         */
        public static RepeatingStages parseString(String value)
        {
            if (REPEATING_OPTION.equals(value))
                return OPTION;
            if (REPEATING_DEFAULT.equals(value))
                return DEFAULT;
            if (REPEATING_DATASOURCE.equals(value))
                return DATASOURCE;
            if (REPEATING_NULL.equals(value))
                return NULL;
            return null;
        }
    }

    /**
     * This defines the default order of processing the options when repeating.
     */
    private static final RepeatingStages[] DEFAULT_ORDER = {RepeatingStages.BEFORE,
                                                            RepeatingStages.OPTION,
                                                            RepeatingStages.DATASOURCE,
                                                            RepeatingStages.DEFAULT,
                                                            RepeatingStages.NULL};

    /**
     * Default value of the options <code>value</code> attribute.
     */
    public static final String NULL_VALUE = "netui_null";

    /**
     * Constant value of the <code>repeatingType</code> attribute for options handling the <code>null</code> option.
     */
    //public static final String REPEATING_NULL = "Null";

    private static final String SELECT_KEY = "select_key";
    private static final String OLDVALUE_SUFFIX = "OldValue";

    // IDataAccessProvider support
    private int _repIdx = 0;            // The current index for repeating over the optionsDataSource
    private RepeatingStages _repCurStage = RepeatingStages.BEFORE; // The current stage defined by the stage constants above
    private boolean _repeater;          // Boolean flag indicating if this is a repeater or not
    private Object _repCurItem;         // The current item access by the IDataAccessProvider
    private Iterator _repeaterIterator; // The iterator being used to output the options.
    private RepeatingStages[] _order = DEFAULT_ORDER;

    private Object _dynamicOptions;     // The interator (or map) for the options data source, repeating this is current var

    private String _saveBody;
    private String _nullableOptionText;

    private List _defaultSelections;
    private ArrayList _formatters;
    private ArrayList _optionList;
    private String[] _match;             // The actual values we will match against
    private boolean _nullable;
    private TagRenderingBase _optRb;

    private static final List _internalNamingChain;

    static
    {
        List l = new ArrayList(3);
        l.add(new FormDataNameInterceptor());
        l.add(new IndexedNameInterceptor());
        l.add(new PrefixNameInterceptor(SELECT_KEY));
        _internalNamingChain = Collections.unmodifiableList(l);

        org.apache.beehive.netui.pageflow.ProcessPopulate.registerPrefixHandler(SELECT_KEY, new SelectPrefixHandler());
    }

    /**
     */
    public static class SelectPrefixHandler
            implements RequestParameterHandler
    {
        public void process(javax.servlet.http.HttpServletRequest request, String key,
                            String expr, ProcessPopulate.ExpressionUpdateNode node)
        {
            String[] returnArray = null;

            if (!key.endsWith(OLDVALUE_SUFFIX)) {
                //This select has values and should stay that way
                returnArray = request.getParameterValues(key);
            }
            else {
                //Check the request to see if select also exists
                String newKey = key.substring(0, key.indexOf(OLDVALUE_SUFFIX));
                String[] select = request.getParameterValues(newKey);
                if (select != null) {
                    returnArray = select;
                }
                else {
                    returnArray = new String[0]; //null;
                }
            }

            if (node.expression.endsWith(OLDVALUE_SUFFIX)) {
                node.expression = node.expression.substring(0, node.expression.indexOf(OLDVALUE_SUFFIX));
            }

            //Check for the NULL_VALUE, replace it with null
            for (int i = 0; i < returnArray.length; i++) {
                if (returnArray[i].equals(NULL_VALUE)) {
                    returnArray[i] = null;
                }
            }

            node.values = returnArray;

            if (logger.isDebugEnabled()) {
                logger.debug("\n*********************************************\n" +
                        "process with key \"" + key + "\" and expression \"" + node.expression + "\"" + "and result size: "
                        + (returnArray != null ? "" + returnArray.length : null) + "\n" +
                        "*********************************************\n");
            }
        }
    }

    public Select()
    {
        super();
    }

    /**
     * Return the name of the Tag.
     */
    public String getTagName()
    {
        return "Select";
    }

    public String getDataSource()
    {
        return "{" + _dataSource.toString() + "}";
    }

    /**
     * This method will return the state associated with the tag.  This is used by this
     * base class to access the individual state objects created by the tags.
     * @return a subclass of the <code>AbstractHtmlState</code> class.
     */
    protected AbstractHtmlState getState()
    {
        return _state;
    }

    /**
     * Return an <code>ArrayList</code> which represents a chain of <code>INameInterceptor</code>
     * objects.  This method by default returns <code>null</code> and should be overridden
     * by objects that support naming.
     * @return an <code>ArrayList</code> that will contain <code>INameInterceptor</code> objects.
     */
    protected List getNamingChain()
    {
        return _internalNamingChain;
    }

    /**
     * Evaluate the defaultValues
     */
    protected Object evaluateDefaultValue()
            throws JspException
    {
        Object val = _defaultValue;

        List defaults = null;
        if (val instanceof String) {
            defaults = new ArrayList();
            defaults.add(val);
        }
        else {
            Iterator optionsIterator = null;
            optionsIterator = IteratorFactory.createIterator(val);

            // default value is optional so only warn
            if (optionsIterator == null && _defaultValue != null)
                logger.warn(Bundle.getString("Tags_IteratorError",
                        new Object[]{getTagName(), "defaultValue", _defaultValue}));

            if (optionsIterator == null)
                optionsIterator = IteratorFactory.EMPTY_ITERATOR;

            defaults = new ArrayList();
            while (optionsIterator.hasNext()) {
                Object o = optionsIterator.next();
                defaults.add(o.toString());
            }
        }

        return defaults;
    }

    /**
     * Set whether multiple selections are allowed.
     * @param multiple the multiple value ("true" or "false")
     * @jsptagref.attributedescription Boolean. Whether or not multi-selection is enabled.
     * If multiple selection is enabled, a null option will not be displayed, even if
     * the <code>nullable</code> is set to true.
     * @jsptagref.databindable false
     * @jsptagref.attributesyntaxvalue <i>boolean_multipleSelectEnabled</i>
     * @netui:attribute required="false" rtexprvalue="true" type="boolean"
     * description="Whether or not multi-selection is enabled.
     * If multiple selection is enabled, a null option will not be displayed, even if
     * the nullable is set to true."
     */
    public void setMultiple(boolean multiple)
    {
        _state.multiple = multiple;
    }

    /**
     * Set whether repeating of contained options is on.
     * @param repeater the repeater value ("true" or "false")
     * @jsptagref.attributedescription Set whether repeating of contained options is on.
     * @jsptagref.databindable false
     * @jsptagref.attributesyntaxvalue <i>boolean_repeater</i>
     * @netui:attribute required="false" rtexprvalue="true" type="boolean"
     * description="Set whether repeating of contained options is on."
     */
    public void setRepeater(boolean repeater)
    {
        _repeater = repeater;
    }

    /**
     * Gets whether a repeating contained options is on.
     * @return the repeater value
     */
    public boolean isRepeater()
    {
        return _repeater;
    }

    /**
     * This method will set the order of the options generated in the select.  It must contain a
     * comma separated string listing the order or the stages that the repeating types are processed.
     * These values are "option", "dataSource", "default", and "null".
     * @param order comma separated ordering of items when there is a repeating select.
     * @jsptagref.attributedescription Define the order of options generated for a repeating Select.
     * It must contain a comma separated string listing the order or the stages that the repeating types
     * are processed. These values are "option", "dataSource", "default", and "null". For example,
     * <pre>    repeatingOrder="dataSource,option"</pre>
     *
     * Then a &lt;netui:selectOption> element could set the repeatingType attribute to "dataSource"
     * while another is defined for "option".
     * @jsptagref.databindable false
     * @jsptagref.attributesyntaxvalue <i>string_order</i>
     * @netui:attribute required="false" rtexprvalue="true"
     * description="Define the order of options for a repeating Select"
     */
    public void setRepeatingOrder(String order)
            throws JspException
    {
        String[] options = order.split(",");
        RepeatingStages[] stageOrder = new RepeatingStages[options.length + 1];
        stageOrder[0] = RepeatingStages.BEFORE;
        for (int i = 0; i < options.length; i++) {
            String opt = options[i].trim();
            stageOrder[i + 1] = RepeatingStages.parseString(opt);
            if (stageOrder[i + 1] == null) {
                String s = Bundle.getString("Tags_SelectBadRepeatingStage", new Object[]{opt});
                registerTagError(s, null);
            }
        }
        _order = stageOrder;
    }

    /**
     * Set whether a null option is desired.
     * @param nullable the nullable value
     * @jsptagref.attributedescription Boolean.
     * Whether a option with the value null should be added to the bottom of the list.
     * If &lt;select> has the multiple <code>attribute</code> set to true, the null option won't be shown.
     * @jsptagref.databindable false
     * @jsptagref.attributesyntaxvalue <i>boolean_nullable</i>
     * @netui:attribute required="false"  rtexprvalue="true" type="boolean"
     * description="Whether a option with the value null should be added to the bottom of the list.
     * If <select> has the multiple attribute set to true, the null option won't be shown."
     */
    public void setNullable(boolean nullable)
    {
        _nullable = nullable;
    }

    /**
     * Gets the options datasource value (an expression).
     * @return the options datasource
     */
    public Object getOptionsDataSource()
    {
        return _optionsDataSource;
    }

    /**
     * Set the text of the nullable option.
     * If the <code>nullable<code> option is true, this is
     * the text of that option. The default is "";
     * @jsptagref.attributedescription Boolean.
     * If the <code>nullable</code> attribute is set to true, then the <code>nullableOptionText</code>
     * attribute determines the display text of the null option.
     * The default is to use the empty string, "", as the display text.
     * @jsptagref.databindable false
     * @jsptagref.attributesyntaxvalue <i>boolean_nullableOptionText</i>
     * @netui:attribute required="false" rtexprvalue="true" type="boolean"
     * description="If the nullable attribute is set to true, then the nullableOptionText
     * attribute determines the display text of the null option.
     */
    public void setNullableOptionText(String nullableOptionText)
    {
        _nullableOptionText = nullableOptionText;
    }

    /**
     * This method will return the object representing the <code>optionsDataSource</code>.  This
     * is overridden from the base class, because there are only two types which will be
     * retunred from the method.  The <code>optionsDataSource</code> will either be a instance of a <code>Map</code>
     * or and instanceof a <code>Iterator</code>.
     * @return the object instance object representing the objectsDataSource.  This may be null.
     * @throws JspException on an error
     */
    protected Object evaluateOptionsDataSource()
            throws JspException
    {
        Object val = _optionsDataSource;
        if (val == null) {
            // optionsDataSource is option so this is a warning
            if (_optionsDataSource != null)
                logger.warn(Bundle.getString("Tags_IteratorError",
                        new Object[]{getTagName(), "optionsDataSource", _optionsDataSource}));
            return null;
        }

        if (val instanceof Map)
            return val;

        Iterator options = null;
        options = IteratorFactory.createIterator(val);
        if (options == null)
            options = IteratorFactory.EMPTY_ITERATOR;

        return options;
    }

    /**
     * Sets how many options are displayed.
     * @param size the size (a number)
     * @jsptagref.attributedescription The number of visible options
     * @jsptagref.databindable false
     * @jsptagref.attributesyntaxvalue <i>integer_size</i>
     * @netui:attribute required="false" rtexprvalue="true" type="int"
     * description="The number of visible options"
     */
    public void setSize(int size)
    {
        _state.size = size;
    }

    /**
     * Does the specified value match one of those we are looking for?
     * @param value Value to be compared
     */
    public boolean isMatched(String value)
    {
        if (value == null)
            return false;
        if ((_match != null)) {
            for (int i = 0; i < _match.length; i++) {
                if (value.equals(_match[i]))
                    return true;
            }
        }
        else {
            if (_defaultSelections != null) {
                return (_defaultSelections.contains(value));
            }
        }

        return false;

    }

    //********************************** IDataAccessProvider Interface  ******************************
    // setDataSource is implemented by the HtmlDataSourceTag class
    // getDataSource is implemented by the HtmlDataSourceTag class

    /**
     * Get the current index in this iteration.  This should be a
     * zero based integer that increments after each iteration.
     * @return the current index of iteration or 0
     */
    public int getCurrentIndex()
    {
        return _repIdx;
    }

    /**
     * Get the current data item in this IDataAccessProvider.
     * @return the current data item or <code>null</code>
     */
    public Object getCurrentItem()
    {
        return _repCurItem;
    }

    /**
     * Get a metadata object for the current item.  This interface
     * is optional, and implementations of this interface are
     * provided by the IDataAccessProvider interface.  See these
     * implementations for information about their support for
     * current item metadata.
     * @return the current metadata or <code>null</code> if no metadata can be
     *         found or metadata is not supported by a IDataAccessProvider implementation
     */
    public Object getCurrentMetadata()
    {
        return this;
    }

    /**
     * Get the parent IDataAccessProvider of a IDataAccessProvider.  A IDataAccessProvider
     * implementation may be able to nest IDataAccessProviders.  In this case,
     * it can be useful to be able to also nest access to data from parent
     * providers.  Implementations of this interface are left with having
     * to discover and export parents.  The return value from this call
     * on an implementing Object can be <code>null</code>.
     * @return the parent IDataAccessProvider or <code>null</code> if this method
     *         is not supported or the parent can not be found.
     */
    public IDataAccessProvider getProviderParent()
    {
        return (IDataAccessProvider) findAncestorWithClass(this, IDataAccessProvider.class);
    }

    /**
     * Return the enum value of the currently repeating stage.
     * @return The currently repeating stage.
     */
    public RepeatingStages getRepeatingStage()
    {
        return _repCurStage;
    }

    /**
     * Boolean indicating that we are processing the optionsDataSource.
     * @return <code>true</code> if we are processing the optionsDataSource.
     */
    public boolean isOptionStage()
    {
        return _repCurStage == RepeatingStages.OPTION;
    }

    /**
     * Boolean indicating that we are processing the defaultValue.
     * @return <code>true</code> if we are processing the defaultValue.
     */
    public boolean isDefaultStage()
    {
        return _repCurStage == RepeatingStages.DEFAULT;
    }

    /**
     * Boolean indicating that we are processing the dataSource.
     * @return <code>true</code> if we are processing the dataSource.
     */
    public boolean isDataSourceStage()
    {
        return _repCurStage == RepeatingStages.DATASOURCE;
    }

    /**
     * Boolean indicating that we are processing the defined null value.
     * @return <code>true</code> if we are processing the defined null value.
     */
    public boolean isNullStage()
    {
        return _repCurStage == RepeatingStages.NULL;
    }

    /**
     * Render the beginning of this select.
     * @throws JspException if a JSP exception has occurred
     */
    public int doStartTag() throws JspException
    {
        Object val = evaluateDataSource();
        _defaultSelections = (List) evaluateDefaultValue();

        // if there were expression errors report them
        if (hasErrors())
            return SKIP_BODY;

        buildMatch(val);
        if (hasErrors())
            return SKIP_BODY;


        _formatters = new ArrayList();
        _optionList = new ArrayList();

        // Walk the options data source
        _dynamicOptions = evaluateOptionsDataSource();
        if (_repeater) {
            _repCurStage = _order[0];
            boolean valid = doRepeaterAfterBody();
            if (!valid)
                return SKIP_BODY;
            DataAccessProviderStack.addDataAccessProvider(this, pageContext);
        }

        // Continue processing this page
        return EVAL_BODY_BUFFERED;
    }

    /**
     * Save any body content of this tag, which will generally be the
     * option(s) representing the values displayed to the user.
     * @throws JspException if a JSP exception has occurred
     */
    public int doAfterBody() throws JspException
    {
        if (hasErrors()) {
            return SKIP_BODY;
        }

        // if this is a repeater we need to repeater over the body...
        if (_repeater) {
            if (doRepeaterAfterBody())
                return EVAL_BODY_AGAIN;
        }

        if (bodyContent != null) {
            String value = bodyContent.getString();
            bodyContent.clearBody();
            if (value == null)
                value = "";
            _saveBody = value.trim();
        }
        return SKIP_BODY;
    }

    /**
     * Render the end of this select.
     * @throws JspException if a JSP exception has occurred
     */
    public int doEndTag() throws JspException
    {
        ServletRequest req = pageContext.getRequest();

        String fmtErrors = null;
        if (_formatterError) {
            fmtErrors = getErrorsFromBody();
        }
        if (hasErrors())
            return reportAndExit(EVAL_PAGE);

        _state.disabled = isDisabled();

        //Create hidden field for state tracking
        ByRef ref = new ByRef();
        nameHtmlControl(_state, ref);

        if (hasErrors())
            return reportAndExit(EVAL_PAGE);

        // Only write out the hidden field if the select is not
        // disabled.  If it is disabled, then nothing will be posted
        // back from this.
        WriteRenderAppender writer = new WriteRenderAppender(pageContext);
        if (!_state.disabled) {
            _hiddenState.clear();
            String hiddenParamName = null;
            hiddenParamName = _state.name + OLDVALUE_SUFFIX;
            _hiddenState.name = hiddenParamName;
            _hiddenState.value = "true";

            TagRenderingBase hiddenTag = TagRenderingBase.Factory.getRendering(TagRenderingBase.INPUT_HIDDEN_TAG, req);
            hiddenTag.doStartTag(writer, _hiddenState);
            hiddenTag.doEndTag(writer);
            write("\n");
        }

        // Render any formatting errors that may have occurred.
        if (fmtErrors != null)
            write(fmtErrors);


        TagRenderingBase br = TagRenderingBase.Factory.getRendering(TagRenderingBase.SELECT_TAG, req);
        br.doStartTag(writer, _state);

        // Render the content of the body, these would be the options
        if (_saveBody != null) {
            write(_saveBody);
        }

        // if we are repeating then the body contained the options so we can exit here
        if (_repeater) {

            if (hasErrors())
                return reportAndExit(EVAL_PAGE);

            br.doEndTag(writer);
            if (!ref.isNull())
                write((String) ref.getRef());

            // Continue processing this page
            localRelease();
            return EVAL_PAGE;
        }

        // All of the code below will pass through the optionsDataSource, the dataSource and defaultValue and
        // create a full Select.
        if (_dynamicOptions != null) {
            if (_dynamicOptions instanceof Map) {
                Map dynamicOptionsMap = (Map) _dynamicOptions;
                Iterator keyIterator = dynamicOptionsMap.keySet().iterator();
                while (keyIterator.hasNext()) {
                    Object optionValue = keyIterator.next();
                    String optionDisplay = null;
                    if (dynamicOptionsMap.get(optionValue) != null) {
                        optionDisplay = dynamicOptionsMap.get(optionValue).toString();
                    }

                    if (optionValue != null) {
                        addOption(req, optionValue.toString(), optionDisplay);
                    }
                }
            }
            else if (_dynamicOptions instanceof Iterator) {
                Iterator dynamicOptionsIterator = (Iterator) evaluateOptionsDataSource();
                while (dynamicOptionsIterator.hasNext()) {
                    Object o = dynamicOptionsIterator.next();
                    if (o != null) {
                        String optionValue = o.toString();
                        addOption(req, optionValue, optionValue);
                    }
                }
            }
        }

        // add the value from the DataSource and Default value
        addDatasourceIfNeeded(req);
        addDefaultsIfNeeded(req);
        if (_nullable && !isMultiple()) {
            String txt = (_nullableOptionText != null) ? _nullableOptionText : "";
            addOption(req, NULL_VALUE, txt);
        }

        br.doEndTag(writer);
        if (!ref.isNull())
            write((String) ref.getRef());

        // Continue processing this page
        localRelease();
        return EVAL_PAGE;
    }

    /**
     * Release any acquired resources.
     */
    protected void localRelease()
    {
        if (_repeater)
            DataAccessProviderStack.removeDataAccessProvider(pageContext);

        super.localRelease();
        _state.clear();

        _defaultSelections = null;
        _formatters = null;
        _match = null;
        _saveBody = null;
        _nullable = false;
        _nullableOptionText = null;
        _optionList = null;

        _repIdx = 0;
        _repeater = false;
        _repCurItem = null;
        _repCurStage = RepeatingStages.BEFORE;
        _dynamicOptions = null;
        _formatterError = false;
        _optRb = null;

        _order = DEFAULT_ORDER;
    }

    private String getErrorsFromBody()
    {
        final String END_TOKEN = "</span>";
        assert(_saveBody != null);
        InternalStringBuilder body = new InternalStringBuilder(_saveBody.length());
        InternalStringBuilder error = new InternalStringBuilder(_saveBody.length());

        // pull out all of the spans  These should be legally constructed, otherwise we will ignore them.
        int len = _saveBody.length();
        int pos = 0;
        while (pos < len) {

            // find the start of a span, if we dont' find one then it's over....
            int start = _saveBody.indexOf("<span", pos);
            if (start == -1)
                break;

            // if we don't find the end of the <span> then we don't have a legal span so ignore it
            int end = _saveBody.indexOf(END_TOKEN);
            if (end == -1)
                break;

            // copy the pos to start into the body
            int realEnd = end + END_TOKEN.length() + 1;
            body.append(_saveBody.substring(pos, start));
            error.append(_saveBody.substring(start, realEnd));
            pos = realEnd;
        }

        // recreate the remainder of the body, everything not left
        body.append(_saveBody.substring(pos, len));
        _saveBody = body.toString();

        // return the error
        return error.toString();
    }

    /**
     * This method will side affects the <code>_repCurItem</code> to insure that it
     * is set to the next item in the iteration set.  It will return <code>true</code>
     * if there is a next item, and <code>false</code> when we are done with the iteration
     * @return returns <code>true</code> when <code>_repCurItem</code> contains the next item and
     *         <code>false</code> when we are done.
     * @throws JspException
     */
    private boolean doRepeaterAfterBody()
            throws JspException
    {
        switch (_repCurStage.getValue()) {
            case RepeatingStages.INT_BEFORE:
                if (!moveNext())
                    return false;
                return doRepeaterAfterBody();
            case RepeatingStages.INT_OPTION:
                assert (_repeaterIterator instanceof Iterator);
                while (_repeaterIterator.hasNext()) {
                    _repCurItem = _repeaterIterator.next();
                    if (_repCurItem != null) {
                        _optionList.add(_repCurItem);
                        return true;
                    }
                }
                if (!moveNext())
                    return false;
                return doRepeaterAfterBody();

            case RepeatingStages.INT_DEFAULT:
            case RepeatingStages.INT_DATASOURCE:
            case RepeatingStages.INT_NULL:
                assert (_repeaterIterator instanceof Iterator);
                while (_repeaterIterator.hasNext()) {
                    _repCurItem = _repeaterIterator.next();
                    if (!_optionList.contains(_repCurItem)) {
                        _optionList.add(_repCurItem);
                        return true;
                    }
                }
                if (!moveNext())
                    return false;
                return doRepeaterAfterBody();
        }
        return false;
    }

    /**
     * This method will move to the next iteration type.  The order of the
     * iteration is defined by the <code>_order</code> array.  The result
     * is side affecting the _repeaterIterator by initializing it.  If there
     * is nothing further, then we will return false, otherwise we return true.
     * @return
     * @throws JspException
     */
    private boolean moveNext()
            throws JspException
    {
        // increment the current position, if we are beyond the end of the array return
        _repIdx++;
        if (_repIdx == _order.length)
            return false;

        // Get the next stage and clear the _repeaterIterator
        _repCurStage = _order[_repIdx];
        _repeaterIterator = null;

        // process each type of iteration...
        // Each will recursively call moveNext, if that stage doesn't support iteration
        switch (_repCurStage.getValue()) {
            case RepeatingStages.INT_BEFORE:
                break;
            case RepeatingStages.INT_OPTION:
                // This produces an error if the optionsDataSource is an instance of an iterator
                if (!(_dynamicOptions instanceof Iterator)) {
                    String s = Bundle.getString("Tags_OptionsDSIteratorError");
                    registerTagError(s, null);
                    return false;
                }

                assert(_dynamicOptions instanceof Iterator);
                _repeaterIterator = (Iterator) _dynamicOptions;
                break;

            case RepeatingStages.INT_DEFAULT:
                if (_defaultSelections != null)
                    _repeaterIterator = _defaultSelections.iterator();
                break;
            case RepeatingStages.INT_DATASOURCE:
                if (_match != null)
                    _repeaterIterator = Arrays.asList(_match).iterator();
                break;
            case RepeatingStages.INT_NULL:
                if (_nullable)
                    _repeaterIterator = new ArrayIterator(NULL_INSTANCE);
                break;
        }

        // return true when we set the iterator, otherwise move to the next stage.
        return (_repeaterIterator != null) ? true : moveNext();
    }

    /**
     * This method builds the list of selected items so that they can be marked as selected.
     * @param val The <code>dataSource</code>
     */
    private void buildMatch(Object val)
    {
        // create the match data
        if (val != null) {
            if (val instanceof String) {
                _match = new String[]{(String) val};
            }
            else if (val instanceof String[]) {
                String[] s = (String[]) val;
                int cnt = 0;
                for (int i = 0; i < s.length; i++) {
                    if (s[i] != null)
                        cnt++;
                }
                if (cnt == s.length)
                    _match = s;
                else {
                    if (cnt > 0) {
                        _match = new String[cnt];
                        cnt = 0;
                        for (int i = 0; i < s.length; i++) {
                            if (s[i] != null) {
                                _match[cnt++] = s[i];
                            }
                        }
                    }
                }
            }
            else {
                Iterator matchIterator = null;
                // val is never null so this would be an error
                matchIterator = IteratorFactory.createIterator(val);
                if (matchIterator == null) {
                    matchIterator = IteratorFactory.EMPTY_ITERATOR;
                }

                ArrayList matchList = new ArrayList();
                while (matchIterator.hasNext()) {
                    Object o = matchIterator.next();
                    if (o == null)
                        continue;
                    matchList.add(o);
                }

                int size = matchList.size();
                _match = new String[size];
                for (int i = 0; i < size; i++) {
                    assert (matchList.get(i) != null);
                    assert (matchList.get(i).toString() != null);
                    _match[i] = matchList.get(i).toString();
                }
            }
            if (logger.isDebugEnabled()) {
                logger.debug("****** Select Matches ******");
                if (_match != null) {
                    for (int i = 0; i < _match.length; i++) {
                        logger.debug(i + ": " + _match[i]);
                    }
                }
            }
        }
        else {
            if (_nullable && !isMultiple() && (_defaultSelections == null || _defaultSelections.size() == 0)) {
                _match = new String[]{NULL_VALUE};
            }
        }
    }

    // add the default values specified in the tag if they are needed.
    private void addDefaultsIfNeeded(ServletRequest req)
            throws JspException
    {
        if (_defaultSelections != null) {
            Iterator iterator = _defaultSelections.iterator();
            while (iterator.hasNext()) {
                Object selection = iterator.next();
                if (!_optionList.contains(selection)) {
                    addOption(req, selection.toString(), selection.toString());
                }
            }
        }
    }

    private boolean isMultiple()
    {
        return _state.multiple;
    }

    // add dthe datasource values if needed.
    private void addDatasourceIfNeeded(ServletRequest req)
            throws JspException
    {
        if (_match == null)
            return;

        for (int i = 0; i < _match.length; i++) {
            if (!_optionList.contains(_match[i])) {
                if (!_match[i].equals(NULL_VALUE))
                    addOption(req, _match[i], _match[i]);
            }
        }
    }

    private void addOption(ServletRequest req, String optionValue, String optionDisplay)
            throws JspException
    {
        assert(optionValue != null);
        assert(optionDisplay != null);

        write("\n");
        _optionState.clear();
        _optionState.value = optionValue;
        _optionState.style = _state.style;
        _optionState.styleClass = _state.styleClass;

        if (isMatched(optionValue)) {
            _optionState.selected = true;
        }

        WriteRenderAppender writer = new WriteRenderAppender(pageContext);
        if (_optRb == null)
            _optRb = TagRenderingBase.Factory.getRendering(TagRenderingBase.OPTION_TAG, req);
        _optRb.doStartTag(writer, _optionState);


        if (optionDisplay != null) {
            write(formatText(optionDisplay));
        }
        else {
            write("&lt;");
            write(optionValue);
            write(">");
        }

        _optRb.doEndTag(writer);

        addOptionToList(optionValue);
    }

    /**
     * Adds a FormatTag.Formatter to the Select's set of formatters
     * @param formatter a FormatTag.Formatter added by a child FormatTag.
     */
    public void addFormatter(FormatTag.Formatter formatter)
    {
        _formatters.add(formatter);
    }

    /**
     * Indicate that a formatter has reported an error so the formatter should output it's
     * body text.
     */
    public void formatterHasError()
    {
        _formatterError = true;
    }

    /**
     */
    public void addOptionToList(String value)
    {
        _optionList.add(value);
    }

    /**
     * Apply the Select's set of formatters to the given text
     * @param text the text to format.
     * @return the formatted text
     */
    public String formatText(Object text)
            throws JspException
    {
        int cnt = _formatters.size();
        for (int i = 0; i < cnt; i++) {
            FormatTag.Formatter currentFormatter = (FormatTag.Formatter) _formatters.get(i);
            try {
                text = currentFormatter.format(text);
            }
            catch (JspException e) {
                registerTagError(e.getMessage(), e);
            }
        }
        return text.toString();
    }

    /* ==================================================================
     *
     * This tag's publically exposed HTML, CSS, and JavaScript attributes
     *
     * ==================================================================
     */

    /**
     * Sets the accessKey attribute value.  This should key value of the
     * keyboard navigation key.  It is recommended not to use the following
     * values because there are often used by browsers <code>A, C, E, F, G,
     * H, V, left arrow, and right arrow</code>.
     * @param accessKey the accessKey value.
     * @jsptagref.attributedescription The keyboard navigation key for the element.
     * The following values are not recommended because they
     * are often used by browsers: <code>A, C, E, F, G,
     * H, V, left arrow, and right arrow</code>
     * @jsptagref.databindable false
     * @jsptagref.attributesyntaxvalue <i>string_accessKey</i>
     * @netui:attribute required="false" rtexprvalue="true" type="char"
     * description="The keyboard navigation key for the element.
     * The following values are not recommended because they
     * are often used by browsers: A, C, E, F, G,
     * H, V, left arrow, and right arrow"
     */
    public void setAccessKey(char accessKey)
    {
        _state.registerAttribute(AbstractHtmlState.ATTR_GENERAL, ACCESSKEY, Character.toString(accessKey));
    }

    /**
     * Sets the tabIndex of the rendered html tag.
     * @param tabindex the tab index.
     * @jsptagref.attributedescription The tabIndex of the rendered HTML tag.  This attribute determines the position of the
     * tag in the sequence of page elements that the user may advance through by pressing the TAB key.
     * @jsptagref.databindable false
     * @jsptagref.attributesyntaxvalue <i>string_tabIndex</i>
     * @netui:attribute required="false" rtexprvalue="true" type="int"
     * description="The tabIndex of the rendered HTML tag.  This attribute determines the position of the
     * tag in the sequence of page elements that the user may advance through by pressing the TAB key."
     */
    public void setTabindex(int tabindex)
    {
        _state.registerAttribute(AbstractHtmlState.ATTR_GENERAL, TABINDEX, Integer.toString(tabindex));
    }
}

