/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.commons.jelly;

import java.io.File;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Properties;

import org.apache.commons.jelly.parser.XMLParser;
import org.apache.commons.jelly.util.ClassLoaderUtils;
import org.apache.commons.jelly.util.CommandLineParser;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.xml.sax.SAXException;

/** 
 * <p><code>Jelly</code> is a helper class which is capable of
 * running a Jelly script. This class can be used from the command line
 * or can be used as the basis of an Ant task.</p> Command line usage is as follows:
 *
 * <pre>
 * jelly [scriptFile] [-script scriptFile -o outputFile -Dsysprop=syspropval]
 * </pre>
 *
 * @author <a href="mailto:jstrachan@apache.org">James Strachan</a>
 * @version $Revision: 1807798 $
 */
public class Jelly {
    
    /** The Log to which logging calls will be made. */
    private static final Log log = LogFactory.getLog(Jelly.class);
    
    /** The JellyContext to use */
    private JellyContext context;
    
    /** The URL of the script to execute */
    private URL url;
    
    /** The URL of the root context for other scripts */
    private URL rootContext;
    
    /** Whether we have loaded the properties yet */
    private boolean loadedProperties = false;

    /**
     * whether to override the default namespace
     */
    private String defaultNamespaceURI = null;

    /**
     * whether or not to validate the Jelly script
     */
    private boolean validateXML = false;
        
    public Jelly() {
    }

    /**
     * Usage: jelly [scriptFile] [-script scriptFile -o outputFile -Dsysprop=syspropval]
     */
    public static void main(String[] args) throws Exception {

        try {
            if (args.length <= 0) {
                System.out.println("Usage: jelly [scriptFile] [-script scriptFile -o outputFile -Dsysprop=syspropval]");
                return;
            }

            // parse the command line options using CLI
            // using a separate class to avoid unnecessary 
            // dependencies
            CommandLineParser.getInstance().invokeCommandLineJelly(args);
        }
        catch (JellyException e) {
            Throwable cause = e.getCause();

            if (cause == null) {
                e.printStackTrace();
            } else {
                cause.printStackTrace();
            }
        }
    }
    
    
    public static String getJellyVersion() {
        return readBuildTimestampResource("jelly-version.txt");
    }
    
    public static String getJellyBuildDate() {
        return readBuildTimestampResource("jelly-build-date.txt");
    }
    
    private static String readBuildTimestampResource(String name) {
        java.io.Reader in = null;
        try {
            java.io.StringWriter w = new java.io.StringWriter();
            in = new java.io.InputStreamReader(Jelly.class.getResourceAsStream(name),"utf-8");
            int r;
            while ( (r=in.read()) >= 0 ) {
                w.write((char) r);
            }
            return w.toString();
        } catch(Exception ex) {
            ex.printStackTrace();
            try { in.close(); } catch(Exception e) {}
            throw new IllegalStateException("Resource \"" + name + "\" not found.");
        }
    }
    
    

    /**
     * Compiles the script
     */
    public Script compileScript() throws JellyException {
        if (! loadedProperties) {
            loadedProperties = true;
            loadJellyProperties();
        }
        
        XMLParser parser = new XMLParser();
        try {
            parser.setContext(getJellyContext());
        } catch (MalformedURLException e) {
            throw new JellyException(e.toString());
        }
        
        Script script = null;
        try {
            parser.setDefaultNamespaceURI(this.defaultNamespaceURI);
            parser.setValidating(this.validateXML);
            script = parser.parse(getUrl());
            script = script.compile();
            if (log.isDebugEnabled()) {
               log.debug("Compiled script: " + getUrl());
            }
        } catch (IOException e) {
            throw new JellyException("could not parse Jelly script",e);
        } catch (SAXException e) {
            throw new JellyException("could not parse Jelly script",e);
        }
        
        return script;
    }

    
    // Properties
    //-------------------------------------------------------------------------                
    
    /** 
     * Sets the script URL to use as an absolute URL or a relative filename
     */
    public void setScript(String script) throws MalformedURLException {
        setUrl(resolveURL(script));
    }
    
    public URL getUrl() {
        return url;
    }
    
    /** 
     * Sets the script URL to use 
     */
    public void setUrl(URL url) {
        this.url = url;
    }
    
    /** 
     * Gets the root context
     */
    public URL getRootContext() throws MalformedURLException {
        if (rootContext == null) {
            rootContext = new File(System.getProperty("user.dir")).toURL();
        }
        return rootContext;
    }
    
    /** 
     * Sets the root context
     */
    public void setRootContext(URL rootContext) {
        this.rootContext = rootContext;
    }
    
    /**
     * The context to use
     */
    public JellyContext getJellyContext() throws MalformedURLException {
        if (context == null) {
            // take off the name off the URL
            String text = getUrl().toString();
            int idx = text.lastIndexOf('/');
            text = text.substring(0, idx + 1);
            context = new JellyContext(getRootContext(), new URL(text));
        }
        return context;
    }


    /**
     * Set the jelly namespace to use for unprefixed elements.
     * Will be overridden by an explicit namespace in the
     * XML document.
     * 
     * @param namespace jelly namespace to use (e.g. 'jelly:core')
     */
    public void setDefaultNamespaceURI(String namespace) {
        this.defaultNamespaceURI = namespace;
    }

    /**
     * When set to true, the XML parser will attempt to validate
     * the Jelly XML before converting it into a Script.
     * 
     * @param validate whether or not to validate
     */
    public void setValidateXML(boolean validate) {
        this.validateXML = validate;
    }
    
    // Implementation methods
    //-------------------------------------------------------------------------                
    /**
     * @return the URL for the relative file name or absolute URL 
     */
    protected URL resolveURL(String name) throws MalformedURLException {
        
        URL resourceUrl = ClassLoaderUtils.getClassLoader(getClass()).getResource(name);
        if (resourceUrl == null)
        {
            File file = new File(name);
            if (file.exists()) {
                return file.toURL();
            }
            return new URL(name);
        } else {
            return resourceUrl;
        }
    }

    /**
     * Attempts to load jelly.properties from the current directory,
     * the users home directory or from the classpath
     */
    protected void loadJellyProperties() {
        InputStream is = null;
    
        String userDir = System.getProperty("user.home");
        File f = new File(userDir + File.separator + "jelly.properties");
        loadProperties(f);
    
        f = new File("jelly.properties");
        loadProperties(f);
        
        
        is = ClassLoaderUtils.getClassLoader(getClass()).getResourceAsStream("jelly.properties");
        if (is != null) {
            try {
                loadProperties(is);
            }
            catch (Exception e) {
                log.error( "Caught exception while loading jelly.properties from the classpath. Reason: " + e, e );
            }
        }
    }

    /**
     * Load properties from a file into the context
     * @param f
     */
    private void loadProperties(File f) {
        InputStream is = null;
        try {
            if (f.exists()) {
                is = new FileInputStream(f);
                loadProperties(is);
            }
        } catch (Exception e) {
            log.error( "Caught exception while loading: " + f.getName() + ". Reason: " + e, e );
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {
                    if (log.isDebugEnabled()) log.debug("error closing property input stream", e);
                }
            }
        }
    }

    /**
     * Loads the properties from the given input stream 
     */    
    protected void loadProperties(InputStream is) throws IOException {
        JellyContext theContext = getJellyContext();
        Properties props = new Properties();
        props.load(is);
        Enumeration propsEnum = props.propertyNames();
        while (propsEnum.hasMoreElements()) {
            String key = (String) propsEnum.nextElement();
            String value = props.getProperty(key);
            
            // @todo we should parse the value in case its an Expression
            theContext.setVariable(key, value);
        }
    }
}
