1 /*
2 * Copyright 2004,2004 The Apache Software Foundation.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package org.apache.bsf.engines.java;
18
19 import java.io.File;
20 import java.io.FileOutputStream;
21 import java.io.FilenameFilter;
22 import java.lang.reflect.Method;
23 import java.util.Hashtable;
24 import java.util.Vector;
25
26 import org.apache.bsf.BSFException;
27 import org.apache.bsf.BSFManager;
28 import org.apache.bsf.util.BSFEngineImpl;
29 import org.apache.bsf.util.CodeBuffer;
30 import org.apache.bsf.util.EngineUtils;
31 import org.apache.bsf.util.JavaUtils;
32 import org.apache.bsf.util.MethodUtils;
33 import org.apache.bsf.util.ObjInfo;
34 import org.apache.commons.logging.Log;
35 import org.apache.commons.logging.LogFactory;
36
37 /**
38 * This is the interface to Java from the
39 * Bean Scripting Framework.
40 * <p>
41 * The Java code must be written script-style -- that is, just the body of
42 * the function, without class or method headers or footers.
43 * The JavaEngine will generate those via a "boilerplate" wrapper:
44 * <pre>
45 * <code>
46 * import java.lang.*;
47 * import java.util.*;
48 * public class $$CLASSNAME$$ {
49 * static public Object BSFJavaEngineEntry(org.apache.bsf.BSFManager bsf) {
50 * // Your code will be placed here
51 * }
52 * }
53 * </code>
54 * </pre>
55 * $$CLASSNAME$$ will be replaced by a generated classname of the form
56 * BSFJava*, and the bsf parameter can be used to retrieve application
57 * objects registered with the Bean Scripting Framework.
58 * <p>
59 * If you use the placeholder string $$CLASSNAME$$ elsewhere
60 * in your script -- including within text strings -- BSFJavaEngine will
61 * replace it with the generated name of the class before the Java code
62 * is compiled.
63 * <p>
64 * <h2>Hazards:</h2>
65 * <p>
66 * NOTE that it is your responsibility to convert the code into an acceptable
67 * Java string. If you're invoking the JavaEngine directly (as in the
68 * JSPLikeInJava example) that means \"quoting\" characters that would
69 * otherwise cause trouble.
70 * <p>
71 * ALSO NOTE that it is your responsibility to return an object, or null in
72 * lieu thereof!
73 * <p>
74 * Since the code has to be compiled to a Java classfile, invoking it involves
75 * a fair amount of computation to load and execute the compiler. We are
76 * currently making an attempt to manage that by caching the class
77 * after it has been loaded, but the indexing is fairly primitive. It has
78 * been suggested that the Bean Scripting Framework may want to support
79 * preload-and-name-script and execute-preloaded-script-by-name options to
80 * provide better control over when and how much overhead occurs.
81 * <p>
82 * @author Joe Kesselman
83 */
84 public class JavaEngine extends BSFEngineImpl {
85 Class javaclass = null;
86 static Hashtable codeToClass = new Hashtable();
87 static String serializeCompilation = "";
88 static String placeholder = "$$CLASSNAME$$";
89 String minorPrefix;
90
91 private Log logger = LogFactory.getLog(this.getClass().getName());
92
93 /**
94 * Create a scratchfile, open it for writing, return its name.
95 * Relies on the filesystem to provide us with uniqueness testing.
96 * NOTE THAT uniqueFileOffset continues to count; we don't want to
97 * risk reusing a classname we have previously loaded in this session
98 * even if the classfile has been deleted.
99 */
100 private int uniqueFileOffset = -1;
101
102 private class GeneratedFile {
103 File file = null;
104 FileOutputStream fos = null;
105 String className = null;
106 GeneratedFile(File file, FileOutputStream fos, String className) {
107 this.file = file;
108 this.fos = fos;
109 this.className = className;
110 }
111 }
112
113 /**
114 * Constructor.
115 */
116 public JavaEngine () {
117 // Do compilation-possible check here??????????????
118 }
119
120 public Object call (Object object, String method, Object[] args)
121 throws BSFException
122 {
123 throw new BSFException (BSFException.REASON_UNSUPPORTED_FEATURE,
124 "call() is not currently supported by JavaEngine");
125 }
126
127 public void compileScript (String source, int lineNo, int columnNo,
128 Object script, CodeBuffer cb) throws BSFException {
129 ObjInfo oldRet = cb.getFinalServiceMethodStatement ();
130
131 if (oldRet != null && oldRet.isExecutable ()) {
132 cb.addServiceMethodStatement (oldRet.objName + ";");
133 }
134
135 cb.addServiceMethodStatement (script.toString ());
136 cb.setFinalServiceMethodStatement (null);
137 }
138
139 /**
140 * This is used by an application to evaluate a string containing
141 * some expression. It should store the "bsf" handle where the
142 * script can get to it, for callback purposes.
143 * <p>
144 * Note that Java compilation imposes serious overhead,
145 * but in exchange you get full Java performance
146 * once the classes have been created (minus the cache lookup cost).
147 * <p>
148 * Nobody knows whether javac is threadsafe.
149 * I'm going to serialize access to protect it.
150 * <p>
151 * There is no published API for invoking javac as a class. There's a trick
152 * that seems to work for Java 1.1.x, but it stopped working in Java 1.2.
153 * We will attempt to use it, then if necessary fall back on invoking
154 * javac via the command line.
155 */
156 public Object eval (String source, int lineNo, int columnNo,
157 Object oscript) throws BSFException
158 {
159 Object retval = null;
160 String classname = null;
161 GeneratedFile gf = null;
162
163 String basescript = oscript.toString();
164 String script = basescript; // May be altered by $$CLASSNAME$$ expansion
165
166 try {
167 // Do we already have a class exactly matching this code?
168 javaclass = (Class)codeToClass.get(basescript);
169
170 if(javaclass != null) {
171 classname=javaclass.getName();
172 } else {
173 gf = openUniqueFile(tempDir, "BSFJava",".java");
174 if( gf == null) {
175 throw new BSFException("couldn't create JavaEngine scratchfile");
176 }
177 // Obtain classname
178 classname = gf.className;
179
180 // Write the kluge header to the file.
181 gf.fos.write(("import java.lang.*;"+
182 "import java.util.*;"+
183 "public class "+classname+" {\n" +
184 " static public Object BSFJavaEngineEntry(org.apache.bsf.BSFManager bsf) {\n")
185 .getBytes());
186
187 // Edit the script to replace placeholder with the generated
188 // classname. Note that this occurs _after_ the cache was checked!
189 int startpoint = script.indexOf(placeholder);
190 int endpoint;
191 if(startpoint >= 0) {
192 StringBuffer changed = new StringBuffer();
193 for(; startpoint >=0; startpoint = script.indexOf(placeholder,startpoint)) {
194 changed.setLength(0); // Reset for 2nd pass or later
195 if(startpoint > 0) {
196 changed.append(script.substring(0,startpoint));
197 }
198 changed.append(classname);
199 endpoint = startpoint+placeholder.length();
200 if(endpoint < script.length()) {
201 changed.append(script.substring(endpoint));
202 }
203 script = changed.toString();
204 }
205 }
206
207 // MJD - debug
208 // BSFDeclaredBean tempBean;
209 // String className;
210 //
211 // for (int i = 0; i < declaredBeans.size (); i++) {
212 // tempBean = (BSFDeclaredBean) declaredBeans.elementAt (i);
213 // className = StringUtils.getClassName (tempBean.bean.getClass ());
214 //
215 // gf.fos.write ((className + " " +
216 // tempBean.name + " = (" + className +
217 // ")bsf.lookupBean(\"" +
218 // tempBean.name + "\");").getBytes ());
219 // }
220 // MJD - debug
221
222 // Copy the input to the file.
223 // Assumes all available -- probably mistake, but same as other engines.
224 gf.fos.write(script.getBytes());
225 // Close the method and class
226 gf.fos.write(("\n }\n}\n").getBytes());
227 gf.fos.close();
228
229 // Compile through Java to .class file
230 // May not be threadsafe. Serialize access on static object:
231 synchronized(serializeCompilation) {
232 JavaUtils.JDKcompile(gf.file.getPath(), classPath);
233 }
234
235 // Load class.
236 javaclass = EngineUtils.loadClass(mgr, classname);
237
238 // Stash class for reuse
239 codeToClass.put(basescript, javaclass);
240 }
241
242 Object[] callArgs = {mgr};
243 retval = internalCall(this,"BSFJavaEngineEntry",callArgs);
244 }
245
246
247 catch(Exception e) {
248 e.printStackTrace ();
249 throw new BSFException (BSFException.REASON_IO_ERROR, e.getMessage ());
250 } finally {
251 // Cleanup: delete the .java and .class files
252
253 // if(gf!=null && gf.file!=null && gf.file.exists())
254 // gf.file.delete(); // .java file
255
256
257 if(classname!=null) {
258 // Generated class
259 File file = new File(tempDir+File.separatorChar+classname+".class");
260 // if(file.exists())
261 // file.delete();
262
263 // Search for and clean up minor classes, classname$xxx.class
264 file = new File(tempDir); // ***** Is this required?
265 minorPrefix = classname+"$"; // Indirect arg to filter
266 String[] minorClassfiles = file.list(new FilenameFilter()
267 {
268 // Starts with classname$ and ends with .class
269 public boolean accept(File dir,String name) {
270 return
271 (0 == name.indexOf(minorPrefix))
272 &&
273 (name.lastIndexOf(".class") == name.length()-6);
274 }
275 });
276 for(int i = 0; i < minorClassfiles.length; ++i) {
277 file = new File(minorClassfiles[i]);
278 // file.delete();
279 }
280 }
281 }
282 return retval;
283 }
284
285 public void initialize (BSFManager mgr, String lang,
286 Vector declaredBeans) throws BSFException {
287 super.initialize (mgr, lang, declaredBeans);
288 }
289 /**
290 * Return an object from an extension.
291 * @param object Object on which to make the internal_call (ignored).
292 * @param method The name of the method to internal_call.
293 * @param args an array of arguments to be
294 * passed to the extension, which may be either
295 * Vectors of Nodes, or Strings.
296 */
297 Object internalCall (Object object, String method, Object[] args)
298 throws BSFException
299 {
300 //***** ISSUE: Only static methods are currently supported
301 Object retval = null;
302 try {
303 if(javaclass != null) {
304 //***** This should call the lookup used in BML, for typesafety
305 Class[] argtypes = new Class[args.length];
306 for(int i=0; i<args.length; ++i) {
307 argtypes[i]=args[i].getClass();
308 }
309 Method m = MethodUtils.getMethod(javaclass, method, argtypes);
310 retval = m.invoke(null, args);
311 }
312 }
313 catch(Exception e) {
314 throw new BSFException (BSFException.REASON_IO_ERROR, e.getMessage ());
315 }
316 return retval;
317 }
318
319 private GeneratedFile openUniqueFile(String directory,String prefix,String suffix) {
320 File file = null;
321 FileOutputStream fos = null;
322 int max = 1000; // Don't try forever
323 GeneratedFile gf = null;
324 int i;
325 String className = null;
326 for(i=max,++uniqueFileOffset; fos==null && i>0;--i,++uniqueFileOffset) {
327 // Probably a timing hazard here... ***************
328 try {
329 className = prefix+uniqueFileOffset;
330 file = new File(directory+File.separatorChar+className+suffix);
331 if(file != null && !file.exists()) {
332 fos = new FileOutputStream(file);
333 }
334 }
335 catch(Exception e) {
336 // File could not be opened for write, or Security Exception
337 // was thrown. If someone else created the file before we could
338 // open it, that's probably a threading conflict and we don't
339 // bother reporting it.
340 if(!file.exists()) {
341 logger.error("openUniqueFile: unexpected ", e);
342 }
343 }
344 }
345 if(fos==null) {
346 logger.error("openUniqueFile: Failed "+max+"attempts.");
347 } else {
348 gf = new GeneratedFile(file,fos,className);
349 }
350 return gf;
351 }
352 }