001: /*
002: * BeanShell.java - BeanShell scripting support
003: * :tabSize=8:indentSize=8:noTabs=false:
004: * :folding=explicit:collapseFolds=1:
005: *
006: * Copyright (C) 2000, 2004 Slava Pestov
007: *
008: * This program is free software; you can redistribute it and/or
009: * modify it under the terms of the GNU General Public License
010: * as published by the Free Software Foundation; either version 2
011: * of the License, or any later version.
012: *
013: * This program is distributed in the hope that it will be useful,
014: * but WITHOUT ANY WARRANTY; without even the implied warranty of
015: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
016: * GNU General Public License for more details.
017: *
018: * You should have received a copy of the GNU General Public License
019: * along with this program; if not, write to the Free Software
020: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
021: */
022:
023: package org.gjt.sp.jedit;
024:
025: //{{{ Imports
026: import org.gjt.sp.jedit.bsh.*;
027: import org.gjt.sp.jedit.bsh.classpath.ClassManagerImpl;
028:
029: import java.io.*;
030: import java.lang.ref.*;
031: import java.lang.reflect.InvocationTargetException;
032: import java.util.*;
033: import org.gjt.sp.jedit.io.*;
034: import org.gjt.sp.jedit.gui.BeanShellErrorDialog;
035: import org.gjt.sp.jedit.textarea.*;
036: import org.gjt.sp.util.Log;
037:
038: //}}}
039:
040: /**
041: * BeanShell is jEdit's extension language.<p>
042: *
043: * When run from jEdit, BeanShell code has access to the following predefined
044: * variables:
045: *
046: * <ul>
047: * <li><code>view</code> - the currently active {@link View}.</li>
048: * <li><code>editPane</code> - the currently active {@link EditPane}.</li>
049: * <li><code>textArea</code> - the edit pane's {@link JEditTextArea}.</li>
050: * <li><code>buffer</code> - the edit pane's {@link Buffer}.</li>
051: * <li><code>wm</code> - the view's {@link
052: * org.gjt.sp.jedit.gui.DockableWindowManager}.</li>
053: * <li><code>scriptPath</code> - the path name of the currently executing
054: * BeanShell script.</li>
055: * </ul>
056: *
057: * @author Slava Pestov
058: * @version $Id: BeanShell.java 10803 2007-10-04 20:45:31Z kpouer $
059: */
060: public class BeanShell {
061: private static final String REQUIRED_VERSION = "2.0b1.1-jedit-1";
062:
063: //{{{ evalSelection() method
064: /**
065: * Evaluates the text selected in the specified text area.
066: * @since jEdit 2.7pre2
067: */
068: public static void evalSelection(View view, JEditTextArea textArea) {
069: String command = textArea.getSelectedText();
070: if (command == null) {
071: view.getToolkit().beep();
072: return;
073: }
074: Object returnValue = eval(view, global, command);
075: if (returnValue != null)
076: textArea.setSelectedText(returnValue.toString());
077: } //}}}
078:
079: //{{{ showEvaluateDialog() method
080: /**
081: * Prompts for a BeanShell expression to evaluate.
082: * @since jEdit 2.7pre2
083: */
084: public static void showEvaluateDialog(View view) {
085: String command = GUIUtilities.input(view,
086: "beanshell-eval-input", null);
087: if (command != null) {
088: if (!command.endsWith(";"))
089: command = command + ";";
090:
091: int repeat = view.getInputHandler().getRepeatCount();
092:
093: if (view.getMacroRecorder() != null) {
094: view.getMacroRecorder().record(repeat, command);
095: }
096:
097: Object returnValue = null;
098: try {
099: for (int i = 0; i < repeat; i++) {
100: returnValue = _eval(view, global, command);
101: }
102: } catch (Throwable e) {
103: Log.log(Log.ERROR, BeanShell.class, e);
104:
105: handleException(view, null, e);
106: }
107:
108: if (returnValue != null) {
109: String[] args = { returnValue.toString() };
110: GUIUtilities.message(view, "beanshell-eval", args);
111: }
112: }
113: } //}}}
114:
115: //{{{ showEvaluateLinesDialog() method
116: /**
117: * Evaluates the specified script for each selected line.
118: * @since jEdit 4.0pre1
119: */
120: public static void showEvaluateLinesDialog(View view) {
121: String command = GUIUtilities.input(view,
122: "beanshell-eval-line", null);
123:
124: JEditTextArea textArea = view.getTextArea();
125: Buffer buffer = view.getBuffer();
126:
127: if (command == null || command.length() == 0)
128: return;
129:
130: Selection[] selection = textArea.getSelection();
131: if (selection.length == 0) {
132: view.getToolkit().beep();
133: return;
134: }
135:
136: if (!command.endsWith(";"))
137: command = command + ";";
138:
139: String script = "int[] lines = textArea.getSelectedLines();\n"
140: + "for(int i = 0; i < lines.length; i++)\n" + "{\n"
141: + "line = lines[i];\n" + "index = line - lines[0];\n"
142: + "start = buffer.getLineStartOffset(line);\n"
143: + "end = buffer.getLineEndOffset(line);\n"
144: + "text = buffer.getText(start,end - start - 1);\n"
145: + "newText = " + command + "\n"
146: + "if(newText != null)\n" + "{\n"
147: + "buffer.remove(start,end - start - 1);\n"
148: + "buffer.insert(start,String.valueOf(newText));\n"
149: + "}\n" + "}\n";
150:
151: if (view.getMacroRecorder() != null)
152: view.getMacroRecorder().record(1, script);
153:
154: try {
155: buffer.beginCompoundEdit();
156:
157: BeanShell.eval(view, global, script);
158: } finally {
159: buffer.endCompoundEdit();
160: }
161:
162: textArea.selectNone();
163: } //}}}
164:
165: //{{{ runScript() method
166: /**
167: * Runs a BeanShell script. Errors are shown in a dialog box.<p>
168: *
169: * If the <code>in</code> parameter is non-null, the script is
170: * read from that stream; otherwise it is read from the file identified
171: * by <code>path</code>.<p>
172: *
173: * The <code>scriptPath</code> BeanShell variable is set to the path
174: * name of the script.
175: *
176: * @param view The view. Within the script, references to
177: * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
178: * are determined with reference to this parameter.
179: * @param path The script file's VFS path.
180: * @param in The reader to read the script from, or <code>null</code>.
181: * @param ownNamespace If set to <code>false</code>, methods and
182: * variables defined in the script will be available to all future
183: * uses of BeanShell; if set to <code>true</code>, they will be lost as
184: * soon as the script finishes executing. jEdit uses a value of
185: * <code>false</code> when running startup scripts, and a value of
186: * <code>true</code> when running all other macros.
187: *
188: * @since jEdit 4.0pre7
189: */
190: public static void runScript(View view, String path, Reader in,
191: boolean ownNamespace) {
192: try {
193: _runScript(view, path, in, ownNamespace);
194: } catch (Throwable e) {
195: Log.log(Log.ERROR, BeanShell.class, e);
196:
197: handleException(view, path, e);
198: }
199: } //}}}
200:
201: //{{{ runScript() method
202: /**
203: * Runs a BeanShell script. Errors are shown in a dialog box.<p>
204: *
205: * If the <code>in</code> parameter is non-null, the script is
206: * read from that stream; otherwise it is read from the file identified
207: * by <code>path</code>.<p>
208: *
209: * The <code>scriptPath</code> BeanShell variable is set to the path
210: * name of the script.
211: *
212: * @param view The view. Within the script, references to
213: * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
214: * are determined with reference to this parameter.
215: * @param path The script file's VFS path.
216: * @param in The reader to read the script from, or <code>null</code>.
217: * @param namespace The namespace to run the script in.
218: *
219: * @since jEdit 4.2pre5
220: */
221: public static void runScript(View view, String path, Reader in,
222: NameSpace namespace) {
223: try {
224: _runScript(view, path, in, namespace);
225: } catch (Throwable e) {
226: Log.log(Log.ERROR, BeanShell.class, e);
227:
228: handleException(view, path, e);
229: }
230: } //}}}
231:
232: //{{{ _runScript() method
233: /**
234: * Runs a BeanShell script. Errors are passed to the caller.<p>
235: *
236: * If the <code>in</code> parameter is non-null, the script is
237: * read from that stream; otherwise it is read from the file identified
238: * by <code>path</code>.<p>
239: *
240: * The <code>scriptPath</code> BeanShell variable is set to the path
241: * name of the script.
242: *
243: * @param view The view. Within the script, references to
244: * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
245: * are determined with reference to this parameter.
246: * @param path The script file's VFS path.
247: * @param in The reader to read the script from, or <code>null</code>.
248: * @param ownNamespace If set to <code>false</code>, methods and
249: * variables defined in the script will be available to all future
250: * uses of BeanShell; if set to <code>true</code>, they will be lost as
251: * soon as the script finishes executing. jEdit uses a value of
252: * <code>false</code> when running startup scripts, and a value of
253: * <code>true</code> when running all other macros.
254: * @exception Exception instances are thrown when various BeanShell errors
255: * occur
256: * @since jEdit 4.0pre7
257: */
258: public static void _runScript(View view, String path, Reader in,
259: boolean ownNamespace) throws Exception {
260: _runScript(view, path, in, ownNamespace ? new NameSpace(global,
261: "namespace") : global);
262: } //}}}
263:
264: //{{{ _runScript() method
265: /**
266: * Runs a BeanShell script. Errors are passed to the caller.<p>
267: *
268: * If the <code>in</code> parameter is non-null, the script is
269: * read from that stream; otherwise it is read from the file identified
270: * by <code>path</code>.<p>
271: *
272: * The <code>scriptPath</code> BeanShell variable is set to the path
273: * name of the script.
274: *
275: * @param view The view. Within the script, references to
276: * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
277: * are determined with reference to this parameter.
278: * @param path The script file's VFS path.
279: * @param in The reader to read the script from, or <code>null</code>.
280: * @param namespace The namespace to run the script in.
281: * @exception Exception instances are thrown when various BeanShell errors
282: * occur
283: * @since jEdit 4.2pre5
284: */
285: public static void _runScript(View view, String path, Reader in,
286: NameSpace namespace) throws Exception {
287: Log.log(Log.MESSAGE, BeanShell.class, "Running script " + path);
288:
289: Interpreter interp = createInterpreter(namespace);
290:
291: VFS vfs = null;
292: Object session = null;
293:
294: try {
295: if (in == null) {
296: Buffer buffer = jEdit.openTemporary(null, null, path,
297: false);
298:
299: if (!buffer.isLoaded())
300: VFSManager.waitForRequests();
301:
302: in = new StringReader(buffer.getText(0, buffer
303: .getLength()));
304: }
305:
306: setupDefaultVariables(namespace, view);
307: interp.set("scriptPath", path);
308:
309: running = true;
310:
311: interp.eval(in, namespace, path);
312: } catch (Exception e) {
313: unwrapException(e);
314: } finally {
315: running = false;
316:
317: if (session != null) {
318: try {
319: vfs._endVFSSession(session, view);
320: } catch (IOException io) {
321: Log.log(Log.ERROR, BeanShell.class, io);
322: GUIUtilities.error(view, "read-error",
323: new String[] { path, io.toString() });
324: }
325: }
326:
327: try {
328: // no need to do this for macros!
329: if (namespace == global) {
330: resetDefaultVariables(namespace);
331: interp.unset("scriptPath");
332: }
333: } catch (EvalError e) {
334: // do nothing
335: }
336: }
337: } //}}}
338:
339: //{{{ eval() method
340: /**
341: * Evaluates the specified BeanShell expression. Errors are reported in
342: * a dialog box.
343: * @param view The view. Within the script, references to
344: * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
345: * are determined with reference to this parameter.
346: * @param namespace The namespace
347: * @param command The expression
348: * @since jEdit 4.0pre8
349: */
350: public static Object eval(View view, NameSpace namespace,
351: String command) {
352: try {
353: return _eval(view, namespace, command);
354: } catch (Throwable e) {
355: Log.log(Log.ERROR, BeanShell.class, e);
356:
357: handleException(view, null, e);
358: }
359:
360: return null;
361: } //}}}
362:
363: //{{{ _eval() method
364: /**
365: * Evaluates the specified BeanShell expression. Unlike
366: * <code>eval()</code>, this method passes any exceptions to the caller.
367: *
368: * @param view The view. Within the script, references to
369: * <code>buffer</code>, <code>textArea</code> and <code>editPane</code>
370: * are determined with reference to this parameter.
371: * @param namespace The namespace
372: * @param command The expression
373: * @exception Exception instances are thrown when various BeanShell
374: * errors occur
375: * @since jEdit 3.2pre7
376: */
377: public static Object _eval(View view, NameSpace namespace,
378: String command) throws Exception {
379: Interpreter interp = createInterpreter(namespace);
380:
381: try {
382: setupDefaultVariables(namespace, view);
383: if (Debug.BEANSHELL_DEBUG)
384: Log.log(Log.DEBUG, BeanShell.class, command);
385: return interp.eval(command);
386: } catch (Exception e) {
387: unwrapException(e);
388: // never called
389: return null;
390: } finally {
391: try {
392: resetDefaultVariables(namespace);
393: } catch (UtilEvalError e) {
394: // do nothing
395: }
396: }
397: } //}}}
398:
399: //{{{ cacheBlock() method
400: /**
401: * Caches a block of code, returning a handle that can be passed to
402: * runCachedBlock().
403: * @param id An identifier. If null, a unique identifier is generated
404: * @param code The code
405: * @param namespace If true, the namespace will be set
406: * @exception Exception instances are thrown when various BeanShell errors
407: * occur
408: * @since jEdit 4.1pre1
409: */
410: public static BshMethod cacheBlock(String id, String code,
411: boolean namespace) throws Exception {
412: String name = "__internal_" + id;
413:
414: // evaluate a method declaration
415: if (namespace) {
416: _eval(null, global, name
417: + "(ns) {\nthis.callstack.set(0,ns);\n" + code
418: + "\n}");
419: return global.getMethod(name,
420: new Class[] { NameSpace.class });
421: } else {
422: _eval(null, global, name + "() {\n" + code + "\n}");
423: return global.getMethod(name, new Class[0]);
424: }
425: } //}}}
426:
427: //{{{ runCachedBlock() method
428: /**
429: * Runs a cached block of code in the specified namespace. Faster than
430: * evaluating the block each time.
431: * @param method The method instance returned by cacheBlock()
432: * @param view The view
433: * @param namespace The namespace to run the code in
434: * @exception Exception instances are thrown when various BeanShell
435: * errors occur
436: * @since jEdit 4.1pre1
437: */
438: public static Object runCachedBlock(BshMethod method, View view,
439: NameSpace namespace) throws Exception {
440: boolean useNamespace;
441: if (namespace == null) {
442: useNamespace = false;
443: namespace = global;
444: } else
445: useNamespace = true;
446:
447: try {
448: setupDefaultVariables(namespace, view);
449:
450: Object retVal = method
451: .invoke(useNamespace ? new Object[] { namespace }
452: : NO_ARGS, interpForMethods,
453: new CallStack(), null);
454: if (retVal instanceof Primitive) {
455: if (retVal == Primitive.VOID)
456: return null;
457: else
458: return ((Primitive) retVal).getValue();
459: } else
460: return retVal;
461: } catch (Exception e) {
462: unwrapException(e);
463: // never called
464: return null;
465: } finally {
466: resetDefaultVariables(namespace);
467: }
468: } //}}}
469:
470: //{{{ isScriptRunning() method
471: /**
472: * Returns if a BeanShell script or macro is currently running.
473: * @since jEdit 2.7pre2
474: */
475: public static boolean isScriptRunning() {
476: return running;
477: } //}}}
478:
479: //{{{ getNameSpace() method
480: /**
481: * Returns the global namespace.
482: * @since jEdit 3.2pre5
483: */
484: public static NameSpace getNameSpace() {
485: return global;
486: } //}}}
487:
488: //{{{ Deprecated functions
489:
490: //{{{ runScript() method
491: /**
492: * @deprecated The <code>rethrowBshErrors</code> parameter is now
493: * obsolete; call <code>_runScript()</code> or <code>runScript()</code>
494: * instead.
495: */
496: public static void runScript(View view, String path,
497: boolean ownNamespace, boolean rethrowBshErrors) {
498: runScript(view, path, null, ownNamespace);
499: } //}}}
500:
501: //{{{ runScript() method
502: /**
503: * @deprecated The <code>rethrowBshErrors</code> parameter is now
504: * obsolete; call <code>_runScript()</code> or <code>runScript()</code>
505: * instead.
506: */
507: public static void runScript(View view, String path, Reader in,
508: boolean ownNamespace, boolean rethrowBshErrors) {
509: runScript(view, path, in, ownNamespace);
510: } //}}}
511:
512: //{{{ eval() method
513: /**
514: * @deprecated The <code>rethrowBshErrors</code> parameter is now
515: * obsolete; call <code>_eval()</code> or <code>eval()</code> instead.
516: */
517: public static Object eval(View view, String command,
518: boolean rethrowBshErrors) {
519: return eval(view, global, command);
520: } //}}}
521:
522: //{{{ eval() method
523: /**
524: * @deprecated The <code>rethrowBshErrors</code> parameter is now
525: * obsolete; call <code>_eval()</code> or <code>eval()</code> instead.
526: */
527: public static Object eval(View view, NameSpace namespace,
528: String command, boolean rethrowBshErrors) {
529: return eval(view, namespace, command);
530: } //}}}
531:
532: //}}}
533:
534: //{{{ Package-private members
535:
536: //{{{ init() method
537: static void init() {
538: /*try
539: {
540: NameSpace.class.getMethod("addCommandPath",
541: new Class[] { String.class, Class.class });
542: }
543: catch(Exception e)
544: {
545: Log.log(Log.ERROR,BeanShell.class,"You have BeanShell version " + getVersion() + " in your CLASSPATH.");
546: Log.log(Log.ERROR,BeanShell.class,"Please remove it from the CLASSPATH since jEdit can only run with the bundled BeanShell version " + REQUIRED_VERSION);
547: System.exit(1);
548: } */
549:
550: classManager = new ClassManagerImpl();
551: classManager.setClassLoader(new JARClassLoader());
552:
553: global = new NameSpace(classManager,
554: "jEdit embedded BeanShell interpreter");
555: global.importPackage("org.gjt.sp.jedit");
556: global.importPackage("org.gjt.sp.jedit.browser");
557: global.importPackage("org.gjt.sp.jedit.buffer");
558: global.importPackage("org.gjt.sp.jedit.gui");
559: global.importPackage("org.gjt.sp.jedit.help");
560: global.importPackage("org.gjt.sp.jedit.io");
561: global.importPackage("org.gjt.sp.jedit.menu");
562: global.importPackage("org.gjt.sp.jedit.msg");
563: global.importPackage("org.gjt.sp.jedit.options");
564: global.importPackage("org.gjt.sp.jedit.pluginmgr");
565: global.importPackage("org.gjt.sp.jedit.print");
566: global.importPackage("org.gjt.sp.jedit.search");
567: global.importPackage("org.gjt.sp.jedit.syntax");
568: global.importPackage("org.gjt.sp.jedit.textarea");
569: global.importPackage("org.gjt.sp.util");
570:
571: interpForMethods = createInterpreter(global);
572: } //}}}
573:
574: //{{{ resetClassManager() method
575: /**
576: * Causes BeanShell internal structures to drop references to cached
577: * Class instances.
578: */
579: static void resetClassManager() {
580: classManager.reset();
581: } //}}}
582:
583: //}}}
584:
585: //{{{ Private members
586:
587: //{{{ Static variables
588: private static final Object[] NO_ARGS = new Object[0];
589: private static BshClassManager classManager;
590: private static Interpreter interpForMethods;
591: private static NameSpace global;
592: private static boolean running;
593:
594: //}}}
595:
596: //{{{ setupDefaultVariables() method
597: private static void setupDefaultVariables(NameSpace namespace,
598: View view) throws UtilEvalError {
599: if (view != null) {
600: EditPane editPane = view.getEditPane();
601: namespace.setVariable("view", view, false);
602: namespace.setVariable("editPane", editPane, false);
603: namespace
604: .setVariable("buffer", editPane.getBuffer(), false);
605: namespace.setVariable("textArea", editPane.getTextArea(),
606: false);
607: namespace.setVariable("wm",
608: view.getDockableWindowManager(), false);
609: }
610: } //}}}
611:
612: //{{{ resetDefaultVariables() method
613: private static void resetDefaultVariables(NameSpace namespace)
614: throws UtilEvalError {
615: namespace.setVariable("view", null, false);
616: namespace.setVariable("editPane", null, false);
617: namespace.setVariable("buffer", null, false);
618: namespace.setVariable("textArea", null, false);
619: namespace.setVariable("wm", null, false);
620: } //}}}
621:
622: //{{{ unwrapException() method
623: /**
624: * This extracts an exception from a 'wrapping' exception, as BeanShell
625: * sometimes throws. This gives the user a more accurate error traceback
626: */
627: private static void unwrapException(Exception e) throws Exception {
628: if (e instanceof TargetError) {
629: Throwable t = ((TargetError) e).getTarget();
630: if (t instanceof Exception)
631: throw (Exception) t;
632: else if (t instanceof Error)
633: throw (Error) t;
634: }
635:
636: if (e instanceof InvocationTargetException) {
637: Throwable t = ((InvocationTargetException) e)
638: .getTargetException();
639: if (t instanceof Exception)
640: throw (Exception) t;
641: else if (t instanceof Error)
642: throw (Error) t;
643: }
644:
645: throw e;
646: } //}}}
647:
648: //{{{ handleException() method
649: private static void handleException(View view, String path,
650: Throwable t) {
651: if (t instanceof IOException) {
652: VFSManager.error(view, path, "ioerror.read-error",
653: new String[] { t.toString() });
654: } else
655: new BeanShellErrorDialog(view, t);
656: } //}}}
657:
658: //{{{ createInterpreter() method
659: private static Interpreter createInterpreter(NameSpace nameSpace) {
660: return new Interpreter(null, System.out, System.err, false,
661: nameSpace);
662: } //}}}
663:
664: //{{{ getVersion() method
665: private static String getVersion() {
666: try {
667: return (String) Interpreter.class.getField("VERSION").get(
668: null);
669: } catch (Exception e) {
670: return "unknown";
671: }
672: } //}}}
673:
674: //}}}
675:
676: //{{{ CustomClassManager class
677: static class CustomClassManager extends ClassManagerImpl {
678: private LinkedList listeners = new LinkedList();
679: private ReferenceQueue refQueue = new ReferenceQueue();
680:
681: // copy and paste from bsh/classpath/ClassManagerImpl.java...
682: public synchronized void addListener(Listener l) {
683: listeners.add(new WeakReference(l, refQueue));
684:
685: // clean up old listeners
686: Reference deadref;
687: while ((deadref = refQueue.poll()) != null) {
688: boolean ok = listeners.remove(deadref);
689: if (ok) {
690: //System.err.println("cleaned up weak ref: "+deadref);
691: } else {
692: if (Interpreter.DEBUG)
693: Interpreter
694: .debug("tried to remove non-existent weak ref: "
695: + deadref);
696: }
697: }
698: }
699:
700: public void removeListener(Listener l) {
701: throw new Error("unimplemented");
702: }
703:
704: public void reset() {
705: classLoaderChanged();
706: }
707:
708: protected synchronized void classLoaderChanged() {
709: // clear the static caches in BshClassManager
710: clearCaches();
711: if (listeners != null) {
712:
713: for (Iterator iter = listeners.iterator(); iter
714: .hasNext();) {
715: WeakReference wr = (WeakReference) iter.next();
716: Listener l = (Listener) wr.get();
717: if (l == null) // garbage collected
718: iter.remove();
719: else
720: l.classLoaderChanged();
721: }
722: }
723: }
724: } //}}}
725: }
|