// Copyright 2008-2012 severally by the contributors
//
// 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 net.sf.practicalxml.xpath;

import java.util.HashMap;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.xpath.XPathFunction;


/**
 *  A factory for producing {@link XPathWrapper} instances with the same
 *  configuration (namespace bindings and functions), reducing the amount
 *  of code needed when applying multiple expressions to the same complex
 *  XML.
 *  <p>
 *  As with <code>XPathWrapper</code> itself, this object is intended to
 *  be configured using the builder pattern: all configuration methods
 *  return the factory instance. Binding methods are the same as
 *  <code>XPathWrapper</code>, excepting the deprecated
 *  <code>bindDefaultNamespace()</code>.
 *  <p>
 *  As a potential performance improvement, for XPath expressions that are
 *  repeatedly executed, you can create an optional cache for created wrapper
 *  instances. Note that changes to the factory configuration will not affect
 *  cached wrapper instances. <em>Note:</em> At the present time, there is no
 *  way to clear the cache.
 *  <p>
 *  The {@link #newXPath} method is reentrant, and may be invoked from multiple
 *  threads concurrently. However, this class is not generally thread-safe: you
 *  cannot configure in one thread and create wrapper instances in another.
 *
 *  @since 1.1.12
 */
public class XPathWrapperFactory
implements Cloneable
{
    /**
     *  Defines the various options for caching.
     */
    public enum CacheType
    {
        /**
         *  No cache. Each call to to <code>newXPath()</code> produces a new
         *  instance.
         */
        NONE,

        /**
         *  A simple cache, keyed by XPath expression. This should <em>not</em>
         *  be used in a multi-threaded application.
         */
        SIMPLE,

        /**
         *  A thread-local cache, keyed by XPath expression.
         */
        THREADSAFE
    }


//----------------------------------------------------------------------------
//  Instance Variables and Constructors
//----------------------------------------------------------------------------

    // only one of these will be not-null, and only if we use that cache type
    private Map<String,XPathWrapper> _simpleCache;
    private ThreadLocal<Map<String,XPathWrapper>> _threadsafeCache;

    // these will be shared between paths
    private Map<String,String> _namespaces = new HashMap<String,String>();
    private Map<QName,Object> _variables = new HashMap<QName,Object>();

    // this will be copied for each path
    private FunctionResolver _functions = new FunctionResolver();


    /**
     *  Creates a factory that will always return new <code>XPathWrapper</code>
     *  instances.
     */
    public XPathWrapperFactory()
    {
        this(CacheType.NONE);
    }


    /**
     *  Creates a factory that will optionally store <code>XPathWrapper</code>
     *  instances in a cache.
     *
     *  @param  cacheType   One of the {@link XPathWrapperFactory.CacheType} values.
     */
    public XPathWrapperFactory(CacheType cacheType)
    {
        switch (cacheType)
        {
            case SIMPLE :
                _simpleCache = new HashMap<String,XPathWrapper>();
                break;
            case THREADSAFE :
                _threadsafeCache = new ThreadLocal<Map<String,XPathWrapper>>()
                {
                    @Override
                    protected Map<String,XPathWrapper> initialValue()
                    {
                        return new HashMap<String,XPathWrapper>();
                    }
                };
                break;
            default :
                // no cache, don't do anything
        }
    }


//----------------------------------------------------------------------------
//  Public methods
//----------------------------------------------------------------------------

    /**
     *  Adds a namespace binding, which will be used for subsequent wrappers
     *  created from this factory. If you add multiple bindings with the same
     *  namespace, only the last is retained.
     *
     *  @param  prefix  The prefix used to reference this namespace in the
     *                  XPath expression. Note that this does <em>not</em>
     *                  need to be the same prefix used by the document.
     *  @param  nsURI   The namespace URI to associate with this prefix.
     *
     *  @return The wrapper, so that calls may be chained.
     */
    public XPathWrapperFactory bindNamespace(String prefix, String nsURI)
    {
        _namespaces.put(prefix, nsURI);
        return this;
    }


    /**
     *  Binds a value to a variable, replacing any previous value for that
     *  variable. Unlike other configuration methods, this may be called
     *  after calling <code>evaluate()</code>; the new values will be used
     *  for subsequent evaluations.
     *
     *  @param  name    The name of the variable; this is turned into a
     *                  <code>QName</code> without namespace.
     *  @param  value   The value of the variable; the XPath evaluator must
     *                  be able to convert this value into a type usable in
     *                  an XPath expression.
     *
     *  @return The wrapper, so that calls may be chained.
     */
    public XPathWrapperFactory bindVariable(String name, Object value)
    {
        return bindVariable(new QName(name), value);
    }


    /**
     *  Binds a value to a variable, replacing any previous value for that
     *  variable. Unlike other configuration methods, this may be called
     *  after calling <code>evaluate()</code>; the new values will be used
     *  for subsequent evaluations.
     *
     *  @param  name    The fully-qualified name of the variable.
     *  @param  value   The value of the variable; the XPath evaluator must
     *                  be able to convert this value into a type usable in
     *                  an XPath expression.
     *
     *  @return The wrapper, so that calls may be chained.
     */
    public XPathWrapperFactory bindVariable(QName name, Object value)
    {
        _variables.put(name, value);
        return this;
    }


    /**
     *  Binds an {@link net.sf.practicalxml.xpath.AbstractFunction} to this
     *  expression. Subsequent calls to this method with the same name will
     *  silently replace the binding.
     *  <p>
     *  All XPath instances created from this factory will share a single
     *  instance of the bound function. If your function maintains state,
     *  you are responsible for proper synchronization.
     *  <p>
     *  Per the JDK documentation, user-defined functions must occupy their
     *  own namespace. You must bind a namespace for this function, and use
     *  the bound prefix to reference it in your expression. Alternatively,
     *  you can call the variant of <code>bindFunction()</code> that binds
     *  a prefix to the function's namespace.
     *
     *  @param  func    The function.
     *
     *  @return The wrapper, so that calls may be chained.
     */
    public XPathWrapperFactory bindFunction(AbstractFunction<?> func)
    {
        _functions.addFunction(func);
        return this;
    }


    /**
     *  Binds an {@link net.sf.practicalxml.xpath.AbstractFunction} to this
     *  expression, along with the prefix used to access that function.
     *  Subsequent calls to this method with the same name will silently
     *  replace the binding.
     *  <p>
     *  All XPath instances created from this factory will share a single
     *  instance of the bound function. If your function maintains state,
     *  you are responsible for proper synchronization.
     *  <p>
     *  Per the JDK documentation, user-defined functions must occupy their
     *  own namespace. This method will retrieve the namespace from the
     *  function, and bind it to the passed prefix.
     *
     *  @param  func    The function.
     *  @param  prefix  The prefix to bind to this function's namespace.
     *
     *  @return The wrapper, so that calls may be chained.
     */
    public XPathWrapperFactory bindFunction(AbstractFunction<?> func, String prefix)
    {
        _functions.addFunction(func);
        bindNamespace(prefix, func.getNamespaceUri());
        return this;
    }


    /**
     *  Binds a standard <code>XPathFunction</code> to this expression,
     *  handling any number of arguments. Subsequent calls to this method
     *  with the same name will silently replace the binding.
     *  <p>
     *  All XPath instances created from this factory will share a single
     *  instance of the bound function. If your function maintains state,
     *  you are responsible for proper synchronization.
     *  <p>
     *  Per the JDK documentation, user-defined functions must occupy their
     *  own namespace. If the qualified name that you pass to this method
     *  includes a prefix, the associated namespace will be bound to that
     *  prefix. If not, you must bind the namespace explicitly. In either
     *  case, you must refer to the function in your expression using a
     *  bound prefix.
     *
     *  @param  name    The qualified name for this function. Must contain
     *                  a name and namespace, may contain a prefix.
     *  @param  func    The function to bind to this name.
     *
     *  @return The wrapper, so that calls may be chained.
     */
    public XPathWrapperFactory bindFunction(QName name, XPathFunction func)
    {
        _functions.addFunction(func, name);
        return this;
    }


    /**
     *  Binds a standard <code>XPathFunction</code> to this expression,
     *  handling a specific number of arguments. Subsequent calls to this
     *  method with the same name and arity will silently replace the binding.
     *  <p>
     *  All XPath instances created from this factory will share a single
     *  instance of the bound function. If your function maintains state,
     *  you are responsible for proper synchronization.
     *  <p>
     *  Per the JDK documentation, user-defined functions must occupy their
     *  own namespace. If the qualified name that you pass to this method
     *  includes a prefix, the associated namespace will be bound to that
     *  prefix. If not, you must bind the namespace explicitly. In either
     *  case, you must refer to the function in your expression using a
     *  bound prefix.
     *
     *  @param  name    The qualified name for this function. Must contain
     *                  a name and namespace, may contain a prefix.
     *  @param  func    The function to bind to this name.
     *  @param  arity   The number of arguments accepted by this function.
     *
     *  @return The wrapper, so that calls may be chained.
     */
    public XPathWrapperFactory bindFunction(QName name, XPathFunction func, int arity)
    {
        _functions.addFunction(func, name, arity);
        return this;
    }


    /**
     *  Binds a standard <code>XPathFunction</code> to this expression,
     *  handling a specific range of arguments. Subsequent calls to this
     *  method with the same name and range of arguments will silently
     *  replace the binding.
     *  <p>
     *  All XPath instances created from this factory will share a single
     *  instance of the bound function. If your function maintains state,
     *  you are responsible for proper synchronization.
     *  <p>
     *  Per the JDK documentation, user-defined functions must occupy their
     *  own namespace. If the qualified name that you pass to this method
     *  includes a prefix, the associated namespace will be bound to that
     *  prefix. If not, you must bind the namespace explicitly. In either
     *  case, you must refer to the function in your expression using a
     *  bound prefix.
     *
     *  @param  name      The qualified name for this function. Must contain
     *                    a name and namespace, may contain a prefix.
     *  @param  func      The function to bind to this name.
     *  @param  minArity  The minimum number of arguments accepted by this
     *                    function.
     *  @param  maxArity  The maximum number of arguments accepted by this
     *                    function.
     *
     *  @return The wrapper, so that calls may be chained.
     */
    public XPathWrapperFactory bindFunction(
            QName name, XPathFunction func, int minArity, int maxArity)
    {
        _functions.addFunction(func, name, minArity, maxArity);
        return this;
    }


    /**
     *  Returns an {@link XPathWrapper} instance for the specified expression.
     *  This may either be a new or cached instance.
     */
    public XPathWrapper newXPath(String xpath)
    {
        XPathWrapper wrapper = retrieveFromCache(xpath);
        if (wrapper != null)
            return wrapper;

        wrapper = new XPathWrapper(xpath);

        for (Map.Entry<String,String> namespace : _namespaces.entrySet())
            wrapper.bindNamespace(namespace.getKey(), namespace.getValue());

        for (Map.Entry<QName,Object> variable : _variables.entrySet())
            wrapper.bindVariable(variable.getKey(), variable.getValue());

        wrapper.setFunctionResolver(_functions.clone());

        addToCache(xpath, wrapper);
        return wrapper;
    }


//----------------------------------------------------------------------------
//  Internals
//----------------------------------------------------------------------------

    private XPathWrapper retrieveFromCache(String xpath)
    {
        if (_simpleCache != null)
        {
            return _simpleCache.get(xpath);
        }
        else if (_threadsafeCache != null)
        {
            return _threadsafeCache.get().get(xpath);
        }

        return null;
    }


    private void addToCache(String xpath, XPathWrapper wrapper)
    {
        if (_simpleCache != null)
        {
            _simpleCache.put(xpath, wrapper);
        }
        else if (_threadsafeCache != null)
        {
            _threadsafeCache.get().put(xpath, wrapper);
        }
    }
}
