blob: e99f9f96efcb628f9c627dd4b9d05e47dd424b3e [file] [log] [blame]
/*
* 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;
}
}