package votorola.g.script; // Copyright 2008-2009, 2012, Michael Allan, Christian Weilbach. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. import java.io.*; import java.net.*; import javax.script.*; import votorola.g.lang.*; /** A facility for including JavaScript modules in a running script. Scripts executed in * the {@linkplain #engine() engine} are provided with a global instance of this includer * (named vo), with which to load modules. For example: * *
vo.{@linkplain #inc(String) inc}( 'ABS-OR-REL-PATH/module.jsm' );
* *

Script files must be encoded in UTF-8, or equivalently ASCII.

*/ public @ThreadRestricted final class JavaScriptIncluder { // cf. jrunscript GLOBALS.load(), in your jdk/docs/technotes/tools/share/jsdocs/index.html /** Constructs a JavaScriptIncluder with a default engine, and no initial script. */ public JavaScriptIncluder() throws ScriptException { engine = newDefaultEngine(); init(); } /** Constructs a JavaScriptIncluder with a default engine. * * @param scriptFile to execute initially, * per {@linkplain #scriptFile() scriptFile}(); or null to execute none */ public JavaScriptIncluder( File scriptFile ) throws IOException, ScriptException { this( scriptFile, newDefaultEngine() ); } /** Constructs a JavaScriptIncluder with the specified engine. * * @param scriptFile to execute initially, * per {@linkplain #scriptFile() scriptFile}(); or null to execute none */ public JavaScriptIncluder( final File scriptFile, ScriptEngine _engine ) throws IOException, ScriptException { if( _engine == null ) throw new NullPointerException( "script engine" ); // Fail fast. Seen for a mis-packaged OpenJDK 1.7. engine = _engine; init(); if( scriptFile != null ) inc( scriptFile ); } private final void init() throws ScriptException { engine.put( "vo", JavaScriptIncluder.this ); // engine.put( engine.FILENAME, "internal/votorola.g.script.JavaScriptIncluder" ); // for nicer error messages // engine.eval( "function include( path ) { vo.inc( path ); }" ); //// but then include errors (file not found) are reported against this wrapper function (line #1), rather than the including file } // ------------------------------------------------------------------------------------ /** Returns the engine in which included modules are executed. */ public ScriptEngine engine() { return engine; } private final ScriptEngine engine; /** Includes a JavaScript file by executing it in the engine. * * @param file the absolute path to the file */ public void inc( File file ) throws IOException, ScriptException { file = file.getCanonicalFile(); if( !file.isAbsolute() ) throw new ScriptException( "relative path to include file, not supported: " + file, scriptIfNotSame(file), -1 ); // Java resolves relative File paths against user.dir (System property), which // should not be altered until Java bug 4117557 is fixed. if( scriptFile == null ) scriptFile = file; final Object filenameOld = engine.get( ScriptEngine.FILENAME ); try { ScriptEngineX.eval( file, engine ); } catch( IOException x ) { throw (ScriptException)new ScriptException( "unable to include file: " + file, scriptIfNotSame(file), -1 ).initCause( x ); } finally { if( !ObjectX.nullEquals( filenameOld, engine.get(ScriptEngine.FILENAME) )) { engine.put( ScriptEngine.FILENAME, filenameOld ); // no longer compiling, so revert back } } } /** Includes a JavaScript file by executing it in the engine. * * @param path the file's absolute or relative path, * per {@linkplain #resolveFile(String) resolveFile}(path) */ public void inc( String path ) throws IOException, ScriptException { inc( resolveFile( path )); } /** The same as (Invocable)engine().{@linkplain * Invocable#invokeFunction(String,Object[]) invokeFunction}(name,args), but throws * MisconfigurationException instead of NoSuchMethodException. */ public Object invokeKnownFunction( String name, Object... args ) throws ScriptException { try { return ((Invocable)engine()).invokeFunction( name, args ); } catch( NoSuchMethodException x ) { throw new MisconfigurationException( "unable to invoke function '" + name + "'", scriptFile(), x ); } } /** The path to the first file that was executed by inclusion. It is guaranteed to be * in canonical form. * * @return path to first file that was executed, or null if none was executed * * @see #inc(String) * @see #inc(File) */ public File scriptFile() { return scriptFile; } private File scriptFile = null; // till one is included //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static ScriptEngine newDefaultEngine() throws ScriptException { return new ScriptEngineManager().getEngineByMimeType( "application/javascript" ); } /** Resolves a relative path into a file. * * @param path The path. It must not contain any shell variable, or tilde ~. * It may be either absolute or relative. If relative, * it is resolved either against the script file currently being compiled; * or against the main script file. The latter is the normal case * when invoking functions, because they are usually invoked after compilation. */ private File resolveFile( String path ) throws ScriptException { final String compilingFilename = (String)engine.get( ScriptEngine.FILENAME ); try { final URI baseURI; if( compilingFilename == null ) baseURI = scriptFile.toURI(); else baseURI = new URI( "file", compilingFilename, /*fragment*/null ); return new File( baseURI.resolve( path )); } catch( URISyntaxException x ) { final ScriptException xS = new ScriptException( "unable to resolve file path: " + path, scriptIfNotSame(path), -1 ); xS.initCause( x ); throw xS; } } private String scriptIfNotSame( File file ) { if( scriptFile == null || scriptFile.equals(file) ) return null; return scriptFile.getPath(); } private String scriptIfNotSame( String path ) { if( scriptFile == null || scriptFile.getPath().equals(path) ) return null; return scriptFile.getPath(); } }