001package 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. 002 003import java.io.*; 004import java.net.*; 005import javax.script.*; 006import votorola.g.lang.*; 007 008 009/** A facility for including JavaScript modules in a running script. Scripts executed in 010 * the {@linkplain #engine() engine} are provided with a global instance of this includer 011 * (named <code>vo</code>), with which to load modules. For example: 012 * 013 * <pre class='vspace indent'>vo.{@linkplain #inc(String) inc}( '<var>ABS-OR-REL-PATH</var>/module.jsm' );</pre> 014 * 015 * <p>Script files must be encoded in UTF-8, or equivalently ASCII.</p> 016 */ 017public @ThreadRestricted final class JavaScriptIncluder 018{ 019 020 // cf. jrunscript GLOBALS.load(), in your jdk/docs/technotes/tools/share/jsdocs/index.html 021 022 023 /** Constructs a JavaScriptIncluder with a default engine, and no initial script. 024 */ 025 public JavaScriptIncluder() throws ScriptException 026 { 027 engine = newDefaultEngine(); 028 init(); 029 } 030 031 032 033 /** Constructs a JavaScriptIncluder with a default engine. 034 * 035 * @param scriptFile to execute initially, 036 * per {@linkplain #scriptFile() scriptFile}(); or null to execute none 037 */ 038 public JavaScriptIncluder( File scriptFile ) throws IOException, ScriptException 039 { 040 this( scriptFile, newDefaultEngine() ); 041 } 042 043 044 045 /** Constructs a JavaScriptIncluder with the specified engine. 046 * 047 * @param scriptFile to execute initially, 048 * per {@linkplain #scriptFile() scriptFile}(); or null to execute none 049 */ 050 public JavaScriptIncluder( final File scriptFile, ScriptEngine _engine ) 051 throws IOException, ScriptException 052 { 053 if( _engine == null ) throw new NullPointerException( "script engine" ); 054 // Fail fast. Seen for a mis-packaged OpenJDK 1.7. 055 056 engine = _engine; 057 init(); 058 if( scriptFile != null ) inc( scriptFile ); 059 } 060 061 062 063 private final void init() throws ScriptException 064 { 065 engine.put( "vo", JavaScriptIncluder.this ); 066 // engine.put( engine.FILENAME, "internal/votorola.g.script.JavaScriptIncluder" ); // for nicer error messages 067 // engine.eval( "function include( path ) { vo.inc( path ); }" ); 068 //// but then include errors (file not found) are reported against this wrapper function (line #1), rather than the including file 069 } 070 071 072 073 // ------------------------------------------------------------------------------------ 074 075 076 /** Returns the engine in which included modules are executed. 077 */ 078 public ScriptEngine engine() { return engine; } 079 080 081 private final ScriptEngine engine; 082 083 084 085 /** Includes a JavaScript file by executing it in the engine. 086 * 087 * @param file the absolute path to the file 088 */ 089 public void inc( File file ) throws IOException, ScriptException 090 { 091 file = file.getCanonicalFile(); 092 if( !file.isAbsolute() ) throw new ScriptException( 093 "relative path to include file, not supported: " + file, scriptIfNotSame(file), -1 ); 094 // Java resolves relative File paths against user.dir (System property), which 095 // should not be altered until Java bug 4117557 is fixed. 096 097 if( scriptFile == null ) scriptFile = file; 098 final Object filenameOld = engine.get( ScriptEngine.FILENAME ); 099 try 100 { 101 ScriptEngineX.eval( file, engine ); 102 } 103 catch( IOException x ) { throw (ScriptException)new ScriptException( "unable to include file: " + file, scriptIfNotSame(file), -1 ).initCause( x ); } 104 finally 105 { 106 if( !ObjectX.nullEquals( filenameOld, engine.get(ScriptEngine.FILENAME) )) 107 { 108 engine.put( ScriptEngine.FILENAME, filenameOld ); // no longer compiling, so revert back 109 } 110 } 111 } 112 113 114 115 /** Includes a JavaScript file by executing it in the engine. 116 * 117 * @param path the file's absolute or relative path, 118 * per {@linkplain #resolveFile(String) resolveFile}(path) 119 */ 120 public void inc( String path ) throws IOException, ScriptException { inc( resolveFile( path )); } 121 122 123 124 /** The same as (Invocable)engine().{@linkplain 125 * Invocable#invokeFunction(String,Object[]) invokeFunction}(name,args), but throws 126 * MisconfigurationException instead of NoSuchMethodException. 127 */ 128 public Object invokeKnownFunction( String name, Object... args ) throws ScriptException 129 { 130 try 131 { 132 return ((Invocable)engine()).invokeFunction( name, args ); 133 } 134 catch( NoSuchMethodException x ) 135 { 136 throw new MisconfigurationException( "unable to invoke function '" + name + "'", scriptFile(), x ); 137 } 138 } 139 140 141 142 /** The path to the first file that was executed by inclusion. It is guaranteed to be 143 * in canonical form. 144 * 145 * @return path to first file that was executed, or null if none was executed 146 * 147 * @see #inc(String) 148 * @see #inc(File) 149 */ 150 public File scriptFile() { return scriptFile; } 151 152 153 private File scriptFile = null; // till one is included 154 155 156 157//// P r i v a t e /////////////////////////////////////////////////////////////////////// 158 159 160 private static ScriptEngine newDefaultEngine() throws ScriptException 161 { 162 return new ScriptEngineManager().getEngineByMimeType( "application/javascript" ); 163 } 164 165 166 167 /** Resolves a relative path into a file. 168 * 169 * @param path The path. It must not contain any shell variable, or tilde ~. 170 * It may be either absolute or relative. If relative, 171 * it is resolved either against the script file currently being compiled; 172 * or against the main script file. The latter is the normal case 173 * when invoking functions, because they are usually invoked after compilation. 174 */ 175 private File resolveFile( String path ) throws ScriptException 176 { 177 final String compilingFilename = (String)engine.get( ScriptEngine.FILENAME ); 178 try 179 { 180 final URI baseURI; 181 if( compilingFilename == null ) baseURI = scriptFile.toURI(); 182 else baseURI = new URI( "file", compilingFilename, /*fragment*/null ); 183 184 return new File( baseURI.resolve( path )); 185 } 186 catch( URISyntaxException x ) 187 { 188 final ScriptException xS = new ScriptException( 189 "unable to resolve file path: " + path, scriptIfNotSame(path), -1 ); 190 xS.initCause( x ); 191 throw xS; 192 } 193 } 194 195 196 197 private String scriptIfNotSame( File file ) 198 { 199 if( scriptFile == null || scriptFile.equals(file) ) return null; 200 201 return scriptFile.getPath(); 202 } 203 204 205 206 private String scriptIfNotSame( String path ) 207 { 208 if( scriptFile == null || scriptFile.getPath().equals(path) ) return null; 209 210 return scriptFile.getPath(); 211 } 212 213 214}