package org.apache.velocity.script;

/*
 * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
 * Use is subject to license terms.
 *
 * 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 Sun Microsystems nor the names of
 * is 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.
 */

/*
 * Main class for the Velocity script engine.
 * All the variants of the eval() methods return the writer. The default writer is a PrintWriter towards System.out.
 * To specify a specific writer, use getContext().setWriter(writer). To get a resulting string, pass a StringWriter.
 *
 * You can specify a pathname towards a Velocity properties file using the "org.apache.velocity.script.properties" key,
 * either as a ScriptContext attribute, or as a System property.
 *
 * Example use:
 * <pre>
 *     ScriptEngine vel = new VelocityScriptEngine();
 *     vel.getContext().setAttribute(VelocityScriptEngine.VELOCITY_PROPERTIES_KEY, "path/to/velocity.properties");
 *     vel.getContext().setWriter(new StringWriter());
 *     vel.put("foo","World");
 *     Object result = vel.eval("Hello $foo !");
 *     String helloWorld = result.toString()
 * </pre>
 *
 * Please refer to the javax.script.ScriptEngine documentation for additional details.
 *
 * @author A. Sundararajan
 * @author <a href="mailto:claude.brisson@gmail.com">Claude Brisson</a>
 * @version $Id: VelocityScriptEngine.java$
 */

import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.apache.velocity.runtime.RuntimeInstance;
import org.apache.velocity.runtime.resource.loader.ResourceLoader;
import org.apache.velocity.runtime.resource.loader.StringResourceLoader;

import javax.script.AbstractScriptEngine;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import java.io.File;
import java.io.FileInputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Properties;

public class VelocityScriptEngine extends AbstractScriptEngine implements Compilable
{
    /**
     * Key used to provide this engine with the pathname of the Velocity properties file.
     * This key is first searched in the ScriptContext attributes, then as a System property
     */
    public static final String VELOCITY_PROPERTIES_KEY = "org.apache.velocity.script.properties";

    // my factory, may be null
    private volatile ScriptEngineFactory factory;
    private volatile RuntimeInstance velocityEngine;

    /**
     * constructs a new Velocity script engine, linked to the given factory
     * @param factory
     */
    public VelocityScriptEngine(ScriptEngineFactory factory)
    {
        this.factory = factory;
    }

    /**
     * constructs a new standalone Velocity script engine
     */
    public VelocityScriptEngine()
    {
        this(null);
    }

    /**
     * get the internal Velocity RuntimeInstance
     * @return the internal Velocity RuntimeInstance
     */
    protected RuntimeInstance getVelocityEngine()
    {
        return velocityEngine;
    }

    /**
     * Evaluate the given script.
     * If you wish to get a resulting string, call getContext().setWriter(new StringWriter()) then call toString()
     * on the returned writer.
     * @param str script source
     * @param ctx script context
     * @return the script context writer (by default a PrintWriter towards System.out)
     * @throws ScriptException
     */
    @Override
    public Object eval(String str, ScriptContext ctx)
                       throws ScriptException
    {
        return eval(new StringReader(str), ctx);
    }

    /**
     * Evaluate the given script.
     * If you wish to get a resulting string, call getContext().setWriter(new StringWriter()) then call toString()
     * on the returned writer.
     * @param reader script source reader
     * @param ctx script context
     * @return the script context writer (by default a PrintWriter towards System.out)
     * @throws ScriptException
     */
    @Override
    public Object eval(Reader reader, ScriptContext ctx)
                       throws ScriptException
    {
        initVelocityEngine(ctx);
        String fileName = getFilename(ctx);
        VelocityContext vctx = getVelocityContext(ctx);
        Writer out = ctx.getWriter();
        if (out == null)
        {
            out = new StringWriter();
            ctx.setWriter(out);
        }
        try
        {
            velocityEngine.evaluate(vctx, out, fileName, reader);
            out.flush();
        }
        catch (Exception exp)
        {
            throw new ScriptException(exp);
        }
        return out;
    }

    /**
     * get the factory used to create this script engine
     * @return factory
     */
    @Override
    public ScriptEngineFactory getFactory()
    {
        if (factory == null)
        {
            synchronized (this)
            {
                if (factory == null)
                {
                    factory = new VelocityScriptEngineFactory();
                }
            }
        }
        return factory;
    }

    /**
     * creates a new Bindings to be used with this script
     * @return new bindings
     */
    @Override
    public Bindings createBindings()
    {
        return new SimpleBindings();
    }

    private void initVelocityEngine(ScriptContext ctx)
    {
        if (ctx == null)
        {
            ctx = getContext();
        }
        if (velocityEngine == null)
        {
            synchronized (this)
            {
                if (velocityEngine != null) return;

                Properties props = getVelocityProperties(ctx);
                RuntimeInstance tmpEngine = new RuntimeInstance();
                try
                {
                    if (props != null)
                    {
                        tmpEngine.init(props);
                    }
                    else
                    {
                        tmpEngine.init();
                    }
                }
                catch (RuntimeException rexp)
                {
                    throw rexp;
                }
                catch (Exception exp)
                {
                    throw new RuntimeException(exp);
                }
                velocityEngine = tmpEngine;
            }
        }
    }

    protected static VelocityContext getVelocityContext(ScriptContext ctx)
    {
        ctx.setAttribute("context", ctx, ScriptContext.ENGINE_SCOPE);
        Bindings globalScope = ctx.getBindings(ScriptContext.GLOBAL_SCOPE);
        Bindings engineScope = ctx.getBindings(ScriptContext.ENGINE_SCOPE);
        if (globalScope != null)
        {
            return new VelocityContext(engineScope, new VelocityContext(globalScope));
        }
        else
        {
            return new VelocityContext(engineScope);
        }
    }

    protected static String getFilename(ScriptContext ctx)
    {
        Object fileName = ctx.getAttribute(ScriptEngine.FILENAME);
        return fileName != null? fileName.toString() : "<unknown>";
    }

    protected static Properties getVelocityProperties(ScriptContext ctx)
    {
        try
        {
            Object props = ctx.getAttribute(VELOCITY_PROPERTIES_KEY);
            if (props instanceof Properties)
            {
                return (Properties) props;
            }
            else
            {
                String propsName = System.getProperty(VELOCITY_PROPERTIES_KEY);
                if (propsName != null)
                {
                    File propsFile = new File(propsName);
                    if (propsFile.exists() && propsFile.canRead())
                    {
                        Properties p = new Properties();
                        p.load(new FileInputStream(propsFile));
                        return p;
                    }
                }
            }
        }
        catch (Exception exp)
        {
            System.err.println(exp);
        }
        return null;
    }

    /**
     * Compile a template
     * @param script template source
     * @return compiled template
     * @throws ScriptException
     */
    @Override
    public CompiledScript compile(String script) throws ScriptException
    {
        return compile(new StringReader(script));
    }

    /**
     * Compile a template
     * @param script template source
     * @return compiled template
     * @throws ScriptException
     */
    @Override
    public CompiledScript compile(Reader script) throws ScriptException
    {
        initVelocityEngine(null);
        ResourceLoader resourceLoader = new SingleResourceReader(script);
        Template template = new Template();
        template.setRuntimeServices(velocityEngine);
        template.setResourceLoader(resourceLoader);
        try
        {
            template.process();
        }
        catch(Exception e)
        {
            // CB TODO - exception may have line/col informations, that ScriptException can exploit
            throw new ScriptException(e);
        }
        return new VelocityCompiledScript(this, template);
    }

    // a dummy resource reader class, serving a single resource given by the provided resource reader
    protected static class SingleResourceReader extends StringResourceLoader
    {
        private Reader reader;

        public SingleResourceReader(Reader r)
        {
            reader = r;
        }

        @Override
        public Reader getResourceReader(String source, String encoding) throws ResourceNotFoundException
        {
            return reader;
        }
    }
}
