//  Copyright 2004 The Apache Software Foundation
//
// Licensed 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.tapestry;

import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.ResourceBundle;
import java.util.Set;

import javax.servlet.ServletContext;

import org.apache.tapestry.event.ChangeObserver;
import org.apache.tapestry.event.ObservedChangeEvent;
import org.apache.tapestry.request.RequestContext;
import org.apache.tapestry.resource.ContextResourceLocation;
import org.apache.tapestry.spec.IComponentSpecification;
import org.apache.tapestry.util.AdaptorRegistry;
import org.apache.tapestry.util.StringSplitter;

/**
 *  A placeholder for a number of (static) methods that don't belong elsewhere, as well
 *  as a global location for static constants.
 *
 *  @since 1.0.1
 *  @version $Id: Tapestry.java 243955 2004-09-01 13:18:38Z ehatcher $
 *  @author Howard Lewis Ship
 *
 **/

public final class Tapestry
{
    /**
     *  Name of a request attribute used with the
     *  {@link #TAGSUPPORT_SERVICE} service.  The attribute
     *  defines the underlying service to for which a URL will be generated.
     *
     *  @since 3.0
     *
     **/

    public final static String TAG_SUPPORT_SERVICE_ATTRIBUTE =
        "org.apache.tapestry.tagsupport.service";

    /**
     * Name of a request attribute used with the
     * {@link #TAGSUPPORT_SERVICE} service.  The attribute
     * defines the correct servlet path for the
     * Tapestry application (which, for the odd-man-out TAGSUPPORT_SERVICE
     * may not match HttpServletRequest.getServletPath() because of
     * the use of an include.
     *
     * @since 3.0
     */

    public final static String TAG_SUPPORT_SERVLET_PATH_ATTRIBUTE =
        "org.apache.tapestry.tagsupport.servlet-path";

    /**
     *  Name of a request attribute used with the
     *  {@link #TAGSUPPORT_SERVICE} service.  The attribute
     *  defines an array of objects to be converted into
     *  service parameters (i.e., for use with the
     *  {@link #EXTERNAL_SERVICE}).
     *
     *  @since 3.0
     *
     **/

    public final static String TAG_SUPPORT_PARAMETERS_ATTRIBUTE =
        "org.apache.tapestry.tagsupport.parameters";

    /**
     *  Service used to support rendering of JSP tags.  tagsupport is provided
     *  with a service and service parameters via request attributes
     *  and creates a URI from the result, which is output to the response.
     *
     *  @since 3.0
     *
     **/

    public static final String TAGSUPPORT_SERVICE = "tagsupport";

    /**
     *  The name ("action") of a service that allows behavior to be associated with
     *  an {@link IAction} component, such as {@link org.apache.tapestry.link.ActionLink} or
     *  {@link org.apache.tapestry.form.Form}.
     *
     *  <p>This service is used with actions that are tied to the
     *  dynamic state of the page, and which require a rewind of the page.
     *
     **/

    public final static String ACTION_SERVICE = "action";

    /**
     *  The name ("direct") of a service that allows stateless behavior for an {@link
     *  org.apache.tapestry.link.DirectLink} component.
     *
     *  <p>This service rolls back the state of the page but doesn't
     *  rewind the the dynamic state of the page the was the action
     *  service does, which is more efficient but less powerful.
     *
     *  <p>An array of String parameters may be included with the
     *  service URL; these will be made available to the {@link org.apache.tapestry.link.DirectLink}
     *  component's listener.
     *
     **/

    public final static String DIRECT_SERVICE = "direct";

    /**
     *  The name ("external") of a service that a allows {@link IExternalPage} to be selected.
     *  Associated with a {@link org.apache.tapestry.link.ExternalLink} component.
     *
     *  <p>This service enables {@link IExternalPage}s to be accessed via a URL.
     *  External pages may be booked marked using their URL for future reference.
     *
     *  <p>An array of Object parameters may be included with the
     *  service URL; these will be passed to the
     *  {@link IExternalPage#activateExternalPage(Object[], IRequestCycle)} method.
     *
     **/

    public final static String EXTERNAL_SERVICE = "external";

    /**
     *  The name ("page") of a service that allows a new page to be selected.
     *  Associated with a {@link org.apache.tapestry.link.PageLink} component.
     *
     *  <p>The service requires a single parameter:  the name of the target page.
     **/

    public final static String PAGE_SERVICE = "page";

    /**
     *  The name ("home") of a service that jumps to the home page.  A stand-in for
     *  when no service is provided, which is typically the entrypoint
     *  to the application.
     *
     **/

    public final static String HOME_SERVICE = "home";

    /**
     *  The name ("restart") of a service that invalidates the session and restarts
     *  the application.  Typically used just
     *  to recover from an exception.
     *
     **/

    public static final String RESTART_SERVICE = "restart";

    /**
     *  The name ("asset") of a service used to access internal assets.
     *
     **/

    public static final String ASSET_SERVICE = "asset";

    /**
     *  The name ("reset") of a service used to clear cached template
     *  and specification data and remove all pooled pages.
     *  This is only used when debugging as
     *  a quick way to clear the out cached data, to allow updated
     *  versions of specifications and templates to be loaded (without
     *  stopping and restarting the servlet container).
     *
     *  <p>This service is only available if the Java system property
     *  <code>org.apache.tapestry.enable-reset-service</code>
     *  is set to <code>true</code>.
     *
     **/

    public static final String RESET_SERVICE = "reset";

    /**
     *  Query parameter that identfies the service for the
     *  request.
     *
     *  @since 1.0.3
     *
     **/

    public static final String SERVICE_QUERY_PARAMETER_NAME = "service";

    /**
     *  The query parameter for application specific parameters to the
     *  service (this is used with the direct service).  Each of these
     *  values is encoded with {@link java.net.URLEncoder#encode(String)} before
     *  being added to the URL.  Multiple values are handle by repeatedly
     *  establishing key/value pairs (this is a change from behavior in
     *  2.1 and earlier).
     *
     *  @since 1.0.3
     *
     **/

    public static final String PARAMETERS_QUERY_PARAMETER_NAME = "sp";

    /**
     *  Property name used to get the extension used for templates.  This
     *  may be set in the page or component specification, or in the page (or
     *  component's) immediate container (library or application specification).
     *  Unlike most properties, value isn't inherited all the way up the chain.
     *  The default template extension is "html".
     *
     *  @since 3.0
     *
     **/

    public static final String TEMPLATE_EXTENSION_PROPERTY =
        "org.apache.tapestry.template-extension";

    /**
     *  The default extension for templates, "html".
     *
     *  @since 3.0
     *
     **/

    public static final String DEFAULT_TEMPLATE_EXTENSION = "html";

    /**
     *  The name of an {@link org.apache.tapestry.IRequestCycle} attribute in which the
     *  currently rendering {@link org.apache.tapestry.components.ILinkComponent}
     *  is stored.  Link components do not nest.
     *
     **/

    public static final String LINK_COMPONENT_ATTRIBUTE_NAME =
        "org.apache.tapestry.active-link-component";

    /**
     *  Suffix appended to a parameter name to form the name of a property that stores the
     *  binding for the parameter.
     *
     *  @since 3.0
     *
     **/

    public static final String PARAMETER_PROPERTY_NAME_SUFFIX = "Binding";

    /**
     *  Name of application extension used to resolve page and component
     *  specifications that can't be located by the normal means.  The
     *  extension must implement
     *  {@link org.apache.tapestry.resolver.ISpecificationResolverDelegate}.
     *
     *  @since 3.0
     *
     **/

    public static final String SPECIFICATION_RESOLVER_DELEGATE_EXTENSION_NAME =
        "org.apache.tapestry.specification-resolver-delegate";

    /**
     *  Name of application extension used to resolve page and component
     *  templates that can't be located by the normal means.
     *  The extension must implement
     *  {@link org.apache.tapestry.engine.ITemplateSourceDelegate}.
     *
     *  @since 3.0
     *
     **/

    public static final String TEMPLATE_SOURCE_DELEGATE_EXTENSION_NAME =
        "org.apache.tapestry.template-source-delegate";

    /**
     *   Key used to obtain an extension from the application specification.  The extension,
     *   if it exists, implements {@link org.apache.tapestry.request.IRequestDecoder}.
     *
     *   @since 2.2
     *
     **/

    public static final String REQUEST_DECODER_EXTENSION_NAME =
        "org.apache.tapestry.request-decoder";

    /**
     *  Name of optional application extension for the multipart decoder
     *  used by the application.  The extension must implement
     *  {@link org.apache.tapestry.multipart.IMultipartDecoder}
     *  (and is generally a configured instance of
     *  {@link org.apache.tapestry.multipart.DefaultMultipartDecoder}).
     *
     *  @since 3.0
     *
     **/

    public static final String MULTIPART_DECODER_EXTENSION_NAME =
        "org.apache.tapestry.multipart-decoder";

    /**
     * Method id used to check that {@link IPage#validate(IRequestCycle)}
     * is invoked.
     * @see #checkMethodInvocation(Object, String, Object)
     * @since 3.0
     */

    public static final String ABSTRACTPAGE_VALIDATE_METHOD_ID = "AbstractPage.validate()";

    /**
     * Method id used to check that {@link IPage#detach()} is invoked.
     * @see #checkMethodInvocation(Object, String, Object)
     * @since 3.0
     */

    public static final String ABSTRACTPAGE_DETACH_METHOD_ID = "AbstractPage.detach()";

    /**
     *  Regular expression defining a simple property name.  Used by several different
     *  parsers. Simple property names match Java variable names; a leading letter
     *  (or underscore), followed by letters, numbers and underscores.
     *
     *  @since 3.0
     *
     **/

    public static final String SIMPLE_PROPERTY_NAME_PATTERN = "^_?[a-zA-Z]\\w*$";

    /**
     * Name of an application extension used as a factory for
     * {@link org.apache.tapestry.engine.IMonitor} instances.  The extension
     * must implement {@link org.apache.tapestry.engine.IMonitorFactory}.
     *
     * @since 3.0
     */

    public static final String MONITOR_FACTORY_EXTENSION_NAME =
        "org.apache.tapestry.monitor-factory";

    /**
     * Class name of an {@link ognl.TypeConverter} implementing class
     * to use as a type converter for {@link org.apache.tapestry.binding.ExpressionBinding}
     */
    public static final String OGNL_TYPE_CONVERTER = "org.apache.tapestry.ognl-type-converter";

    /**
     *  Prevent instantiation.
     *
     **/

    private Tapestry()
    {
    }

    /**
     *  The version of the framework; this is updated for major releases.
     *
     **/

    public static final String VERSION = readVersion();

    /**
     *  Contains strings loaded from TapestryStrings.properties.
     *
     *  @since 1.0.8
     *
     **/

    private static ResourceBundle _strings;

    /**
     *  A {@link Map} that links Locale names (as in {@link Locale#toString()} to
     *  {@link Locale} instances.  This prevents needless duplication
     *  of Locales.
     *
     **/

    private static final Map _localeMap = new HashMap();

    static {
        Locale[] locales = Locale.getAvailableLocales();
        for (int i = 0; i < locales.length; i++)
        {
            _localeMap.put(locales[i].toString(), locales[i]);
        }
    }

    /**
     *  Used for tracking if a particular super-class method has been invoked.
     */

    private static final ThreadLocal _invokedMethodIds = new ThreadLocal();

    /**
     *  A {@link org.apache.tapestry.util.AdaptorRegistry} used to coerce arbitrary objects
     *  to boolean values.
     *
     *  @see #evaluateBoolean(Object)
     **/

    private static final AdaptorRegistry _booleanAdaptors = new AdaptorRegistry();

    private static abstract class BoolAdaptor
    {
        /**
         *  Implemented by subclasses to coerce an object to a boolean.
         *
         **/

        public abstract boolean coerce(Object value);
    }

    private static class BooleanAdaptor extends BoolAdaptor
    {
        public boolean coerce(Object value)
        {
            Boolean b = (Boolean) value;

            return b.booleanValue();
        }
    }

    private static class NumberAdaptor extends BoolAdaptor
    {
        public boolean coerce(Object value)
        {
            Number n = (Number) value;

            return n.intValue() > 0;
        }
    }

    private static class CollectionAdaptor extends BoolAdaptor
    {
        public boolean coerce(Object value)
        {
            Collection c = (Collection) value;

            return c.size() > 0;
        }
    }

    private static class StringAdaptor extends BoolAdaptor
    {
        public boolean coerce(Object value)
        {
            String s = (String) value;

            if (s.length() == 0)
                return false;

            String ts = s.trim();
            if (ts.length() == 0)
                return false;

            // Here probably Boolean.getBoolean(s) should be used,
            // but we need the opposite check
            if (ts.equalsIgnoreCase("false"))
                return false;

            return true;
        }
    }

    static {
        _booleanAdaptors.register(Boolean.class, new BooleanAdaptor());
        _booleanAdaptors.register(Number.class, new NumberAdaptor());
        _booleanAdaptors.register(Collection.class, new CollectionAdaptor());
        _booleanAdaptors.register(String.class, new StringAdaptor());

        // Register a default, catch-all adaptor.

        _booleanAdaptors.register(Object.class, new BoolAdaptor()
        {
            public boolean coerce(Object value)
            {
                return true;
            }
        });
    }

    /**
     *  {@link AdaptorRegistry} used to extract an {@link Iterator} from
     *  an arbitrary object.
     *
     **/

    private static AdaptorRegistry _iteratorAdaptors = new AdaptorRegistry();

    private abstract static class IteratorAdaptor
    {
        /**
         *  Coeerces the object into an {@link Iterator}.
         *
         **/

        abstract public Iterator coerce(Object value);
    }

    private static class DefaultIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            return (Iterator) value;
        }

    }

    private static class CollectionIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            Collection c = (Collection) value;

            if (c.size() == 0)
                return null;

            return c.iterator();
        }
    }

    private static class ObjectIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            return Collections.singleton(value).iterator();
        }
    }

    private static class ObjectArrayIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            Object[] array = (Object[]) value;

            if (array.length == 0)
                return null;

            return Arrays.asList(array).iterator();
        }
    }

    private static class BooleanArrayIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            boolean[] array = (boolean[]) value;

            if (array.length == 0)
                return null;

            List l = new ArrayList(array.length);

            for (int i = 0; i < array.length; i++)
                l.add(array[i] ? Boolean.TRUE : Boolean.FALSE);

            return l.iterator();
        }
    }

    private static class ByteArrayIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            byte[] array = (byte[]) value;

            if (array.length == 0)
                return null;

            List l = new ArrayList(array.length);

            for (int i = 0; i < array.length; i++)
                l.add(new Byte(array[i]));

            return l.iterator();
        }
    }

    private static class CharArrayIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            char[] array = (char[]) value;

            if (array.length == 0)
                return null;

            List l = new ArrayList(array.length);

            for (int i = 0; i < array.length; i++)
                l.add(new Character(array[i]));

            return l.iterator();
        }
    }

    private static class ShortArrayIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            short[] array = (short[]) value;

            if (array.length == 0)
                return null;

            List l = new ArrayList(array.length);

            for (int i = 0; i < array.length; i++)
                l.add(new Short(array[i]));

            return l.iterator();
        }
    }

    private static class IntArrayIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            int[] array = (int[]) value;

            if (array.length == 0)
                return null;

            List l = new ArrayList(array.length);

            for (int i = 0; i < array.length; i++)
                l.add(new Integer(array[i]));

            return l.iterator();
        }
    }

    private static class LongArrayIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            long[] array = (long[]) value;

            if (array.length == 0)
                return null;

            List l = new ArrayList(array.length);

            for (int i = 0; i < array.length; i++)
                l.add(new Long(array[i]));

            return l.iterator();
        }
    }

    private static class FloatArrayIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            float[] array = (float[]) value;

            if (array.length == 0)
                return null;

            List l = new ArrayList(array.length);

            for (int i = 0; i < array.length; i++)
                l.add(new Float(array[i]));

            return l.iterator();
        }
    }

    private static class DoubleArrayIteratorAdaptor extends IteratorAdaptor
    {
        public Iterator coerce(Object value)
        {
            double[] array = (double[]) value;

            if (array.length == 0)
                return null;

            List l = new ArrayList(array.length);

            for (int i = 0; i < array.length; i++)
                l.add(new Double(array[i]));

            return l.iterator();
        }
    }

    static {
        _iteratorAdaptors.register(Iterator.class, new DefaultIteratorAdaptor());
        _iteratorAdaptors.register(Collection.class, new CollectionIteratorAdaptor());
        _iteratorAdaptors.register(Object.class, new ObjectIteratorAdaptor());
        _iteratorAdaptors.register(Object[].class, new ObjectArrayIteratorAdaptor());
        _iteratorAdaptors.register(boolean[].class, new BooleanArrayIteratorAdaptor());
        _iteratorAdaptors.register(byte[].class, new ByteArrayIteratorAdaptor());
        _iteratorAdaptors.register(char[].class, new CharArrayIteratorAdaptor());
        _iteratorAdaptors.register(short[].class, new ShortArrayIteratorAdaptor());
        _iteratorAdaptors.register(int[].class, new IntArrayIteratorAdaptor());
        _iteratorAdaptors.register(long[].class, new LongArrayIteratorAdaptor());
        _iteratorAdaptors.register(float[].class, new FloatArrayIteratorAdaptor());
        _iteratorAdaptors.register(double[].class, new DoubleArrayIteratorAdaptor());
    }

    /**
     *  Copys all informal {@link IBinding bindings} from a source component
     *  to the destination component.  Informal bindings are bindings for
     *  informal parameters.  This will overwrite parameters (formal or
     *  informal) in the
     *  destination component if there is a naming conflict.
     *
     *
     **/

    public static void copyInformalBindings(IComponent source, IComponent destination)
    {
        Collection names = source.getBindingNames();

        if (names == null)
            return;

        IComponentSpecification specification = source.getSpecification();
        Iterator i = names.iterator();

        while (i.hasNext())
        {
            String name = (String) i.next();

            // If not a formal parameter, then copy it over.

            if (specification.getParameter(name) == null)
            {
                IBinding binding = source.getBinding(name);

                destination.setBinding(name, binding);
            }
        }
    }

    /**
     *  Evaluates an object to determine its boolean value.
     *
     *  <table border=1>
     *	<tr> <th>Class</th> <th>Test</th> </tr>
     *  <tr>
     *		<td>{@link Boolean}</td>
     *		<td>Self explanatory.</td>
     *	</tr>
     *	<tr> <td>{@link Number}</td>
     *		<td>True if non-zero, false otherwise.</td>
     *		</tr>
     *	<tr>
     *		<td>{@link Collection}</td>
     *		<td>True if contains any elements (non-zero size), false otherwise.</td>
     *		</tr>
     *	<tr>
     *		<td>{@link String}</td>
     *		<td>True if contains any non-whitespace characters, false otherwise.</td>
     *		</tr>
     *	<tr>
     *		<td>Any Object array type</td>
     *		<td>True if contains any elements (non-zero length), false otherwise.</td>
     *	<tr>
     *</table>
     *
     * <p>Any other non-null object evaluates to true.
     *
     **/

    public static boolean evaluateBoolean(Object value)
    {
        if (value == null)
            return false;

        Class valueClass = value.getClass();
        if (valueClass.isArray())
        {
            Object[] array = (Object[]) value;

            return array.length > 0;
        }

        BoolAdaptor adaptor = (BoolAdaptor) _booleanAdaptors.getAdaptor(valueClass);

        return adaptor.coerce(value);
    }

    /**
     *  Converts an Object into an {@link Iterator}, following some basic rules.
     *
     *  <table border=1>
     * 	<tr><th>Input Class</th> <th>Result</th> </tr>
     * <tr><td>array</td> <td>Converted to a {@link List} and iterator returned.
     * null returned if the array is empty.  This works with both object arrays and
     *  arrays of scalars. </td>
     * </tr>
     * <tr><td>{@link Iterator}</td> <td>Returned as-is.</td>
     * <tr><td>{@link Collection}</td> <td>Iterator returned, or null
     *  if the Collection is empty</td> </tr>
    
     * <tr><td>Any other</td> <td>{@link Iterator} for singleton collection returned</td> </tr>
     * <tr><td>null</td> <td>null returned</td> </tr>
     * </table>
     *
     **/

    public static Iterator coerceToIterator(Object value)
    {
        if (value == null)
            return null;

        IteratorAdaptor adaptor = (IteratorAdaptor) _iteratorAdaptors.getAdaptor(value.getClass());

        return adaptor.coerce(value);
    }

    /**
     *  Gets the {@link Locale} for the given string, which is the result
     *  of {@link Locale#toString()}.  If no such locale is already registered,
     *  a new instance is created, registered and returned.
     *
     *
     **/

    public static Locale getLocale(String s)
    {
        Locale result = null;

        synchronized (_localeMap)
        {
            result = (Locale) _localeMap.get(s);
        }

        if (result == null)
        {
            StringSplitter splitter = new StringSplitter('_');
            String[] terms = splitter.splitToArray(s);

            switch (terms.length)
            {
                case 1 :

                    result = new Locale(terms[0], "");
                    break;

                case 2 :

                    result = new Locale(terms[0], terms[1]);
                    break;

                case 3 :

                    result = new Locale(terms[0], terms[1], terms[2]);
                    break;

                default :

                    throw new IllegalArgumentException(
                        "Unable to convert '" + s + "' to a Locale.");
            }

            synchronized (_localeMap)
            {
                _localeMap.put(s, result);
            }

        }

        return result;

    }

    /**
     *  Closes the stream (if not null), ignoring any {@link IOException} thrown.
     *
     *  @since 1.0.2
     *
     **/

    public static void close(InputStream stream)
    {
        if (stream != null)
        {
            try
            {
                stream.close();
            }
            catch (IOException ex)
            {
                // Ignore.
            }
        }
    }

    /**
     *  Gets a string from the TapestryStrings resource bundle.
     *  The string in the bundle
     *  is treated as a pattern for {@link MessageFormat#format(java.lang.String, java.lang.Object[])}.
     *
     *  @since 1.0.8
     *
     **/

    public static String format(String key, Object[] args)
    {
        if (_strings == null)
            _strings = ResourceBundle.getBundle("org.apache.tapestry.TapestryStrings");

        String pattern = _strings.getString(key);

        if (args == null)
            return pattern;

        return MessageFormat.format(pattern, args);
    }

    /**
     *  Convienience method for invoking {@link #format(String, Object[])}.
     *
     *  @since 3.0
     **/

    public static String getMessage(String key)
    {
        return format(key, null);
    }

    /**
     *  Convienience method for invoking {@link #format(String, Object[])}.
     *
     *  @since 3.0
     **/

    public static String format(String key, Object arg)
    {
        return format(key, new Object[] { arg });
    }

    /**
     *  Convienience method for invoking {@link #format(String, Object[])}.
     *
     *  @since 3.0
     *
     **/

    public static String format(String key, Object arg1, Object arg2)
    {
        return format(key, new Object[] { arg1, arg2 });
    }

    /**
     *  Convienience method for invoking {@link #format(String, Object[])}.
     *
     *  @since 3.0
     *
     **/

    public static String format(String key, Object arg1, Object arg2, Object arg3)
    {
        return format(key, new Object[] { arg1, arg2, arg3 });
    }

    private static final String UNKNOWN_VERSION = "Unknown";

    /**
     *  Invoked when the class is initialized to read the current version file.
     *
     **/

    private static final String readVersion()
    {
        Properties props = new Properties();

        try
        {
            InputStream in = Tapestry.class.getResourceAsStream("Version.properties");

            if (in == null)
                return UNKNOWN_VERSION;

            props.load(in);

            in.close();

            return props.getProperty("framework.version", UNKNOWN_VERSION);
        }
        catch (IOException ex)
        {
            return UNKNOWN_VERSION;
        }

    }

    /**
     *  Returns the size of a collection, or zero if the collection is null.
     *
     *  @since 2.2
     *
     **/

    public static int size(Collection c)
    {
        if (c == null)
            return 0;

        return c.size();
    }

    /**
     *  Returns the length of the array, or 0 if the array is null.
     *
     *  @since 2.2
     *
     **/

    public static int size(Object[] array)
    {
        if (array == null)
            return 0;

        return array.length;
    }

    /**
     *  Returns true if the Map is null or empty.
     *
     *  @since 3.0
     *
     **/

    public static boolean isEmpty(Map map)
    {
        return map == null || map.isEmpty();
    }

    /**
     *  Returns true if the Collection is null or empty.
     *
     *  @since 3.0
     *
     **/

    public static boolean isEmpty(Collection c)
    {
        return c == null || c.isEmpty();
    }

    /**
     *  Converts a {@link Map} to an even-sized array of key/value
     *  pairs.  This may be useful when using a Map as service parameters
     *  (with {@link org.apache.tapestry.link.DirectLink}.  Assuming the keys
     *  and values are simple objects (String, Boolean, Integer, etc.), then
     *  the representation as an array will encode more efficiently
     *  (via {@link org.apache.tapestry.util.io.DataSqueezer} than
     *  serializing the Map and its contents.
     *
     *  @return the array of keys and values, or null if the input
     *  Map is null or empty
     *
     *  @since 2.2
     **/

    public static Object[] convertMapToArray(Map map)
    {
        if (isEmpty(map))
            return null;

        Set entries = map.entrySet();

        Object[] result = new Object[2 * entries.size()];
        int x = 0;

        Iterator i = entries.iterator();
        while (i.hasNext())
        {
            Map.Entry entry = (Map.Entry) i.next();

            result[x++] = entry.getKey();
            result[x++] = entry.getValue();
        }

        return result;
    }

    /**
     *  Converts an even-sized array of objects back
     *  into a {@link Map}.
     *
     *  @see #convertMapToArray(Map)
     *  @return a Map, or null if the array is null or empty
     *  @since 2.2
     *
     **/

    public static Map convertArrayToMap(Object[] array)
    {
        if (array == null || array.length == 0)
            return null;

        if (array.length % 2 != 0)
            throw new IllegalArgumentException(getMessage("Tapestry.even-sized-array"));

        Map result = new HashMap();

        int x = 0;
        while (x < array.length)
        {
            Object key = array[x++];
            Object value = array[x++];

            result.put(key, value);
        }

        return result;
    }

    /**
     *  Returns the application root location, which is in the
     *  {@link javax.servlet.ServletContext}, based on
     *  the {@link javax.servlet.http.HttpServletRequest#getServletPath() servlet path}.
     *
     *  @since 3.0
     *
     **/

    public static IResourceLocation getApplicationRootLocation(IRequestCycle cycle)
    {
        RequestContext context = cycle.getRequestContext();
        ServletContext servletContext = context.getServlet().getServletContext();
        String servletPath = context.getRequest().getServletPath();

        // Could strip off the servlet name (i.e., "app" in "/app") but
        // there's no need.

        return new ContextResourceLocation(servletContext, servletPath);
    }

    /**
     * Given a Class, creates a presentable name for the class, even if the
     * class is a scalar type or Array type.
     *
     * @since 3.0
     */

    public static String getClassName(Class subject)
    {
        if (subject.isArray())
            return getClassName(subject.getComponentType()) + "[]";

        return subject.getName();
    }

    /**
     *  Selects the first {@link org.apache.tapestry.ILocation} in an array of objects.
     *  Skips over nulls.  The objects may be instances of
     *  {Location or {@link org.apache.tapestry.ILocatable}.  May return null
     *  if no Location found found.
     *
     **/

    public static ILocation findLocation(Object[] locations)
    {
        for (int i = 0; i < locations.length; i++)
        {
            Object location = locations[i];

            if (location == null)
                continue;

            if (location instanceof ILocation)
                return (ILocation) location;

            if (location instanceof ILocatable)
            {
                ILocatable locatable = (ILocatable) location;
                ILocation result = locatable.getLocation();

                if (result != null)
                    return result;
            }
        }

        return null;
    }

    /**
     *  Creates an exception indicating the binding value is null.
     *
     *  @since 3.0
     *
     **/

    public static BindingException createNullBindingException(IBinding binding)
    {
        return new BindingException(getMessage("null-value-for-binding"), binding);
    }

    /** @since 3.0 **/

    public static ApplicationRuntimeException createNoSuchComponentException(
        IComponent component,
        String id,
        ILocation location)
    {
        return new ApplicationRuntimeException(
            format("no-such-component", component.getExtendedId(), id),
            component,
            location,
            null);
    }

    /** @since 3.0 **/

    public static BindingException createRequiredParameterException(
        IComponent component,
        String parameterName)
    {
        return new BindingException(
            format("required-parameter", parameterName, component.getExtendedId()),
            component,
            null,
            component.getBinding(parameterName),
            null);
    }

    /** @since 3.0 **/

    public static ApplicationRuntimeException createRenderOnlyPropertyException(
        IComponent component,
        String propertyName)
    {
        return new ApplicationRuntimeException(
            format("render-only-property", propertyName, component.getExtendedId()),
            component,
            null,
            null);
    }

    /**
     * Clears the list of method invocations.
     * @see #checkMethodInvocation(Object, String, Object)
     *
     * @since 3.0
     */

    public static void clearMethodInvocations()
    {
        _invokedMethodIds.set(null);
    }

    /**
     * Adds a method invocation to the list of invocations. This is done
     * in a super-class implementations.
     *
     * @see #checkMethodInvocation(Object, String, Object)
     * @since 3.0
     *
     */

    public static void addMethodInvocation(Object methodId)
    {
        List methodIds = (List) _invokedMethodIds.get();

        if (methodIds == null)
        {
            methodIds = new ArrayList();
            _invokedMethodIds.set(methodIds);
        }

        methodIds.add(methodId);
    }

    /**
     * Checks to see if a particular method has been invoked.  The method is identified by a
     * methodId (usually a String).  The methodName and object are used to create an
     * error message.
     *
     * <p>
     * The caller should invoke {@link #clearMethodInvocations()}, then invoke a method on
     * the object.  The super-class implementation should invoke {@link #addMethodInvocation(Object)}
     * to indicate that it was, in fact, invoked.  The caller then invokes
     * this method to vlaidate that the super-class implementation was invoked.
     *
     * <p>
     * The list of method invocations is stored in a {@link ThreadLocal} variable.
     *
     * @since 3.0
     */

    public static void checkMethodInvocation(Object methodId, String methodName, Object object)
    {
        List methodIds = (List) _invokedMethodIds.get();

        if (methodIds != null && methodIds.contains(methodId))
            return;

        throw new ApplicationRuntimeException(
            Tapestry.format(
                "Tapestry.missing-method-invocation",
                object.getClass().getName(),
                methodName));
    }

    /**
     * Method used by pages and components to send notifications about
     * property changes.
     *
     * @param component the component containing the property
     * @param propertyName the name of the property which changed
     * @param newValue the new value for the property
     *
     * @since 3.0
     */
    public static void fireObservedChange(
        IComponent component,
        String propertyName,
        Object newValue)
    {
        ChangeObserver observer = component.getPage().getChangeObserver();

        if (observer == null)
            return;

        ObservedChangeEvent event = new ObservedChangeEvent(component, propertyName, newValue);

        observer.observeChange(event);
    }

    /**
     * Method used by pages and components to send notifications about
     * property changes.
     *
     * @param component the component containing the property
     * @param propertyName the name of the property which changed
     * @param newValue the new value for the property
     *
     * @since 3.0
     */
    public static void fireObservedChange(
        IComponent component,
        String propertyName,
        boolean newValue)
    {
        ChangeObserver observer = component.getPage().getChangeObserver();

        if (observer == null)
            return;

        ObservedChangeEvent event =
            new ObservedChangeEvent(
                component,
                propertyName,
                newValue ? Boolean.TRUE : Boolean.FALSE);

        observer.observeChange(event);
    }

    /**
     * Method used by pages and components to send notifications about
     * property changes.
     *
     * @param component the component containing the property
     * @param propertyName the name of the property which changed
     * @param newValue the new value for the property
     *
     * @since 3.0
     */
    public static void fireObservedChange(
        IComponent component,
        String propertyName,
        double newValue)
    {
        ChangeObserver observer = component.getPage().getChangeObserver();

        if (observer == null)
            return;

        ObservedChangeEvent event =
            new ObservedChangeEvent(component, propertyName, new Double(newValue));

        observer.observeChange(event);
    }

    /**
     * Method used by pages and components to send notifications about
     * property changes.
     *
     * @param component the component containing the property
     * @param propertyName the name of the property which changed
     * @param newValue the new value for the property
     *
     * @since 3.0
     */
    public static void fireObservedChange(
        IComponent component,
        String propertyName,
        float newValue)
    {
        ChangeObserver observer = component.getPage().getChangeObserver();

        if (observer == null)
            return;

        ObservedChangeEvent event =
            new ObservedChangeEvent(component, propertyName, new Float(newValue));

        observer.observeChange(event);
    }

    /**
    * Method used by pages and components to send notifications about
    * property changes.
    *
    * @param component the component containing the property
    * @param propertyName the name of the property which changed
    * @param newValue the new value for the property
    *
    * @since 3.0
    */
    public static void fireObservedChange(IComponent component, String propertyName, int newValue)
    {
        ChangeObserver observer = component.getPage().getChangeObserver();

        if (observer == null)
            return;

        ObservedChangeEvent event =
            new ObservedChangeEvent(component, propertyName, new Integer(newValue));

        observer.observeChange(event);
    }

    /**
    * Method used by pages and components to send notifications about
    * property changes.
    *
    * @param component the component containing the property
    * @param propertyName the name of the property which changed
    * @param newValue the new value for the property
    *
    * @since 3.0
    */
    public static void fireObservedChange(IComponent component, String propertyName, long newValue)
    {
        ChangeObserver observer = component.getPage().getChangeObserver();

        if (observer == null)
            return;

        ObservedChangeEvent event =
            new ObservedChangeEvent(component, propertyName, new Long(newValue));

        observer.observeChange(event);
    }

    /**
     * Method used by pages and components to send notifications about
     * property changes.
     *
     * @param component the component containing the property
     * @param propertyName the name of the property which changed
     * @param newValue the new value for the property
     *
     * @since 3.0
     */
    public static void fireObservedChange(IComponent component, String propertyName, char newValue)
    {
        ChangeObserver observer = component.getPage().getChangeObserver();

        if (observer == null)
            return;

        ObservedChangeEvent event =
            new ObservedChangeEvent(component, propertyName, new Character(newValue));

        observer.observeChange(event);
    }

    /**
     * Method used by pages and components to send notifications about
     * property changes.
     *
     * @param component the component containing the property
     * @param propertyName the name of the property which changed
     * @param newValue the new value for the property
     *
     * @since 3.0
     */
    public static void fireObservedChange(IComponent component, String propertyName, byte newValue)
    {
        ChangeObserver observer = component.getPage().getChangeObserver();

        if (observer == null)
            return;

        ObservedChangeEvent event =
            new ObservedChangeEvent(component, propertyName, new Byte(newValue));

        observer.observeChange(event);
    }

    /**
     * Method used by pages and components to send notifications about
     * property changes.
     *
     * @param component the component containing the property
     * @param propertyName the name of the property which changed
     * @param newValue the new value for the property
     *
     * @since 3.0
     */
    public static void fireObservedChange(
        IComponent component,
        String propertyName,
        short newValue)
    {
        ChangeObserver observer = component.getPage().getChangeObserver();

        if (observer == null)
            return;

        ObservedChangeEvent event =
            new ObservedChangeEvent(component, propertyName, new Short(newValue));

        observer.observeChange(event);
    }

    /**
     * Returns true if the input is null or contains only whitespace.
     * 
     * <p>
     * Note: Yes, you'd think we'd use <code>StringUtils</code>, but with
     * the change in names and behavior between releases, it is smarter
     * to just implement our own little method!
     * 
     * @since 3.0
     */

    public static boolean isBlank(String input)
    {
        if (input == null || input.length() == 0)
            return true;

        return input.trim().length() == 0;
    }

    /**
     * Returns true if the input is not null and not empty (or only whitespace).
     * 
     * @since 3.0
     * 
     */

    public static boolean isNonBlank(String input)
    {
        return !isBlank(input);
    }
}