/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2012 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package javax.el;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

/**
 * <p>Provides an API for using EL in a stand-alone environment.</p>
 * 
 * <p>This class provides a direct and simple interface for
 * <ul>
 *   <li>Evaluating EL expressions.</li>
 *   <li>Assigning values to beans or setting a bean property.</li>
 *   <li>Setting a {@link ValueExpression} to a EL variable.</li>
 *   <li>Defining a static method as an EL function.</li>
 *   <li>Defining an object instance as an EL name.
 * </ul>
 * 
 * <p>This API is not a replacement for the APIs in EL 2.2.  Containers that
 * maintains EL environments can continue to do so, without using this API.</p>
 * 
 * <p>For EL users who want to manipulate EL environments, like adding custom
 * {@link ELResolver}s, {@link ELManager} can be used.</p>
 *
 * <h3>Scope and Life Cycle</h3>
 * <p>Since it maintains the state of the EL environments,
 * <code>ELProcessor</code> is not thread safe.  In the simplest case,
 * an instance can be created and destroyed before and after evaluating
 * EL expressions.  A more general usage is to use an instance of
 * <code>ELProcessor</code> for a session, so that the user can configure the
 * EL evaluation environment for that session.</p>
 *
 * <h3>Automatic Bracketing of Expressions</h3>
 * <p>A note about the EL expressions strings used in the class.  The strings
 * allowed in the methods {@link ELProcessor#getValue},
 * {@link ELProcessor#setValue}, and {@link ELProcessor#setVariable} are
 * limited to non-composite expressions, i.e. expressions
 * of the form ${...} or #{...} only.  Also, it is not necessary (in fact not
 * allowed) to bracket the expression strings with ${ or #{ and } in these
 * methods: they will be automatically bracketed.  This reduces the visual
 * cluster, without any lost of functionalities (thanks to the addition of the
 * concatenation operator).
 *
 * <h3>Example</h3>
 * The following code snippet illustrates the use of ELProcessor to define
 * a bean and evaluate its property.
 * <blockquote>
 * <pre>
 *   ELProcessor elp = new ELProcessor();
 *   elp.defineBean("employee", new Employee("Charlie Brown"));
 *   String name = elp.eval("employee.name");
 * </pre>
 * </blockquote>
 * @since EL 3.0
 */

public class ELProcessor {

    private ELManager elManager = new ELManager();
    private ExpressionFactory factory = elManager.getExpressionFactory();

    /**
     * Return the ELManager used for EL processing.
     * @return The ELManager used for EL processing.
     */
    public ELManager getELManager() {
        return elManager;
    }

    /**
     * Evaluates an EL expression.
     * @param expression The EL expression to be evaluated.
     * @return The result of the expression evaluation.
     */
    public Object eval(String expression) {
        return getValue(expression, Object.class);
    }

    /**
     * Evaluates an EL expression, and coerces the result to the specified type.
     * @param expression The EL expression to be evaluated.
     * @param expectedType Specifies the type that the resultant evaluation
     *        will be coerced to.
     * @return The result of the expression evaluation.
     */
    public Object getValue(String expression, Class<?> expectedType) {
        ValueExpression exp = factory.createValueExpression(
                                  elManager.getELContext(),
                                  bracket(expression), expectedType);
        return exp.getValue(elManager.getELContext());
    }

    /**
     * Sets an expression with a new value. 
     * The target expression is evaluated, up to the last property resolution,
     * and the resultant (base, property) pair is set to the provided value.
     *
     * @param expression The target expression
     * @param value The new value to set.
     * @throws PropertyNotFoundException if one of the property
     *     resolutions failed because a specified variable or property
     *     does not exist or is not readable.
     * @throws PropertyNotWritableException if the final variable or
     *     property resolution failed because the specified
     *     variable or property is not writable.
     * @throws ELException if an exception was thrown while attempting to
     *     set the property or variable. The thrown exception
     *     must be included as the cause property of this exception, if
     *     available.
     */
    public void setValue(String expression, Object value) {
        ValueExpression exp = factory.createValueExpression(
                                  elManager.getELContext(),
                                  bracket(expression), Object.class);
        exp.setValue(elManager.getELContext(), value);
    }

    /**
     * Assign an EL expression to an EL variable.  The expression is parsed,
     * but not evaluated, and the parsed expression is mapped to the EL
     * variable in the local variable map.
     * Any previously assigned expression to the same variable will be replaced.
     * If the expression is <code>null</code>, the variable will be removed.
     * @param var The name of the variable.
     * @param expression The EL expression to be assigned to the variable.
     */
    public void setVariable(String var, String expression) {
        ValueExpression exp = factory.createValueExpression(
                                  elManager.getELContext(),
                                  bracket(expression), Object.class);
        elManager.setVariable(var, exp);
    }

    /**
     * Define an EL function in the local function mapper.
     * @param prefix The namespace for the function or "" for no namesapce.
     * @param function The name of the function.
     *    If empty (""), the method name is used as the function name.
     * @param className The full Java class name that implements the function.
     * @param method The name (specified without parenthesis) or the signature 
     *    (as in the Java Language Spec) of the static method that implements
     *    the function.  If the name (e.g. "sum") is given, the first declared
     *    method in class that matches the name is selected.  If the signature
     *    (e.g. "int sum(int, int)" ) is given, then the declared method
     *    with the signature is selected.
     *    
     * @throws NullPointerException if any of the arguments is null.
     * @throws ClassNotFoundException if the specified class does not exists.
     * @throws NoSuchMethodException if the method (with or without the
     *    signature) is not a declared method of the class, or if the method
     *    signature is not valid, or if the method is not a static method.
     */
    public void defineFunction(String prefix, String function,
                               String className,
                               String method)
            throws ClassNotFoundException, NoSuchMethodException {
        
        if (prefix == null || function == null || className == null
                || method == null) {
            throw new NullPointerException("Null argument for defineFunction");
        }
        
        Method meth = null;
        ClassLoader loader = getClass().getClassLoader();
        Class<?> klass = Class.forName(className, false, loader);
        int j = method.indexOf('(');
        if (j < 0) {
            // Just a name is given
            for (Method m: klass.getDeclaredMethods()) {
                if (m.getName().equals(method)) {
                    meth = m;
                }
            }
            if (meth == null) {
                throw new NoSuchMethodException();
            }
        } else {
            // method is the signature
            // First get the method name, ignore the return type
            int p = method.indexOf(' ');
            if (p < 0) {
                throw new NoSuchMethodException(
                    "Bad method singnature: " + method);
            }
            String methodName = method.substring(p+1, j).trim();
            // Extract parameter types
            p = method.indexOf(')', j+1);
            if (p < 0) {
                throw new NoSuchMethodException(
                    "Bad method singnature: " + method);
            }
            String[] params = method.substring(j+1, p).split(",");
            Class<?>[] paramTypes = new Class<?>[params.length];
            for (int i = 0; i < params.length; i++) {
                paramTypes[i] = toClass(params[i], loader);
            }
            meth = klass.getDeclaredMethod(methodName, paramTypes);
        }
        if (! Modifier.isStatic(meth.getModifiers())) {
            throw new NoSuchMethodException("The method specified in defineFunction must be static: " + meth);
        }
        if (function.equals("")) {
            function = method;
        }
        elManager.mapFunction(prefix, function, meth);
    }

    /**
     * Define an EL function in the local function mapper.
     * @param prefix The namespace for the function or "" for no namesapce.
     * @param function The name of the function.
     *    If empty (""), the method name is used as the function name.
     * @param method The <code>java.lang.reflect.Method</code> instance of
     *    the method that implements the function.
     * @throws NullPointerException if any of the arguments is null.
     * @throws NoSuchMethodException if the method is not a static method
     */
    public void defineFunction(String prefix, String function, Method method)
            throws NoSuchMethodException {
        if (prefix == null || function == null || method == null) {
            throw new NullPointerException("Null argument for defineFunction");
        }
        if (! Modifier.isStatic(method.getModifiers())) {
            throw new NoSuchMethodException("The method specified in defineFunction must be static: " + method);
        }
        if (function.equals("")) {
            function = method.getName();
       }
        elManager.mapFunction(prefix, function, method);
    }

    /**
     * Define a bean in a local bean repository, hiding other beans of the
     * same name.  
     * @param name The name of the bean
     * @param bean The bean instance to be defined.  If <code>null</code>,
     *   the name will be removed from the local bean repository.
     */
    public void defineBean(String name, Object bean) {
        elManager.defineBean(name, bean);
    }

    /**
     * Return the Class object associated with the class or interface with
     * the given name.
     */
    private static Class<?> toClass(String type, ClassLoader loader)
            throws ClassNotFoundException {

        Class<?> c = null;
        int i0 = type.indexOf('[');
        int dims = 0;
        if (i0 > 0) {
            // This is an array.  Count the dimensions
            for (int i = 0; i < type.length(); i++) {
                if (type.charAt(i) == '[')
                    dims++;
            }
            type = type.substring(0, i0);
        }

        if ("boolean".equals(type))
            c = boolean.class;
        else if ("char".equals(type))
            c = char.class;
        else if ("byte".equals(type))
            c =  byte.class;
        else if ("short".equals(type))
            c = short.class;
        else if ("int".equals(type))
            c = int.class;
        else if ("long".equals(type))
            c = long.class;
        else if ("float".equals(type))
            c = float.class;
        else if ("double".equals(type))
            c = double.class;
        else
            c = loader.loadClass(type);

        if (dims == 0)
            return c;

        if (dims == 1)
            return java.lang.reflect.Array.newInstance(c, 1).getClass();

        // Array of more than i dimension
        return java.lang.reflect.Array.newInstance(c, new int[dims]).getClass();
    }

    private String bracket(String expression) {
        return "${" + expression + '}';
    }
}

