/*
 * Copyright 2004,2004 The Apache Software Foundation.
 * 
 * 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 org.apache.bsf.engines.java;

import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.lang.reflect.Method;
import java.util.Hashtable;
import java.util.Vector;

import org.apache.bsf.BSFException;
import org.apache.bsf.BSFManager;
import org.apache.bsf.util.BSFEngineImpl;
import org.apache.bsf.util.CodeBuffer;
import org.apache.bsf.util.EngineUtils;
import org.apache.bsf.util.JavaUtils;
import org.apache.bsf.util.MethodUtils;
import org.apache.bsf.util.ObjInfo;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This is the interface to Java from the
 * Bean Scripting Framework.
 * <p>
 * The Java code must be written script-style -- that is, just the body of
 * the function, without class or method headers or footers.
 * The JavaEngine will generate those via a "boilerplate" wrapper:
 * <pre>
 * <code>
 * import java.lang.*;
 * import java.util.*;
 * public class $$CLASSNAME$$ {
 *   static public Object BSFJavaEngineEntry(org.apache.bsf.BSFManager bsf) {
 *     // Your code will be placed here
 *   }
 * }
 * </code>
 * </pre>
 * $$CLASSNAME$$ will be replaced by a generated classname of the form
 * BSFJava*, and the bsf parameter can be used to retrieve application
 * objects registered with the Bean Scripting Framework.
 * <p>
 * If you use the placeholder string $$CLASSNAME$$ elsewhere
 * in your script -- including within text strings -- BSFJavaEngine will
 * replace it with the generated name of the class before the Java code
 * is compiled.
 * <p>
 * <h2>Hazards:</h2>
 * <p>
 * NOTE that it is your responsibility to convert the code into an acceptable
 * Java string. If you're invoking the JavaEngine directly (as in the
 * JSPLikeInJava example) that means \"quoting\" characters that would
 * otherwise cause trouble.
 * <p>
 * ALSO NOTE that it is your responsibility to return an object, or null in
 * lieu thereof!
 * <p>
 * Since the code has to be compiled to a Java classfile, invoking it involves
 * a fair amount of computation to load and execute the compiler. We are
 * currently making an attempt to manage that by caching the class
 * after it has been loaded, but the indexing is fairly primitive. It has
 * been suggested that the Bean Scripting Framework may want to support
 * preload-and-name-script and execute-preloaded-script-by-name options to
 * provide better control over when and how much overhead occurs.
 * <p>
 * @author Joe Kesselman
 */
public class JavaEngine extends BSFEngineImpl {
    Class javaclass = null;
    static Hashtable codeToClass = new Hashtable();
    static String serializeCompilation = "";
    static String placeholder = "$$CLASSNAME$$";
    String minorPrefix;
        
    private Log logger = LogFactory.getLog(this.getClass().getName());
    
    /**
     * Create a scratchfile, open it for writing, return its name.
     * Relies on the filesystem to provide us with uniqueness testing.
     * NOTE THAT uniqueFileOffset continues to count; we don't want to
     * risk reusing a classname we have previously loaded in this session
     * even if the classfile has been deleted.
     */
    private int uniqueFileOffset = -1;
    
    private class GeneratedFile {
        File file = null;
        FileOutputStream fos = null;
        String className = null;
        GeneratedFile(File file, FileOutputStream fos, String className) {
            this.file = file;
            this.fos = fos;
            this.className = className;
        }
    }
    
    /**
     * Constructor.
     */
    public JavaEngine () {
        // Do compilation-possible check here??????????????
    }
    
    public Object call (Object object, String method, Object[] args) 
    throws BSFException
    {
        throw new BSFException (BSFException.REASON_UNSUPPORTED_FEATURE,
        "call() is not currently supported by JavaEngine");
    }
    
    public void compileScript (String source, int lineNo, int columnNo,
            Object script, CodeBuffer cb) throws BSFException {
        ObjInfo oldRet = cb.getFinalServiceMethodStatement ();
        
        if (oldRet != null && oldRet.isExecutable ()) {
            cb.addServiceMethodStatement (oldRet.objName + ";");
        }
        
        cb.addServiceMethodStatement (script.toString ());
        cb.setFinalServiceMethodStatement (null);
    }
    
    /**
     * This is used by an application to evaluate a string containing
     * some expression. It should store the "bsf" handle where the
     * script can get to it, for callback purposes.
     * <p>
     * Note that Java compilation imposes serious overhead,
     * but in exchange you get full Java performance
     * once the classes have been created (minus the cache lookup cost).
     * <p>
     * Nobody knows whether javac is threadsafe.
     * I'm going to serialize access to protect it.
     * <p>
     * There is no published API for invoking javac as a class. There's a trick
     * that seems to work for Java 1.1.x, but it stopped working in Java 1.2.
     * We will attempt to use it, then if necessary fall back on invoking
     * javac via the command line.
     */
    public Object eval (String source, int lineNo, int columnNo, 
            Object oscript) throws BSFException
            {
        Object retval = null;
        String classname = null;
        GeneratedFile gf = null;
        
        String basescript = oscript.toString();
        String script = basescript;	// May be altered by $$CLASSNAME$$ expansion
        
        try {
            // Do we already have a class exactly matching this code?
            javaclass = (Class)codeToClass.get(basescript);
            
            if(javaclass != null) {
                classname=javaclass.getName();
            } else {
                gf = openUniqueFile(tempDir, "BSFJava",".java");
                if( gf == null) {
                    throw new BSFException("couldn't create JavaEngine scratchfile");
                }
                // Obtain classname
                classname = gf.className;
                
                // Write the kluge header to the file.
                gf.fos.write(("import java.lang.*;"+
                        "import java.util.*;"+
                        "public class "+classname+" {\n" +
                "  static public Object BSFJavaEngineEntry(org.apache.bsf.BSFManager bsf) {\n")
                .getBytes());
                
                // Edit the script to replace placeholder with the generated
                // classname. Note that this occurs _after_ the cache was checked!
                int startpoint = script.indexOf(placeholder);
                int endpoint;
                if(startpoint >= 0) {
                    StringBuffer changed = new StringBuffer();
                    for(; startpoint >=0; startpoint = script.indexOf(placeholder,startpoint)) {
                        changed.setLength(0);	// Reset for 2nd pass or later
                        if(startpoint > 0) {
                            changed.append(script.substring(0,startpoint));
                        }
                        changed.append(classname);
                        endpoint = startpoint+placeholder.length();
                        if(endpoint < script.length()) {
                            changed.append(script.substring(endpoint));
                        }
                        script = changed.toString();
                    }
                }
                
                // MJD - debug
//              BSFDeclaredBean tempBean;
//              String          className;
//              
//              for (int i = 0; i < declaredBeans.size (); i++) {
//              tempBean  = (BSFDeclaredBean) declaredBeans.elementAt (i);
//              className = StringUtils.getClassName (tempBean.bean.getClass ());
//              
//              gf.fos.write ((className + " " +
//              tempBean.name + " = (" + className +
//              ")bsf.lookupBean(\"" +
//              tempBean.name + "\");").getBytes ());
//              }
                // MJD - debug
                
                // Copy the input to the file.
                // Assumes all available -- probably mistake, but same as other engines.
                gf.fos.write(script.getBytes());
                // Close the method and class
                gf.fos.write(("\n  }\n}\n").getBytes());
                gf.fos.close();
                
                // Compile through Java to .class file
                // May not be threadsafe. Serialize access on static object:
                synchronized(serializeCompilation) {
                    JavaUtils.JDKcompile(gf.file.getPath(), classPath);
                }
                
                // Load class.
                javaclass = EngineUtils.loadClass(mgr, classname);
                
                // Stash class for reuse
                codeToClass.put(basescript, javaclass);
            }
            
            Object[] callArgs = {mgr};      
            retval = internalCall(this,"BSFJavaEngineEntry",callArgs);
        }
        
        
        catch(Exception e) {
            e.printStackTrace ();
            throw new BSFException (BSFException.REASON_IO_ERROR, e.getMessage ());
        } finally {
            // Cleanup: delete the .java and .class files
            
//          if(gf!=null && gf.file!=null && gf.file.exists())
//          gf.file.delete();  // .java file
            
            
            if(classname!=null) {
                // Generated class
                File file = new File(tempDir+File.separatorChar+classname+".class");
//              if(file.exists())
//              file.delete();
                
                // Search for and clean up minor classes, classname$xxx.class
                file = new File(tempDir);  // ***** Is this required?
                minorPrefix = classname+"$"; // Indirect arg to filter
                String[] minorClassfiles = file.list(new FilenameFilter()
                            {
                        // Starts with classname$ and ends with .class
                        public boolean accept(File dir,String name) {
                            return
                            (0 == name.indexOf(minorPrefix))
                            &&
                            (name.lastIndexOf(".class") == name.length()-6);
                        }
                            });
                for(int i = 0; i < minorClassfiles.length; ++i) {
                    file = new File(minorClassfiles[i]);
//                  file.delete();
                }
            }
        }
        return retval;
    }
    
    public void initialize (BSFManager mgr, String lang,
            Vector declaredBeans) throws BSFException {
        super.initialize (mgr, lang, declaredBeans);
    }
    /**
     * Return an object from an extension.
     * @param object Object on which to make the internal_call (ignored).
     * @param method The name of the method to internal_call.
     * @param args an array of arguments to be
     * passed to the extension, which may be either
     * Vectors of Nodes, or Strings.
     */
    Object internalCall (Object object, String method, Object[] args) 
    throws BSFException
    {
        //***** ISSUE: Only static methods are currently supported
        Object retval = null;
        try {
            if(javaclass != null) {
                //***** This should call the lookup used in BML, for typesafety
                Class[] argtypes = new Class[args.length];
                for(int i=0; i<args.length; ++i) {
                    argtypes[i]=args[i].getClass();
                }
                Method m = MethodUtils.getMethod(javaclass, method, argtypes);
                retval = m.invoke(null, args);
            }
        }
        catch(Exception e) {
            throw new BSFException (BSFException.REASON_IO_ERROR, e.getMessage ());
        }
        return retval;
    }
    
    private GeneratedFile openUniqueFile(String directory,String prefix,String suffix) {
        File file = null;
        FileOutputStream fos = null;
        int max = 1000;		// Don't try forever
        GeneratedFile gf = null;
        int i;
        String className = null;
        for(i=max,++uniqueFileOffset; fos==null && i>0;--i,++uniqueFileOffset) {
            // Probably a timing hazard here... ***************
            try {
                className = prefix+uniqueFileOffset;
                file = new File(directory+File.separatorChar+className+suffix);
                if(file != null && !file.exists()) {
                    fos = new FileOutputStream(file);
                }
            }
            catch(Exception e) {
                // File could not be opened for write, or Security Exception
                // was thrown. If someone else created the file before we could
                // open it, that's probably a threading conflict and we don't
                // bother reporting it.
                if(!file.exists()) {
                    logger.error("openUniqueFile: unexpected ", e);
                }
            }
        }
        if(fos==null) {
            logger.error("openUniqueFile: Failed "+max+"attempts.");
        } else {
            gf = new GeneratedFile(file,fos,className);
        }
        return gf;
    }
}