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}