// --------------------------------------------------------------------------
// Copyright (c) 1998-2004, Drew Davidson and Luke Blanshard
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
// Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// Neither the name of the Drew Davidson nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
// OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
// AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
// DAMAGE.
// --------------------------------------------------------------------------
package ognl;

import ognl.enhance.ExpressionAccessor;

import java.io.StringReader;
import java.util.Map;

/**
 *

 * This class provides static methods for parsing and interpreting OGNL expressions.
 *

 *

 * The simplest use of the Ognl class is to get the value of an expression from an object, without
 * extra context or pre-parsing.
 *

 *
 *

 *
 * import ognl.Ognl; import ognl.OgnlException; try { result = Ognl.getValue(expression, root); }
 * catch (OgnlException ex) { // Report error or recover }
 *
 *

 *
 *

 * This will parse the expression given and evaluate it against the root object given, returning the
 * result. If there is an error in the expression, such as the property is not found, the exception
 * is encapsulated into an {@link ognl.OgnlException OgnlException}.
 *

 *

 * Other more sophisticated uses of Ognl can pre-parse expressions. This provides two advantages: in
 * the case of user-supplied expressions it allows you to catch parse errors before evaluation and
 * it allows you to cache parsed expressions into an AST for better speed during repeated use. The
 * pre-parsed expression is always returned as an Object to simplify use for programs
 * that just wish to store the value for repeated use and do not care that it is an AST. If it does
 * care it can always safely cast the value to an AST type.
 *

 *

 * The Ognl class also takes a context map as one of the parameters to the set and get
 * methods. This allows you to put your own variables into the available namespace for OGNL
 * expressions. The default context contains only the #root and #context
 * keys, which are required to be present. The addDefaultContext(Object, Map) method
 * will alter an existing Map to put the defaults in. Here is an example that shows
 * how to extract the documentName property out of the root object and append a
 * string with the current user name in parens:
 *

 *
 *

 *
 * private Map context = new HashMap(); public void setUserName(String value) {
 * context.put("userName", value); } try { // get value using our own custom context map result =
 * Ognl.getValue("documentName + \" (\" + ((#userName == null) ? \"<nobody>\" : #userName) +
 * \")\"", context, root); } catch (OgnlException ex) { // Report error or recover }
 *
 *

 *
 * @author Luke Blanshard (blanshlu@netscape.net)
 * @author Drew Davidson (drew@ognl.org)
 * @version 27 June 1999
 */
public abstract class Ognl
{

    /**
     * Parses the given OGNL expression and returns a tree representation of the expression that can
     * be used by Ognl static methods.
     *
     * @param expression
     *            the OGNL expression to be parsed
     * @return a tree representation of the expression
     * @throws ExpressionSyntaxException
     *             if the expression is malformed
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static Object parseExpression(String expression)
            throws OgnlException
    {
        try {
            OgnlParser parser = new OgnlParser(new StringReader(expression));
            return parser.topLevelExpression();
        } catch (ParseException e) {
            throw new ExpressionSyntaxException(expression, e);
        } catch (TokenMgrError e) {
            throw new ExpressionSyntaxException(expression, e);
        }
    }

    /**
     * Parses and compiles the given expression using the {@link ognl.enhance.OgnlExpressionCompiler} returned
     * from {@link ognl.OgnlRuntime#getCompiler()}.
     *
     * @param context
     *          The context to use.
     * @param root
     *          The root object for the given expression.
     * @param expression
     *          The expression to compile.
     *
     * @return The node with a compiled accessor set on {@link ognl.Node#getAccessor()} if compilation
     *          was successfull. In instances where compilation wasn't possible because of a partially null
     *          expression the {@link ExpressionAccessor} instance may be null and the compilation of this expression
     *          still possible at some as yet indertermined point in the future.
     *
     * @throws Exception If a compilation error occurs.
     */
    public static Node compileExpression(OgnlContext context, Object root, String expression)
            throws Exception
    {
        Node expr = (Node)Ognl.parseExpression(expression);

        OgnlRuntime.compileExpression(context, expr, root);

        return expr;
    }

    /**
     * Creates and returns a new standard naming context for evaluating an OGNL expression.
     *
     * @param root
     *            the root of the object graph
     * @return a new Map with the keys root and context set
     *         appropriately
     *
     * @deprecated it will be removed soon
     */
    @Deprecated
    public static Map createDefaultContext(Object root)
    {
        return addDefaultContext(root, null, null, null, new OgnlContext(null, null, null));
    }

    /**
     * Creates and returns a new standard naming context for evaluating an OGNL expression.
     *
     * @param root
     *          The root of the object graph.
     * @param classResolver
     *          The resolver used to instantiate {@link Class} instances referenced in the expression.
     *
     * @return a new OgnlContext with the keys root and context set
     *         appropriately
     *
     * @deprecated it will be removed soon
     */
    @Deprecated
    public static Map createDefaultContext(Object root, ClassResolver classResolver)
    {
        return addDefaultContext(root, null, classResolver, null, new OgnlContext(classResolver, null, null));
    }

    /**
     * Creates and returns a new standard naming context for evaluating an OGNL expression.
     *
     * @param root
     *          The root of the object graph.
     * @param classResolver
     *          The resolver used to instantiate {@link Class} instances referenced in the expression.
     * @param converter
     *          Converter used to convert return types of an expression in to their desired types.
     *
     * @return a new Map with the keys root and context set
     *         appropriately
     *
     * @deprecated it will be removed soon
     */
    @Deprecated
    public static Map createDefaultContext(Object root, ClassResolver classResolver, TypeConverter converter)
    {
        return addDefaultContext(root, null, classResolver, converter, new OgnlContext(classResolver, converter, null));
    }

    /**
     * Creates and returns a new standard naming context for evaluating an OGNL expression.
     *
     * @param root
     *          The root of the object graph.
     * @param memberAccess
     *          Java security handling object to determine semantics for accessing normally private/protected
     *          methods / fields.
     * @param classResolver
     *          The resolver used to instantiate {@link Class} instances referenced in the expression.
     * @param converter
     *          Converter used to convert return types of an expression in to their desired types.
     * @return a new Map with the keys root and context set
     *         appropriately
     */
    public static Map createDefaultContext(Object root, MemberAccess memberAccess, ClassResolver classResolver,
                                           TypeConverter converter)
    {
        return addDefaultContext(root, memberAccess, classResolver, converter, new OgnlContext(classResolver, converter, memberAccess));
    }

    /**
     * Creates and returns a new standard naming context for evaluating an OGNL expression.
     *
     * @param root
     *          The root of the object graph.
     * @param memberAccess
     *          Java security handling object to determine semantics for accessing normally private/protected
     *          methods / fields.
     * @return a new Map with the keys root and context set
     *         appropriately
     */
    public static Map createDefaultContext(Object root, MemberAccess memberAccess)
    {
        return addDefaultContext(root, memberAccess, null, null, new OgnlContext(null, null, memberAccess));
    }

    /**
     * Appends the standard naming context for evaluating an OGNL expression into the context given
     * so that cached maps can be used as a context.
     *
     * @param root
     *            the root of the object graph
     * @param context
     *            the context to which OGNL context will be added.
     * @return Context Map with the keys root and context set
     *         appropriately
     * @deprecated will be removed soon
     */
    @Deprecated
    public static Map addDefaultContext(Object root, Map context)
    {
        return addDefaultContext(root, null, null, null, context);
    }

    /**
     * Appends the standard naming context for evaluating an OGNL expression into the context given
     * so that cached maps can be used as a context.
     *
     * @param root
     *          The root of the object graph.
     * @param classResolver
     *          The resolver used to instantiate {@link Class} instances referenced in the expression.
     * @param context
     *          The context to which OGNL context will be added.
     *
     * @return Context Map with the keys root and context set
     *         appropriately
     */
    public static Map addDefaultContext(Object root, ClassResolver classResolver, Map context)
    {
        return addDefaultContext(root, null, classResolver, null, context);
    }

    /**
     * Appends the standard naming context for evaluating an OGNL expression into the context given
     * so that cached maps can be used as a context.
     *
     * @param root
     *          The root of the object graph.
     * @param classResolver
     *          The resolver used to instantiate {@link Class} instances referenced in the expression.
     * @param converter
     *          Converter used to convert return types of an expression in to their desired types.
     * @param context
     *          The context to which OGNL context will be added.
     *
     * @return Context Map with the keys root and context set
     *         appropriately
     */
    public static Map addDefaultContext(Object root, ClassResolver classResolver,
                                        TypeConverter converter, Map context)
    {
        return addDefaultContext(root, null, classResolver, converter, context);
    }

    /**
     * Appends the standard naming context for evaluating an OGNL expression into the context given
     * so that cached maps can be used as a context.
     *
     * @param root
     *            the root of the object graph
     * @param memberAccess
     *              Definition for handling private/protected access.
     * @param classResolver
     *            The class loading resolver that should be used to resolve class references.
     * @param converter
     *            The type converter to be used by default.
     * @param context
     *              Default context to use, if not an {@link OgnlContext} will be dumped into
     *              a new {@link OgnlContext} object.
     * @return Context Map with the keys root and context set
     *         appropriately
     */
    public static Map addDefaultContext(Object root, MemberAccess memberAccess, ClassResolver classResolver,
                                        TypeConverter converter, Map context)
    {
        OgnlContext result;

        if (context instanceof OgnlContext) {
            result = (OgnlContext) context;
        } else {
            result = new OgnlContext(memberAccess, classResolver, converter, context);
        }

        result.setRoot(root);
        return result;
    }

    /**
     * Configures the {@link ClassResolver} to use for the given context.  Will be used during
     * expression parsing / execution to resolve class names.
     *
     * @param context
     *          The context to place the resolver.
     * @param classResolver
     *          The resolver to use to resolve classes.
     * @deprecated it ignores any attempts to modify ClassResolver, ClassResolver can be defined
     *          only when creating a new context
     */
    public static void setClassResolver(Map context, ClassResolver classResolver)
    {
        // noop
    }

    /**
     * Gets the previously stored {@link ClassResolver} for the given context - if any.
     *
     * @param context
     *          The context to get the configured resolver from.
     *
     * @return The resolver instance, or null if none found.
     * @deprecated it always return null, access to class resolver was prohibited
     */
    public static ClassResolver getClassResolver(Map context)
    {
        return null;
    }

    /**
     * Configures the type converter to use for a given context. This will be used
     * to convert into / out of various java class types.
     *
     * @param context
     *          The context to configure it for.
     * @param converter
     *          The converter to use.
     *
     * @deprecated do not use
     */
    @Deprecated
    public static void setTypeConverter(Map context, TypeConverter converter)
    {
        // no-op
    }

    /**
     * Gets the currently configured {@link TypeConverter} for the given context - if any.
     *
     * @param context
     *          The context to get the converter from.
     *
     * @return The converter - or null if none found.
     */
    public static TypeConverter getTypeConverter(Map context)
    {
        if (context instanceof OgnlContext) {
            return ((OgnlContext) context).getTypeConverter();
        }
        return null;
    }

    /**
     * Sets the root object to use for all expressions in the given context - doesn't necessarily replace
     * root object instances explicitly passed in to other expression resolving methods on this class.
     *
     * @param context
     *          The context to store the root object in.
     * @param root
     *          The root object.
     */
    public static void setRoot(Map context, Object root)
    {
        context.put(OgnlContext.ROOT_CONTEXT_KEY, root);
    }

    /**
     * Gets the stored root object for the given context - if any.
     *
     * @param context
     *          The context to get the root object from.
     *
     * @return The root object - or null if none found.
     */
    public static Object getRoot(Map context)
    {
        return context.get(OgnlContext.ROOT_CONTEXT_KEY);
    }

    /**
     * Gets the last {@link Evaluation} executed on the given context.
     *
     * @param context
     *          The context to get the evaluation from.
     *
     * @return The {@link Evaluation} - or null if none was found.
     */
    public static Evaluation getLastEvaluation(Map context)
    {
        return (Evaluation) context.get(OgnlContext.LAST_EVALUATION_CONTEXT_KEY);
    }

    /**
     * Evaluates the given OGNL expression tree to extract a value from the given root object. The
     * default context is set for the given context and root via addDefaultContext().
     *
     * @param tree
     *            the OGNL expression tree to evaluate, as returned by parseExpression()
     * @param context
     *            the naming context for the evaluation
     * @param root
     *            the root object for the OGNL expression
     * @return the result of evaluating the expression
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static Object getValue(Object tree, Map context, Object root)
            throws OgnlException
    {
        return getValue(tree, context, root, null);
    }

    /**
     * Evaluates the given OGNL expression tree to extract a value from the given root object. The
     * default context is set for the given context and root via addDefaultContext().
     *
     * @param tree
     *            the OGNL expression tree to evaluate, as returned by parseExpression()
     * @param context
     *            the naming context for the evaluation
     * @param root
     *            the root object for the OGNL expression
     * @param resultType
     *            the converted type of the resultant object, using the context's type converter
     * @return the result of evaluating the expression
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static Object getValue(Object tree, Map context, Object root, Class resultType)
            throws OgnlException
    {
        Object result;
        OgnlContext ognlContext = (OgnlContext) addDefaultContext(root, context);

        Node node = (Node)tree;

        if (node.getAccessor() != null)
            result =  node.getAccessor().get(ognlContext, root);
        else
            result = node.getValue(ognlContext, root);

        if (resultType != null) {
            result = getTypeConverter(context).convertValue(context, root, null, null, result, resultType);
        }
        return result;
    }

    /**
     * Gets the value represented by the given pre-compiled expression on the specified root
     * object.
     *
     * @param expression
     *          The pre-compiled expression, as found in {@link Node#getAccessor()}.
     * @param context
     *          The ognl context.
     * @param root
     *          The object to retrieve the expression value from.
     * @return
     *          The value.
     */
    public static Object getValue(ExpressionAccessor expression, OgnlContext context, Object root)
    {
        return expression.get(context, root);
    }

    /**
     * Gets the value represented by the given pre-compiled expression on the specified root
     * object.
     *
     * @param expression
     *          The pre-compiled expression, as found in {@link Node#getAccessor()}.
     * @param context
     *          The ognl context.
     * @param root
     *          The object to retrieve the expression value from.
     * @param resultType
     *          The desired object type that the return value should be converted to using the {@link #getTypeConverter(java.util.Map)} }.
     * @return
     *          The value.
     */
    public static Object getValue(ExpressionAccessor expression, OgnlContext context,
                                  Object root, Class resultType)
    {
        return getTypeConverter(context).convertValue(context, root, null, null, expression.get(context, root), resultType);
    }

    /**
     * Evaluates the given OGNL expression to extract a value from the given root object in a given
     * context
     *
     * @see #parseExpression(String)
     * @see #getValue(Object,Object)
     * @param expression
     *            the OGNL expression to be parsed
     * @param context
     *            the naming context for the evaluation
     * @param root
     *            the root object for the OGNL expression
     * @return the result of evaluating the expression
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static Object getValue(String expression, Map context, Object root)
            throws OgnlException
    {
        return getValue(expression, context, root, null);
    }

    /**
     * Evaluates the given OGNL expression to extract a value from the given root object in a given
     * context
     *
     * @see #parseExpression(String)
     * @see #getValue(Object,Object)
     * @param expression
     *            the OGNL expression to be parsed
     * @param context
     *            the naming context for the evaluation
     * @param root
     *            the root object for the OGNL expression
     * @param resultType
     *            the converted type of the resultant object, using the context's type converter
     * @return the result of evaluating the expression
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static Object getValue(String expression, Map context, Object root, Class resultType)
            throws OgnlException
    {
        return getValue(parseExpression(expression), context, root, resultType);
    }

    /**
     * Evaluates the given OGNL expression tree to extract a value from the given root object.
     *
     * @param tree
     *            the OGNL expression tree to evaluate, as returned by parseExpression()
     * @param root
     *            the root object for the OGNL expression
     * @return the result of evaluating the expression
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     *
     * @deprecated it will be removed soon
     */
    @Deprecated
    public static Object getValue(Object tree, Object root)
            throws OgnlException
    {
        return getValue(tree, root, null);
    }

    /**
     * Evaluates the given OGNL expression tree to extract a value from the given root object.
     *
     * @param tree
     *            the OGNL expression tree to evaluate, as returned by parseExpression()
     * @param root
     *            the root object for the OGNL expression
     * @param resultType
     *            the converted type of the resultant object, using the context's type converter
     * @return the result of evaluating the expression
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static Object getValue(Object tree, Object root, Class resultType)
            throws OgnlException
    {
        return getValue(tree, createDefaultContext(root), root, resultType);
    }

    /**
     * Convenience method that combines calls to  parseExpression  and
     *  getValue.
     *
     * @see #parseExpression(String)
     * @see #getValue(Object,Object)
     * @param expression
     *            the OGNL expression to be parsed
     * @param root
     *            the root object for the OGNL expression
     * @return the result of evaluating the expression
     * @throws ExpressionSyntaxException
     *             if the expression is malformed
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static Object getValue(String expression, Object root)
            throws OgnlException
    {
        return getValue(expression, root, null);
    }

    /**
     * Convenience method that combines calls to  parseExpression  and
     *  getValue.
     *
     * @see #parseExpression(String)
     * @see #getValue(Object,Object)
     * @param expression
     *            the OGNL expression to be parsed
     * @param root
     *            the root object for the OGNL expression
     * @param resultType
     *            the converted type of the resultant object, using the context's type converter
     * @return the result of evaluating the expression
     * @throws ExpressionSyntaxException
     *             if the expression is malformed
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static Object getValue(String expression, Object root, Class resultType)
            throws OgnlException
    {
        return getValue(parseExpression(expression), root, resultType);
    }

    /**
     * Evaluates the given OGNL expression tree to insert a value into the object graph rooted at
     * the given root object. The default context is set for the given context and root via addDefaultContext().
     *
     * @param tree
     *            the OGNL expression tree to evaluate, as returned by parseExpression()
     * @param context
     *            the naming context for the evaluation
     * @param root
     *            the root object for the OGNL expression
     * @param value
     *            the value to insert into the object graph
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static void setValue(Object tree, Map context, Object root, Object value)
            throws OgnlException
    {
        OgnlContext ognlContext = (OgnlContext) addDefaultContext(root, context);
        Node n = (Node) tree;

        if (n.getAccessor() != null) {
            n.getAccessor().set(ognlContext, root, value);
            return;
        }

        n.setValue(ognlContext, root, value);
    }

    /**
     * Sets the value given using the pre-compiled expression on the specified root
     * object.
     *
     * @param expression
     *          The pre-compiled expression, as found in {@link Node#getAccessor()}.
     * @param context
     *          The ognl context.
     * @param root
     *          The object to set the expression value on.
     * @param value
     *          The value to set.
     */
    public static void setValue(ExpressionAccessor expression, OgnlContext context,
                                Object root, Object value)
    {
        expression.set(context, root, value);
    }

    /**
     * Evaluates the given OGNL expression to insert a value into the object graph rooted at the
     * given root object given the context.
     *
     * @param expression
     *            the OGNL expression to be parsed
     * @param root
     *            the root object for the OGNL expression
     * @param context
     *            the naming context for the evaluation
     * @param value
     *            the value to insert into the object graph
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static void setValue(String expression, Map context, Object root, Object value)
            throws OgnlException
    {
        setValue(parseExpression(expression), context, root, value);
    }

    /**
     * Evaluates the given OGNL expression tree to insert a value into the object graph rooted at
     * the given root object.
     *
     * @param tree
     *            the OGNL expression tree to evaluate, as returned by parseExpression()
     * @param root
     *            the root object for the OGNL expression
     * @param value
     *            the value to insert into the object graph
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static void setValue(Object tree, Object root, Object value)
            throws OgnlException
    {
        setValue(tree, createDefaultContext(root), root, value);
    }

    /**
     * Convenience method that combines calls to  parseExpression  and
     *  setValue.
     *
     * @see #parseExpression(String)
     * @see #setValue(Object,Object,Object)
     * @param expression
     *            the OGNL expression to be parsed
     * @param root
     *            the root object for the OGNL expression
     * @param value
     *            the value to insert into the object graph
     * @throws ExpressionSyntaxException
     *             if the expression is malformed
     * @throws MethodFailedException
     *             if the expression called a method which failed
     * @throws NoSuchPropertyException
     *             if the expression referred to a nonexistent property
     * @throws InappropriateExpressionException
     *             if the expression can't be used in this context
     * @throws OgnlException
     *             if there is a pathological environmental problem
     */
    public static void setValue(String expression, Object root, Object value)
            throws OgnlException
    {
        setValue(parseExpression(expression), root, value);
    }

    /**
     * Checks if the specified {@link Node} instance represents a constant
     * expression.
     *
     * @param tree
     *          The {@link Node} to check.
     * @param context
     *          The context to use.
     *
     * @return True if the node is a constant - false otherwise.
     * @throws OgnlException  If an error occurs checking the expression.
     */
    public static boolean isConstant(Object tree, Map context)
            throws OgnlException
    {
        return ((SimpleNode) tree).isConstant((OgnlContext) addDefaultContext(null, context));
    }

     /**
     * Checks if the specified expression represents a constant expression.
     *
     * @param expression
     *          The expression to check.
     * @param context
     *          The context to use.
     *
     * @return True if the node is a constant - false otherwise.
     * @throws OgnlException  If an error occurs checking the expression.
     */
    public static boolean isConstant(String expression, Map context)
            throws OgnlException
    {
        return isConstant(parseExpression(expression), context);
    }

    /**
     * Same as {@link #isConstant(Object, java.util.Map)} - only the {@link Map} context
     * is created for you.
     *
     * @param tree
     *          The {@link Node} to check.
     *
     * @return True if the node represents a constant expression - false otherwise.
     * @throws OgnlException If an exception occurs.
     */
    public static boolean isConstant(Object tree)
            throws OgnlException
    {
        return isConstant(tree, createDefaultContext(null));
    }

    /**
     * Same as {@link #isConstant(String, java.util.Map)} - only the {@link Map}
     * instance is created for you.
     *
     * @param expression
     *          The expression to check.
     *
     * @return True if the expression represents a constant - false otherwise.
     * @throws OgnlException If an exception occurs.
     */
    public static boolean isConstant(String expression)
            throws OgnlException
    {
        return isConstant(parseExpression(expression), createDefaultContext(null));
    }

    public static boolean isSimpleProperty(Object tree, Map context)
            throws OgnlException
    {
        return ((SimpleNode) tree).isSimpleProperty((OgnlContext) addDefaultContext(null, context));
    }

    public static boolean isSimpleProperty(String expression, Map context)
            throws OgnlException
    {
        return isSimpleProperty(parseExpression(expression), context);
    }

    public static boolean isSimpleProperty(Object tree)
            throws OgnlException
    {
        return isSimpleProperty(tree, createDefaultContext(null));
    }

    public static boolean isSimpleProperty(String expression)
            throws OgnlException
    {
        return isSimpleProperty(parseExpression(expression), createDefaultContext(null));
    }

    public static boolean isSimpleNavigationChain(Object tree, Map context)
            throws OgnlException
    {
        return ((SimpleNode) tree).isSimpleNavigationChain((OgnlContext) addDefaultContext(null, context));
    }

    public static boolean isSimpleNavigationChain(String expression, Map context)
            throws OgnlException
    {
        return isSimpleNavigationChain(parseExpression(expression), context);
    }

    public static boolean isSimpleNavigationChain(Object tree)
            throws OgnlException
    {
        return isSimpleNavigationChain(tree, createDefaultContext(null));
    }

    public static boolean isSimpleNavigationChain(String expression)
            throws OgnlException
    {
        return isSimpleNavigationChain(parseExpression(expression), createDefaultContext(null));
    }

    /** You can't make one of these. */
    private Ognl()
    {
    }
}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.watcher.api;

import java.util.Locale;

/**
 * Helper class.
 *
 * @author Grzegorz Slowikowski
 */
public class FileWatchServices
{
    /**
     * Returns default watch service identifier based on operating system.
     *
     * @return watch service id
     */
    public static String getDefaultWatchServiceId()
    {
        String result = "polling";
        String osName = System.getProperty( "os.name" );
        if ( osName != null )
        {
            osName = osName.toLowerCase( Locale.ENGLISH );
            if ( osName.contains( "windows" ) || osName.contains( "linux" ) )
            {
                result = isAtLeastJava7() ? "jdk7" : "jnotify";
            }
            else if ( osName.contains( "mac" ) )
            {
                result = "jnotify";
            }
        }
        return result;
    }

    private static boolean isAtLeastJava7()
    {
        boolean result = false;
        try
        {
            Class.forName( "java.nio.file.WatchService" );
            result = true;
        }
        catch ( Throwable t )
        {
            // ignore
        }
        return result;
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.plugin;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.taskdefs.Java;

import com.google.code.play2.provider.api.Play2Provider;
import com.google.code.play2.provider.api.Play2Runner;

/**
 * Base class for Play! server asynchronously starting ("start" and "start-server") mojos.
 *
 * @author Grzegorz Slowikowski
 */
public abstract class AbstractPlay2StartServerMojo
    extends AbstractPlay2ServerMojo
{
    protected Java getStartServerTask( boolean spawn )
        throws MojoExecutionException
    {
        File baseDir = project.getBasedir();

        File pidFile = new File( baseDir, "RUNNING_PID" );
        if ( pidFile.exists() )
        {
            throw new MojoExecutionException( String.format( "Play! Server already started (\"%s\" file found)",
                                                             pidFile.getName() ) );
        }

        Play2Provider play2Provider = getProvider();
        Play2Runner play2Runner = play2Provider.getRunner();

        Java javaTask = prepareAntJavaTask( play2Runner.getServerMainClass() );
        if ( spawn )
        {
            javaTask.setSpawn( true );
        }
        else
        {
            javaTask.setFailonerror( true );
            PidFileDeleter.getInstance().add( pidFile );
        }

        // moving here wasn't good PidFileDeleter.getInstance().add( pidFile );
        // addSystemProperty( javaTask, "pidFile", pidFile.getAbsolutePath() );

        /*if ( logFile != null )
        {
            if ( spawn )
            {
                addSystemProperty( javaTask, "outFile", logFile.getAbsolutePath() );
            }
            else
            {
                javaTask.setOutput( logFile );
            }
        }*/

        return javaTask;
    }

    protected String getRootUrl( String url )
    {
        String result = url; // maybe is already absolute
        if ( !url.startsWith( "http:" ) && !url.startsWith( "https:" ) )
        {
            result = String.format( "http://0.0.0.0:9000%s", url ); // was relative, make absolute one
        }
        return result;
    }

    // startTimeout in milliseconds
    protected void waitForServerStarted( String rootUrl, JavaRunnable runner, int startTimeout, boolean spawned )
        throws MojoExecutionException, IOException
    {
        long endTimeMillis = startTimeout > 0  ? System.currentTimeMillis() + startTimeout : 0L;
        boolean started = false;

        URL connectUrl = new URL( rootUrl );
        int verifyWaitDelay = 1000;
        while ( !started )
        {
            if ( startTimeout > 0 && endTimeMillis - System.currentTimeMillis() < 0L )
            {
                if ( spawned )
                {
                    InternalPlay2StopMojo internalStop = new InternalPlay2StopMojo();
                    internalStop.project = project;
                    try
                    {
                        internalStop.execute();
                    }
                    catch ( MojoExecutionException e )
                    {
                        // just ignore
                    }
                    catch ( MojoFailureException e )
                    {
                        // just ignore
                    }
                }
                throw new MojoExecutionException( String.format( "Failed to start Play! server in %d ms",
                                                                 Integer.valueOf( startTimeout ) ) );
            }

            BuildException runnerException = runner.getException();
            if ( runnerException != null )
            {
                throw new MojoExecutionException( "Play! server start exception", runnerException );
            }

            try
            {
                URLConnection conn = connectUrl.openConnection();
                if ( startTimeout > 0 )
                {
                    int connectTimeOut =
                        Long.valueOf( Math.min( endTimeMillis - System.currentTimeMillis(),
                                                Integer.valueOf( Integer.MAX_VALUE ).longValue() ) ).intValue();
                    if ( connectTimeOut > 0 )
                    {
                        conn.setConnectTimeout( connectTimeOut );
                    }
                }
                connectUrl.openConnection().getContent();
                started = true;
            }
            catch ( Exception e )
            {
                // return false;
            }

            if ( !started )
            {
                long sleepTime = verifyWaitDelay;
                if ( startTimeout > 0 )
                {
                    sleepTime = Math.min( sleepTime, endTimeMillis - System.currentTimeMillis() );
                }
                if ( sleepTime > 0 )
                {
                    try
                    {
                        Thread.sleep( sleepTime );
                    }
                    catch ( InterruptedException e )
                    {
                        throw new MojoExecutionException( "?", e );
                    }
                }
            }
        }
    }

    private static class InternalPlay2StopMojo
        extends AbstractPlay2StopServerMojo
    {
        @Override
        protected void internalExecute()
            throws MojoExecutionException, MojoFailureException, IOException
        {
            stopServer();
        }
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.watcher.jdk7;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import com.google.code.play2.watcher.api.AbstractFileWatchService;
import com.google.code.play2.watcher.api.FileWatcher;
import com.google.code.play2.watcher.api.FileWatchCallback;
import com.google.code.play2.watcher.api.FileWatchException;
import com.google.code.play2.watcher.api.FileWatchService;

import org.codehaus.plexus.component.annotations.Component;

/**
 * JDK7 file watch service.
 */
@Component( role = FileWatchService.class, hint = "jdk7", description = "JDK7" )
public class JDK7FileWatchService
    extends AbstractFileWatchService
{
    /**
     * Creates JDK7 file watch service.
     */
    public JDK7FileWatchService()
    {
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public FileWatcher watch( List<File> filesToWatch, FileWatchCallback watchCallback )
        throws FileWatchException
    {
        List<File> dirsToWatch = new ArrayList<File>( filesToWatch.size() );
        for ( File file: filesToWatch )
        {
            if ( file.exists() )
            {
                if ( file.isDirectory() )
                {
                    dirsToWatch.add( file );
                }
                else
                {
                    if ( log != null && log.isWarnEnabled() )
                    {
                        log.warn( String.format( "[jdk7] \"%s\" is not a directory, will not be watched.",
                                                    file.getAbsolutePath() ) );
                    }
                }
            }
            else
            {
                if ( log != null && log.isWarnEnabled() )
                {
                    log.warn( String.format( "[jdk7] \"%s\" does not exist, will not be watched.",
                                                file.getAbsolutePath() ) );
                }
            }
        }

        try
        {
            JDK7FileWatcher result = new JDK7FileWatcher( log, dirsToWatch, watchCallback );

            Thread thread = new Thread( result, "jdk7-play-watch-service" );
            thread.setDaemon( true );
            thread.start();

            return result;
        }
        catch ( IOException e )
        {
            throw new FileWatchException( "JDK7FileWatcher initialization failed", e );
        }
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.spm.routes;

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

import com.google.code.sbt.compiler.api.SourcePosition;

import com.google.code.play2.spm.AbstractPlay2SourcePositionMapper;

/**
 * Play! Framework routes file source position mapper
 *
 * @author Grzegorz Slowikowski
 */
public class Play2RoutesSourcePositionMapper extends AbstractPlay2SourcePositionMapper
{
    private static final String GENERATOR_LINE = "// @GENERATOR:play-routes-compiler";

    /**
     * Performs mapping from the position in generated source file to the position in {@code routes} file it was
     * generated from.

     *

     * Returns:
     *

     *
position in {@code routes} file

     *
{@code null} value if file of the input position is not recognized as generated from {@code routes} file

     *

     *
     * @param p compilation error/warning position in generated file
     * @return position in {@code routes} file or {@code null} value if file of the input position is not recognized as
     *         generated from {@code routes} file
     * @throws IOException I/O exception during generated or original source file reading
     */
    @Override
    public SourcePosition map( SourcePosition p ) throws IOException
    {
        SourcePosition result = null;

        File generatedFile = p.getFile();
        if ( generatedFile != null && generatedFile.isFile() )
        {
            String[] generatedFileLines = readFileAsString( generatedFile ).split( "\n" );
            if ( generatedFileLines[0].startsWith( Play2RoutesGeneratedSource.SOURCE_PREFIX ) // play 2.2.x - 2.3.x
                || Arrays.asList( generatedFileLines ).contains( GENERATOR_LINE ) ) // play 2.4.x +
            {
                Play2RoutesGeneratedSource generatedSource = new Play2RoutesGeneratedSource( generatedFileLines );
                String sourceFileName = generatedSource.getSourceFileName();
                if ( sourceFileName != null )
                {
                    File sourceFile = new File( sourceFileName );
                    if ( sourceFile.isFile() )
                    {
                        int sourceLine = generatedSource.mapLine( p.getLine() );
                        String[] sourceFileLines = readFileAsString( sourceFile ).split( "\n" );
                        if ( sourceFileLines.length >= sourceLine )
                        {
                            String sourceLineContent = sourceFileLines[sourceLine - 1];
                            if ( sourceLineContent.endsWith( "\r" ) ) // remove trailing CR (on Windows)
                            {
                                sourceLineContent = sourceLineContent.substring( 0, sourceLineContent.length() - 1 );
                            }
                            result = new Play2RoutesSourcePosition( sourceLine, sourceLineContent, sourceFile );
                        }
                    }
                }
            }
        }
        return result;
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.provider.api;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Play2BuildFailure
    extends Exception
{
    private static final long serialVersionUID = 1L;

    private Play2BuildException e;

    private String charsetName;

    public Play2BuildFailure( Play2BuildException e, String charsetName )
    {
        //?super( e );
        this.e = e;
        this.charsetName = charsetName;
    }

    /**
     * Error message, if defined.
     *
     * @return exception message
     */
    @Override
    public String getMessage()
    {
        return filterAnnoyingErrorMessages( e.getMessage() );
    }

    /**
     * Error line number, if defined.
     *
     * @return exception line number
     */
    public int line()
    {
        return e.line();
    }

    /**
     * Column position, if defined.
     *
     * @return exception column position
     */
    public int position()
    {
        return e.position();
    }

    /**
     * Source file.
     *
     * @return exception source file
     */
    public File source()
    {
        return e.source();
    }

    public String input()
    {
        String result = null;
        File sourceFile = e.source();
        if ( sourceFile != null )
        {
            try
            {
                result = readFileAsString();
            }
            catch ( IOException e )
            {
                // ignore
            }
        }
        return result;
    }

    /**
     * Reads the content of the file to a string.
     *
     * @param file a file to read the content of
     * @return the content of the file
     * @throws IOException I/O exception when reading from the file
     */
    // copied from AbstractPlay2SourcePositionMapper.java
    private String readFileAsString()
        throws IOException
    {
        FileInputStream is = new FileInputStream( e.source() );
        try
        {
            byte[] buffer = new byte[8192];
            int len = is.read( buffer );
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            while ( len != -1 )
            {
                out.write( buffer, 0, len );
                len = is.read( buffer );
            }
            return charsetName != null ? new String( out.toByteArray(), charsetName ) : new String( out.toByteArray() );
        }
        finally
        {
            is.close();
        }
    }

    private String filterAnnoyingErrorMessages( String message )
    {
        String result = message;
        Pattern pattern =
            Pattern.compile( "(?s)overloaded method value (.*) with alternatives:(.*)cannot be applied to(.*)" );
        Matcher matcher = pattern.matcher( message );
        if ( matcher.find() )
        {
            String method = matcher.group( 1 );
            String signature = matcher.group( 3 );
            result = String.format( "Overloaded method value [%s] cannot be applied to %s", method, signature );
        }
        return result;
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.provider.play27.run;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.List;
import java.util.Map;

import play.core.BuildLink;

import com.google.code.play2.provider.api.Play2Builder;
import com.google.code.play2.provider.api.Play2BuildError;
import com.google.code.play2.provider.api.Play2BuildFailure;

public class Reloader implements BuildLink
{
    private Play2Builder buildLink;

    private ClassLoader baseLoader;

    private File projectPath;

    private List<File> outputDirectories;

    private Map<String, String> devSettings;

    private volatile ClassLoader currentApplicationClassLoader = null;

    private int classLoaderVersion = 0;

    public Reloader( Play2Builder buildLink, ClassLoader baseLoader, File projectPath,
                     List<File> outputDirectories, Map<String, String> devSettings )
    {
        this.buildLink = buildLink;
        this.baseLoader = baseLoader;
        this.projectPath = projectPath;
        this.outputDirectories = outputDirectories;
        this.devSettings = devSettings;
    }

    /**
     * Contrary to its name, this doesn't necessarily reload the app.  It is invoked on every request, and will only
     * trigger a reload of the app if something has changed.
     *
     * Since this communicates across classloaders, it must return only simple objects.
     *
     *
     * @return Either
     * - Throwable - If something went wrong (eg, a compile error).
     * - ClassLoader - If the classloader has changed, and the application should be reloaded.
     * - null - If nothing changed.
     */
    @Override /* BuildLink interface */
    public synchronized Object reload()
    {
        Object result = null;
        try
        {
            boolean reloadRequired = buildLink.build();

            if ( reloadRequired )
            {
                int version = ++classLoaderVersion;
                String name = "ReloadableClassLoader(v" + version + ")";
                currentApplicationClassLoader =
                    new DelegatedResourcesClassLoader( name, toUrls( outputDirectories ), baseLoader );
                result = currentApplicationClassLoader;
            }
        }
        catch ( MalformedURLException e )
        {
            throw new UnexpectedException( "Unexpected reloader exception", e ); //??
        }
        catch ( Play2BuildFailure e )
        {
            result =
                new CompilationException( e.getMessage(), e.line(), e.position(),
                                          e.source() != null ? e.source().getAbsolutePath() : null, e.input() );
        }
        catch ( Play2BuildError e )
        {
            result = new UnexpectedException( e.getMessage(), e.getCause() ); //??
        }

        return result;
    }

    @Override /* BuildLink interface */
    // Called by DevServerStart.mainDev() (play-server) L:65
    public Map<String, String> settings()
    {
        return devSettings;
    }

    @Override /* BuildLink interface */
    // Called by EvolutionsWebCommands.<init> (play-jdbc-evolutions) L:367, 375
    public void forceReload()
    {
        buildLink.forceReload();
    }

    @Override /* BuildLink interface */
    // Called by DevServerStart.mainDev() (play-server) L:144
    public Object[] findSource( String className, Integer line )
    {
        return buildLink.findSource( className, line );
    }

    @Override /* BuildLink interface */
    // Called by DevServerStart.mainDev() (play-server) L:63
    public File projectPath()
    {
        return projectPath;
    }

    void close()
    {
        currentApplicationClassLoader = null;
    }

    ClassLoader getClassLoader()
    {
        return currentApplicationClassLoader;
    }

    public static URL[] toUrls( List<File> cp ) throws MalformedURLException
    {
        URL[] result = new URL[cp.size()];
        for ( int i = 0; i < cp.size(); i++ )
        {
            File file = cp.get( i );
            result[i] = file.toURI().toURL();
        }
        return result;
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.provider.play23;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.ArrayList;
//import java.util.Collections;
import java.util.List;

import org.mozilla.javascript.Context;
import org.mozilla.javascript.Function;
import org.mozilla.javascript.JavaScriptException;
import org.mozilla.javascript.NativeArray;
import org.mozilla.javascript.NativeJavaObject;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
import org.mozilla.javascript.tools.shell.Global;

import com.google.code.play2.provider.api.AssetCompilationException;
import com.google.code.play2.provider.api.LessCompilationResult;
import com.google.code.play2.provider.api.Play2LessCompiler;

// Based on Play! 2.3.x framework/src/sbt-plugin/src/main/scala/less/LessCompiler.scala
public class Play23LessCompiler
    implements Play2LessCompiler
{
    public static final String LESS_SCRIPT = "less-1.4.2.js";

//    private List<String> compilerOptions = Collections.emptyList();

    @Override
    public void setCompilerOptions( List<String> compilerOptions )
    {
//        this.compilerOptions = compilerOptions;
    }

    @Override
    public LessCompilationResult compile( File source )
        throws AssetCompilationException, IOException
    {
        try
        {
            InternalCompileResult result1 = compile( source, false );
            InternalCompileResult result2 = compile( source, true );
            return new CompileResult( result1.css, result2.css, result1.dependencies );
        }
        catch ( JavaScriptException e )
        {
            Scriptable error = (Scriptable) e.getValue();
            String filename = ScriptableObject.getProperty( error, "filename" ).toString();
            File file = new File( filename );
            throw new AssetCompilationException(
                                                 e,
                                                 file,
                                                 ScriptableObject.getProperty( error, "message" ).toString(),
                                                 ( (Double) ScriptableObject.getProperty( error, "line" ) ).intValue(),
                                                 ( (Double) ScriptableObject.getProperty( error, "column" ) ).intValue() );
        }
    }

    private String multiLineString( String[] lines )
    {
        StringBuilder sb = new StringBuilder();
        if ( lines != null && lines.length > 0 )
        {
            sb.append( lines[0] );
            for ( int i = 1; i < lines.length; i++ )
            {
                sb.append( "\n" );
                sb.append( lines[i] );
            }
        }
        return sb.toString();
    }

    private InternalCompileResult compile( File source, boolean minify )
        throws IOException
    {
        Context ctx = Context.enter();
        Global global = new Global();
        global.init( ctx );
        Scriptable scope = ctx.initStandardObjects( global );

        Object wrappedLessCompiler = Context.javaToJS( this, scope );
        ScriptableObject.putProperty( scope, "LessCompiler", wrappedLessCompiler );

        ctx.evaluateString( scope, multiLineString( new String[] {
            "",
            "                var timers = [],",
            "                    window = {",
            "                        document: {",
            "                            getElementById: function(id) { ",
            "                                return [];",
            "                            },",
            "                            getElementsByTagName: function(tagName) {",
            "                                return [];",
            "                            }",
            "                        },",
            "                        location: {",
            "                            protocol: 'file:', ",
            "                            hostname: 'localhost', ",
            "                            port: '80'",
            "                        },",
            "                        setInterval: function(fn, time) {",
            "                            var num = timers.length;",
            "                            timers[num] = fn.call(this, null);",
            "                            return num;",
            "                        }",
            "                    },",
            "                    document = window.document,",
            "                    location = window.location,",
            "                    setInterval = window.setInterval;",
            "",
            "            " } ), "browser.js", 1, null );
        ctx.evaluateReader( scope,
                            new InputStreamReader(
                                                   getClass().getClassLoader().getResource( LESS_SCRIPT ).openConnection().getInputStream(),
                                                   "UTF-8" ), LESS_SCRIPT, 1, null );
        ctx.evaluateString( scope,
                            multiLineString( new String[] {
                                "                var compile = function(source) {",
                                "",
                                "                    var compiled;",
                                "                    // Import tree context",
                                "                    var context = [source];",
                                "                    var dependencies = [source];",
                                "",
                                "                    window.less.Parser.importer = function(path, paths, fn, env) {",
                                "",
                                "                        var imported = LessCompiler.resolve(context[context.length - 1], path);",
                                "                        var importedName = String(imported.getCanonicalPath());",
                                "                        try {",
                                "                          var input = String(LessCompiler.readContent(imported));",
                                "                        } catch (e) {",
                                "                          return fn({ type: \"File\", message: \"File not found: \" + importedName });",
                                "                        }",
                                "",
                                "                        // Store it in the contents, for error reporting",
                                "                        env.contents[importedName] = input;",
                                "",
                                "                        context.push(imported);",
                                "                        dependencies.push(imported)",
                                "",
                                "                        new(window.less.Parser)({",
                                "                            optimization:3,",
                                "                            filename:importedName,",
                                "                            contents:env.contents,",
                                "                            dumpLineNumbers:window.less.dumpLineNumbers",
                                "                        }).parse(input, function (e, root) {",
                                "                            fn(e, root, input);",
                                "",
                                "                            context.pop();",
                                "                        });",
                                "                    }",
                                "",
                                "                    new(window.less.Parser)({optimization:3, filename:String(source.getCanonicalPath())}).parse(String(LessCompiler.readContent(source)), function (e,root) {",
                                "                        if (e) {",
                                "                            throw e;",
                                "                        }",
                                "                        compiled = root.toCSS({compress: " + ( minify ? "true" : "false" ) + "})",
                                "                    })",
                                "",
                                "                    return {css:compiled, dependencies:dependencies}",
                                "                }",
                                "            " } ), "compiler.js", 1, null );
        Function compilerFunction = (Function) scope.get( "compile", scope );

        Context.exit();

        Scriptable result = (Scriptable) Context.call( null, compilerFunction, scope, scope, new Object[] { source } );
        String css = ScriptableObject.getProperty( result, "css" ).toString();
        NativeArray dependencies = (NativeArray) ScriptableObject.getProperty( result, "dependencies" );

        int dependenciesCount = Long.valueOf( dependencies.getLength() ).intValue();
        List<File> deps = new ArrayList<File>( dependenciesCount );
        for ( int i = 0; i < dependenciesCount; i++ )
        {
            Object dependency = ScriptableObject.getProperty( dependencies, i );
            if ( dependency instanceof File )
            {
                deps.add( ( (File) dependency ).getCanonicalFile() );
            }
            else if ( dependency instanceof NativeJavaObject )
            {
                Object x = ( (NativeJavaObject) dependency ).unwrap();
                deps.add( ( (File) x ).getCanonicalFile() );
            }
        }

        return new InternalCompileResult( css, deps );

    }

    // Called from Less script, must be "public"
    public static String readContent( File file )
        throws IOException
    {
        String result = null;

        BufferedReader is = new BufferedReader( new InputStreamReader( new FileInputStream( file ), "UTF-8" ) );
        try
        {
            StringBuilder sb = new StringBuilder();
            String line = is.readLine();
            while ( line != null )
            {
                sb.append( line ).append( '\n' );
                line = is.readLine();
            }
            result = sb.toString();
        }
        finally
        {
            is.close();
        }
        return result;
    }

    // Called from Less script, must be "public"
    public static File resolve( File originalSource, String imported )
    {
        return new File( originalSource.getParentFile(), imported );
    }

    private static class InternalCompileResult
    {
        private String css;

        private List<File> dependencies;

        InternalCompileResult( String css, List<File> dependencies )
        {
            this.css = css;
            this.dependencies = dependencies;
        }

        public String getCss()
        {
            return css;
        }

        public List<File> getDependencies()
        {
            return dependencies;
        }
    }

    public static class CompileResult
        extends InternalCompileResult
        implements LessCompilationResult
    {
        private String minifiedCss;

        public CompileResult( String css, String minifiedCss, List<File> dependencies )
        {
            super( css, dependencies );
            this.minifiedCss = minifiedCss;
        }

        @Override
        public String getMinifiedCss()
        {
            return minifiedCss;
        }
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.plugin;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;

import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;

import com.google.code.play2.provider.api.Play2EbeanEnhancer;
import com.google.code.play2.provider.api.Play2Provider;

import com.google.code.sbt.compiler.api.Analysis;
import com.google.code.sbt.compiler.api.AnalysisProcessor;

/**
 * Enhence Ebean entity classes
 *
 * @author Grzegorz Slowikowski
 * @since 1.0.0
 */
@Mojo( name = "ebean-enhance", defaultPhase = LifecyclePhase.PROCESS_CLASSES, requiresDependencyResolution = ResolutionScope.COMPILE )
public class Play2EbeanEnhanceMojo
    extends AbstractPlay2EnhanceMojo
{
    /**
     * ...
     *
     * @since 1.0.0
     */
    @Parameter( property = "play2.ebeanModels" )
    private String ebeanModels;

    /**
     * Project classpath.
     */
    @Parameter( property = "project.compileClasspathElements", readonly = true, required = true )
    private List<String> classpathElements;

    @Override
    protected void internalExecute()
        throws MojoExecutionException, MojoFailureException, IOException
    {
        File outputDirectory = new File( project.getBuild().getOutputDirectory() );

        File timestampFile = new File( getAnalysisCacheFile().getParentFile(), "play_ebean_instrumentation" );
        long lastEnhanced = 0L;
        if ( timestampFile.exists() )
        {
            String line = readFileFirstLine( timestampFile, "ASCII" );
            lastEnhanced = Long.parseLong( line );
        }

        int processedFiles = 0;
        int enhancedFiles = 0;

        classpathElements.remove( outputDirectory.getAbsolutePath() );
        List<File> classpathFiles = new ArrayList<File>( classpathElements.size() );
        for ( String path : classpathElements )
        {
            classpathFiles.add( new File( path ) );
        }

        List<URL> classPathUrls = new ArrayList<URL>( classpathFiles.size() + 1 );
        for ( File classpathFile : classpathFiles )
        {
            classPathUrls.add( classpathFile.toURI().toURL() );
        }
        classPathUrls.add( outputDirectory.toURI().toURL() );

        Play2Provider play2Provider = getProvider();
        Play2EbeanEnhancer enhancer = play2Provider.getEbeanEnhancer();
        enhancer.setOutputDirectory( outputDirectory );
        enhancer.setClassPathUrls( classPathUrls );
        String models = this.ebeanModels;
        if ( models == null || "".equals( models ) )
        {
            models = enhancer.getModelsToEnhance();
        }

        List<File> modelClassesToEnhance = collectClassFilesToEnhance( lastEnhanced, outputDirectory, models );
        if ( !modelClassesToEnhance.isEmpty() )
        {
            File analysisCacheFile = getAnalysisCacheFile();
            if ( !analysisCacheFile.exists() )
            {
                throw new MojoExecutionException( String.format( "Analysis cache file \"%s\" not found",
                                                                 analysisCacheFile.getAbsolutePath() ) );
            }
            if ( !analysisCacheFile.isFile() )
            {
                throw new MojoExecutionException( String.format( "Analysis cache \"%s\" is not a file",
                                                                 analysisCacheFile.getAbsolutePath() ) );
            }
            Analysis analysis = null;
            AnalysisProcessor sbtAnalysisProcessor = getSbtAnalysisProcessor();
            if ( sbtAnalysisProcessor.areClassFileTimestampsSupported() )
            {
                analysis = sbtAnalysisProcessor.readFromFile( analysisCacheFile );
            }

            for ( File classFile: modelClassesToEnhance )
            {
                processedFiles++;
                try
                {
                    if ( enhancer.enhanceModel( classFile ) )
                    {
                        enhancedFiles++;
                        getLog().debug( String.format( "\"%s\" enhanced", classFile.getPath() ) );
                        if ( analysis != null )
                        {
                            analysis.updateClassFileTimestamp( classFile );
                        }
                    }
                    else
                    {
                        getLog().debug( String.format( "\"%s\" skipped", classFile.getPath() ) );
                    }
                }
                catch ( Exception e )
                {
                    //??
                }
            }

            if ( enhancedFiles > 0 )
            {
                if ( analysis != null )
                {
                    analysis.writeToFile( analysisCacheFile );
                }
                writeToFile( timestampFile, "ASCII", Long.toString( System.currentTimeMillis() ) );
            }
        }

        if ( processedFiles > 0 )
        {
            getLog().info( String.format( "%d Ebean %s processed, %d enhanced", Integer.valueOf( processedFiles ),
                                          processedFiles > 1 ? "classes" : "class", Integer.valueOf( enhancedFiles ) ) );
        }
        else
        {
            getLog().info( "No Ebean classes to enhance" );
        }
    }

    /**
     * Process all the comma delimited list of packages.
     *

     * Package names are effectively converted into a directory on the file
     * system, and the class files are found and processed.
     *

     *
     * @param lastEnhanced previous enhancement timestamp
     * @param outputDirectory directory containing classes to enhance
     * @param packageNames package names include filter
     * @return list of files to enhance
     */
    public List<File> collectClassFilesToEnhance( long lastEnhanced, File outputDirectory, String packageNames )
    {
        if ( packageNames == null || packageNames.isEmpty() )
        {
            return collectClassFilesToEnhanceFromPackage( lastEnhanced, outputDirectory, "", true );
            // return;
        }

        List<File> result = new ArrayList<File>();

        String[] pkgs = packageNames.split( "," );
        for ( int i = 0; i < pkgs.length; i++ )
        {

            String pkg = pkgs[i].trim().replace( '.', '/' );

            boolean recurse = false;
            if ( pkg.endsWith( "**" ) )
            {
                recurse = true;
                pkg = pkg.substring( 0, pkg.length() - 2 );
            }
            else if ( pkg.endsWith( "*" ) )
            {
                recurse = true;
                pkg = pkg.substring( 0, pkg.length() - 1 );
            }

            pkg = trimSlash( pkg );

            result.addAll( collectClassFilesToEnhanceFromPackage( lastEnhanced, outputDirectory, pkg, recurse ) );
        }
        return result;
    }

    private List<File> collectClassFilesToEnhanceFromPackage( long lastEnhanced, File outputDirectory, String dir,
                                                              boolean recurse )
    {
        List<File> result = new ArrayList<File>();

        File d = new File( outputDirectory.getAbsolutePath(), dir );
        if ( !d.exists() )
        {
            getLog().warn( String.format( "\"%s\" directory does not exist", d.getPath() ) );
        }
        else if ( !d.isDirectory() )
        {
            getLog().warn( String.format( "\"%s\" is not a directory", d.getPath() ) );
        }
        else
        {
            for ( File file: d.listFiles() )
            {
                if ( file.isDirectory() )
                {
                    if ( recurse )
                    {
                        String subdir = dir + "/" + file.getName();
                        result.addAll( collectClassFilesToEnhanceFromPackage( lastEnhanced, outputDirectory, subdir,
                                                                              recurse ) );
                    }
                }
                else
                {
                    if ( file.getName().endsWith( ".class" ) && ( file.lastModified() > lastEnhanced ) )
                    {
                        result.add( file );
                        // transformFile(file);
                    }
                }
            }
        }
        return result;
    }

    private String trimSlash( String dir )
    {
        String result = dir;
        if ( dir.endsWith( "/" ) )
        {
            result = dir.substring( 0, dir.length() - 1 );
        }
        return result;
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.plugin;

import org.apache.maven.plugins.annotations.Component;

import org.codehaus.plexus.archiver.Archiver;
import org.codehaus.plexus.archiver.ResourceIterator;
import org.codehaus.plexus.archiver.manager.ArchiverManager;
import org.codehaus.plexus.archiver.manager.NoSuchArchiverException;

/**
 * Base class for Play! packaging mojos.
 *
 * @author Grzegorz Slowikowski
 */
public abstract class AbstractArchivingMojo
    extends AbstractPlay2Mojo
{

    /**
     * To look up Archiver/UnArchiver implementations.
     */
    @Component
    private ArchiverManager archiverManager;

    /**
     * Returns preconfigured archiver
     *
     * @param archiverName archiver name
     * @return archiver
     * @throws NoSuchArchiverException for unsupported archiver name
     */
    protected Archiver getArchiver( String archiverName )
        throws NoSuchArchiverException
    {
        Archiver result = archiverManager.getArchiver( archiverName );
        result.setDuplicateBehavior( Archiver.DUPLICATES_FAIL ); // Just in case
        return result;
    }

    /**
     * Check for potential "Duplicate file" exception before archive processing starts
     *
     * @param archiver archiver to check
     */
    protected void checkArchiverForProblems( Archiver archiver )
    {
        for ( ResourceIterator iter = archiver.getResources(); iter.hasNext(); )
        {
            iter.next();
        }
    }

}


/*
 * Copyright 2013-2019 Grzegorz Slowikowski (gslowikowski at gmail dot com)
 *
 * 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 com.google.code.play2.plugin;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.repository.ArtifactRepository;
import org.apache.maven.artifact.resolver.ArtifactResolutionException;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.artifact.resolver.ResolutionErrorHandler;
import org.apache.maven.execution.MavenSession;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.apache.maven.repository.RepositorySystem;

import org.codehaus.plexus.util.FileUtils;

import com.google.code.play2.provider.api.Play2Provider;
import com.google.code.play2.provider.api.Play2Providers;

/**
 * Abstract base class for Play! Mojos.
 *
 * @author Grzegorz Slowikowski
 */
public abstract class AbstractPlay2Mojo
    extends AbstractMojo
{
    /**
     * Used to automatically select one of the "well known" Play! providers if no provider added explicitly as plugin's dependency.
     *
     * @since 1.0.0
     */
    @Parameter( property = "play2.version" )
    protected String playVersion;

    /**
     * Maven Internal: Project to interact with.
     */
    @Parameter( defaultValue = "${project}", readonly = true, required = true )
    protected MavenProject project; // == session.getCurrentProject()

    /**
     * The current build session instance.
     */
    @Parameter( defaultValue = "${session}", readonly = true, required = true )
    protected MavenSession session;

    /**
     * Location of the local repository.
     */
    @Parameter( property = "localRepository", readonly = true, required = true )
    protected ArtifactRepository localRepo;

    /**
     * Remote repositories used by the resolver
     */
    @Parameter( property = "project.remoteArtifactRepositories", readonly = true, required = true )
    protected List<ArtifactRepository> remoteRepos;

    /**
     * ...
     */
    @Component
    protected RepositorySystem repositorySystem;

    /**
     * ...
     */
    @Component
    protected ResolutionErrorHandler resolutionErrorHandler;

    /**
     * Plugin's groupId used for well known providers resolution
     */
    @Parameter( property = "plugin.groupId", readonly = true, required = true )
    protected String pluginGroupId;

    /**
     * Plugin's version used for well known providers resolution
     */
    @Parameter( property = "plugin.version", readonly = true, required = true )
    protected String pluginVersion;

    /**
     * Map of provider implementations. For now only zero or one allowed.
     */
    @Component( role = Play2Provider.class )
    private Map<String, Play2Provider> play2Providers;

    @Override
    public void execute()
        throws MojoExecutionException, MojoFailureException
    {
        if ( !"pom".equals( project.getPackaging() ) )
        {
            try
            {
                long ts = System.currentTimeMillis();
                internalExecute();
                long te = System.currentTimeMillis();
                getLog().debug( String.format( "Mojo execution time: %d ms", Long.valueOf( te - ts ) ) );
            }
            catch ( IOException e )
            {
                throw new MojoExecutionException( "?", e );
            }
        }
    }

    protected abstract void internalExecute()
        throws MojoExecutionException, MojoFailureException, IOException;

    protected final BufferedReader createBufferedFileReader( File file, String encoding )
        throws FileNotFoundException, UnsupportedEncodingException
    {
        FileInputStream fis = new FileInputStream( file );
        InputStreamReader isr = encoding != null ? new InputStreamReader( fis, encoding ) : new InputStreamReader( fis );
        return new BufferedReader( isr );
    }

    protected final BufferedWriter createBufferedFileWriter( File file, String encoding )
        throws FileNotFoundException, UnsupportedEncodingException
    {
        FileOutputStream fos = new FileOutputStream( file );
        OutputStreamWriter osw = encoding != null ? new OutputStreamWriter( fos, encoding ) : new OutputStreamWriter( fos );
        return new BufferedWriter( osw );
    }

    protected String readFileFirstLine( File file, String encoding )
        throws IOException
    {
        String result = null;

        BufferedReader is = createBufferedFileReader( file, encoding );
        try
        {
            result = is.readLine();
        }
        finally
        {
            is.close();
        }
        return result;
    }

    protected void writeToFile( File file, String encoding, String line )
        throws IOException
    {
        BufferedWriter writer = createBufferedFileWriter( file, encoding );
        try
        {
            writer.write( line );
        }
        finally
        {
            writer.flush();
            writer.close();
        }
    }

    protected void createDirectory( File directory, boolean overwrite )
        throws IOException
    {
        if ( directory.exists() )
        {
            if ( directory.isDirectory() )
            {
                if ( overwrite )
                {
                    FileUtils.cleanDirectory( directory );
                }
            }
            else
            // file if ( directory.isFile() )
            {
                getLog().info( String.format( "Deleting \"%s\" file", directory ) ); // TODO-more descriptive message
                if ( !directory.delete() )
                {
                    throw new IOException( String.format( "Cannot delete \"%s\" file", directory.getCanonicalPath() ) );
                }
            }
        }

        if ( !directory.exists() )
        {
            if ( !directory.mkdirs() )
            {
                throw new IOException( String.format( "Cannot create \"%s\" directory", directory.getCanonicalPath() ) );
            }
        }
    }

    // Cached classloaders
    private static final ConcurrentHashMap<String, ClassLoader> CACHED_CLASS_LOADERS =
        new ConcurrentHashMap<String, ClassLoader>( 2 );

    private static ClassLoader getCachedClassLoader( String providerId )
    {
        return CACHED_CLASS_LOADERS.get( providerId );
    }

    private static void setCachedClassLoader( String providerId, ClassLoader classLoader )
    {
        CACHED_CLASS_LOADERS.put( providerId, classLoader );
    }

    protected Play2Provider getProvider()
        throws MojoExecutionException
    {
        Play2Provider provider = null;
        if ( !play2Providers.isEmpty() )
        {
            provider = getDeclaredProvider();
        }
        else
        {
            provider = getWellKnownProvider();
        }
        return provider;
    }

    private Play2Provider getDeclaredProvider()
        throws MojoExecutionException
    {
        if ( play2Providers.size() > 1 )
        {
            throw new MojoExecutionException( "Too many Play! providers defined. A maximum of one allowed." );
        }

        Map.Entry<String, Play2Provider> providerEntry = play2Providers.entrySet().iterator().next();
        String providerId = providerEntry.getKey();
        Play2Provider provider = providerEntry.getValue();

        getLog().debug( String.format( "Using declared provider \"%s\".", providerId ) );

        return provider;
    }

    private Play2Provider getWellKnownProvider()
        throws MojoExecutionException
    {
        try
        {
            String providerId = Play2Providers.getDefaultProviderId( playVersion );
            ClassLoader providerClassLoader = getCachedClassLoader( providerId );
            if ( providerClassLoader == null )
            {
                getLog().debug( String.format( "Cached classloader for provider \"%s\" not available.", providerId ) );
            }
            else
            {
                if ( providerClassLoader.getParent() == Thread.currentThread().getContextClassLoader() )
                {
                    getLog().debug( String.format( "Using cached classloader for provider \"%s\".", providerId ) );
                }
                else
                {
                    getLog().debug( String.format( "Invalidated cached classloader for provider \"%s\". Parent classloader changed from %d to %d.",
                                                   providerId,
                                                   Integer.valueOf( providerClassLoader.getParent().hashCode() ),
                                                   Integer.valueOf( Thread.currentThread().getContextClassLoader().hashCode() ) ) );
                    providerClassLoader = null;
                }
            }

            if ( providerClassLoader == null )
            {
                Set<Artifact> providerArtifacts =
                    getResolvedArtifact( pluginGroupId, "play2-provider-" + providerId, pluginVersion );
                List<File> classPathFiles = new ArrayList<File>( providerArtifacts.size()/*providerDependencies.size() + 2*/ );
                for ( Artifact dependencyArtifact : providerArtifacts/*providerDependencies*/ )
                {
                    classPathFiles.add( dependencyArtifact.getFile() );
                }

                List<URL> classPathUrls = new ArrayList<URL>( classPathFiles.size() );
                for ( File classPathFile : classPathFiles )
                {
                    classPathUrls.add( new URL( classPathFile.toURI().toASCIIString() ) );
                }

                providerClassLoader =
                    new URLClassLoader( classPathUrls.toArray( new URL[classPathUrls.size()] ),
                                        Thread.currentThread().getContextClassLoader() );
                getLog().debug( String.format( "Setting cached classloader for provider \"%s\" with parent classloader %d",
                                               providerId, Integer.valueOf( providerClassLoader.getParent().hashCode() ) ) );
                setCachedClassLoader( providerId, providerClassLoader );
            }

            ServiceLoader<Play2Provider> providerServiceLoader =
                ServiceLoader.load( Play2Provider.class, providerClassLoader );
            // get first (there should be exactly one)
            Play2Provider provider = providerServiceLoader.iterator().next();

            getLog().debug( String.format( "Using autodetected provider \"%s\".", providerId ) );

            return provider;
        }
        catch ( ArtifactResolutionException e )
        {
            throw new MojoExecutionException( "Provider autodetection failed", e );
        }
        catch ( MalformedURLException e )
        {
            throw new MojoExecutionException( "Provider autodetection failed", e );
        }
    }

    // Private utility methods

    protected/*private*/ Set<Artifact> getResolvedArtifact( String groupId, String artifactId, String version )
        throws ArtifactResolutionException
    {
        return getResolvedArtifact( groupId, artifactId, version, true );
    }

    protected/*private*/ Set<Artifact> getResolvedArtifact( String groupId, String artifactId, String version,
                                                             boolean transitively )
        throws ArtifactResolutionException
    {
        Artifact artifact =
            repositorySystem.createArtifact( groupId, artifactId, version, Artifact.SCOPE_RUNTIME, "jar" );
        return getResolvedArtifact( artifact, transitively );
    }

    protected/*private*/ Set<Artifact> getResolvedArtifact( Artifact artifact, boolean transitively )
        throws ArtifactResolutionException
    {
        ArtifactResolutionRequest request =
            new ArtifactResolutionRequest().setArtifact( artifact ).setLocalRepository( localRepo ).setRemoteRepositories( remoteRepos ).setResolveTransitively( transitively );
        ArtifactResolutionResult result = repositorySystem.resolve( request );
        resolutionErrorHandler.throwErrors( request, result );
        return result.getArtifacts();
    }

    protected boolean isMatchingProject( MavenProject p, String selector )
    {
        boolean result;
        if ( !selector.contains( ":" ) ) // artifactId
        {
            result = selector.equals( p.getArtifactId() );
        }
        else if ( selector.startsWith( ":" ) ) // :artifactId
        {
            result = selector.equals( ":" + p.getArtifactId() );
        }
        else // groupId:artifactId
        {
            result = selector.equals( p.getGroupId() + ":" + p.getArtifactId() );
        }
        return result;
    }

}