1 /*
   2  * Copyright (c) 2011, 2012, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package apple.applescript;
  27 
  28 import java.io.*;
  29 import java.nio.file.Files;
  30 import java.util.*;
  31 import java.util.Map.Entry;
  32 
  33 import javax.script.*;
  34 
  35 /**
  36  * AppleScriptEngine implements JSR 223 for AppleScript on Mac OS X
  37  */
  38 public class AppleScriptEngine implements ScriptEngine {
  39     private static native void initNative();
  40 
  41     private static native long createContextFrom(final Object object);
  42     private static native Object createObjectFrom(final long context);
  43     private static native void disposeContext(final long context);
  44 
  45     private static native long evalScript(final String script, long contextptr);
  46     private static native long evalScriptFromURL(final String filename, long contextptr);
  47 
  48     static {
  49         System.loadLibrary("AppleScriptEngine");
  50         initNative();
  51         TRACE("<static-init>");
  52     }
  53 
  54     static void checkSecurity() {
  55         final SecurityManager securityManager = System.getSecurityManager();
  56         if (securityManager != null) securityManager.checkExec("/usr/bin/osascript");
  57     }
  58 
  59     static void TRACE(final String str) {
  60 //        System.out.println(AppleScriptEngine.class.getName() + "." + str);
  61     }
  62 
  63     /**
  64      * Accessor for the ScriptEngine's long name variable
  65      * @return the long name of the ScriptEngine
  66      */
  67     protected static String getEngine() {
  68         TRACE("getEngine()");
  69         return AppleScriptEngineFactory.ENGINE_NAME;
  70     }
  71 
  72     /**
  73      * Accessor for the ScriptEngine's version
  74      * @return the version of the ScriptEngine
  75      */
  76     protected static String getEngineVersion() {
  77         TRACE("getEngineVersion()");
  78         return AppleScriptEngineFactory.ENGINE_VERSION;
  79     }
  80 
  81     /**
  82      * Accessor for the ScriptEngine's short name
  83      * @return the short name of the ScriptEngine
  84      */
  85     protected static String getName() {
  86         TRACE("getName()");
  87         return AppleScriptEngineFactory.ENGINE_SHORT_NAME;
  88     }
  89 
  90     /**
  91      * Accessor for the ScriptEngine's supported language name
  92      * @return the language the ScriptEngine supports
  93      */
  94     protected static String getLanguage() {
  95         TRACE("getLanguage()");
  96         return AppleScriptEngineFactory.LANGUAGE;
  97     }
  98 
  99     /**
 100      * The no argument constructor sets up the object with default members,
 101      * a factory for the engine and a fresh context.
 102      * @see com.apple.applescript.AppleScriptEngine#init()
 103      */
 104     public AppleScriptEngine() {
 105         TRACE("<ctor>()");
 106         // set our parent factory to be a new factory
 107         factory = AppleScriptEngineFactory.getFactory();
 108 
 109         // set up our noarg bindings
 110         setContext(new SimpleScriptContext());
 111         put(ARGV, "");
 112 
 113         init();
 114     }
 115 
 116     /**
 117      * All AppleScriptEngines share the same ScriptEngineFactory
 118      */
 119     private final ScriptEngineFactory factory;
 120 
 121     /**
 122      * The local context for the AppleScriptEngine
 123      */
 124     private ScriptContext context;
 125 
 126     /**
 127      * The constructor taking a factory as an argument sets the parent factory for
 128      * this engine to be the passed factory, and sets up the engine with a fresh context
 129      * @param factory
 130      * @see com.apple.applescript.AppleScriptEngine#init()
 131      */
 132     public AppleScriptEngine(final ScriptEngineFactory factory) {
 133         // inherit the factory passed to us
 134         this.factory = factory;
 135 
 136         // set up our noarg bindings
 137         setContext(new SimpleScriptContext());
 138         put(ARGV, "");
 139 
 140         init();
 141     }
 142 
 143     /**
 144      * The initializer populates the local context with some useful predefined variables:
 145      * <ul><li><code>javax_script_language_version</code> - the version of AppleScript that the AppleScriptEngine supports.</li>
 146      * <li><code>javax_script_language</code> - "AppleScript" -- the language supported by the AppleScriptEngine.</li>
 147      * <li><code>javax_script_engine</code> - "AppleScriptEngine" -- the name of the ScriptEngine.</li>
 148      * <li><code>javax_script_engine_version</code> - the version of the AppleScriptEngine</li>
 149      * <li><code>javax_script_argv</code> - "" -- AppleScript does not take arguments from the command line</li>
 150      * <li><code>javax_script_filename</code> - "" -- the currently executing filename</li>
 151      * <li><code>javax_script_name</code> - "AppleScriptEngine" -- the short name of the AppleScriptEngine</li>
 152      * <li><code>THREADING</code> - null -- the AppleScriptEngine does not support concurrency, you will have to implement thread-safeness yourself.</li></ul>
 153      */
 154     private void init() {
 155         TRACE("init()");
 156         // set up our context
 157 /* TODO -- name of current executable?  bad java documentation at:
 158  * http://java.sun.com/javase/6/docs/api/javax/script/ScriptEngine.html#FILENAME */
 159         put(ScriptEngine.FILENAME, "");
 160         put(ScriptEngine.ENGINE, getEngine());
 161         put(ScriptEngine.ENGINE_VERSION, getEngineVersion());
 162         put(ScriptEngine.NAME, getName());
 163         put(ScriptEngine.LANGUAGE, getLanguage());
 164         put(ScriptEngine.LANGUAGE_VERSION, getLanguageVersion());
 165 
 166         // TODO -- for now, err on the side of caution and say that we are NOT thread-safe
 167         put("THREADING", null);
 168     }
 169 
 170     /**
 171      * Uses the AppleScriptEngine to get the local AppleScript version
 172      * @return the version of AppleScript running on the system
 173      */
 174     protected String getLanguageVersion() {
 175         TRACE("AppleScriptEngine.getLanguageVersion()");
 176         try {
 177             final Object result = eval("get the version of AppleScript");
 178             if (result instanceof String) return (String)result;
 179         } catch (final ScriptException e) { e.printStackTrace(); }
 180         return "unknown";
 181     }
 182 
 183     /**
 184      * Implementation required by ScriptEngine parent<br />
 185      * Returns the factory parent of this AppleScriptEngine
 186      */
 187     public ScriptEngineFactory getFactory() {
 188         return factory;
 189     }
 190 
 191     /**
 192      * Implementation required by ScriptEngine parent<br />
 193      * Return the engine's context
 194      * @return this ScriptEngine's context
 195      */
 196     public ScriptContext getContext() {
 197         return context;
 198     }
 199 
 200     /**
 201      * Implementation required by ScriptEngine parent<br />
 202      * Set a new context for the engine
 203      * @param context the new context to install in the engine
 204      */
 205     public void setContext(final ScriptContext context) {
 206         this.context = context;
 207     }
 208 
 209     /**
 210      * Implementation required by ScriptEngine parent<br />
 211      * Create and return a new set of simple bindings.
 212      * @return a new and empty set of bindings
 213      */
 214     public Bindings createBindings() {
 215         return new SimpleBindings();
 216     }
 217 
 218     /**
 219      * Implementation required by ScriptEngine parent<br />
 220      * Return the engines bindings for the context indicated by the argument.
 221      * @param scope contextual scope to return.
 222      * @return the bindings in the engine for the scope indicated by the parameter
 223      */
 224     public Bindings getBindings(final int scope) {
 225         return context.getBindings(scope);
 226     }
 227 
 228     /**
 229      * Implementation required by ScriptEngine parent<br />
 230      * Sets the bindings for the indicated scope
 231      * @param bindings a set of bindings to assign to the engine
 232      * @param scope the scope that the passed bindings should be assigned to
 233      */
 234     public void setBindings(final Bindings bindings, final int scope) {
 235         context.setBindings(bindings, scope);
 236     }
 237 
 238     /**
 239      * Implementation required by ScriptEngine parent<br />
 240      * Insert a key and value into the engine's bindings (scope: engine)
 241      * @param key the key of the pair
 242      * @param value the value of the pair
 243      */
 244     public void put(final String key, final Object value) {
 245         getBindings(ScriptContext.ENGINE_SCOPE).put(key, value);
 246     }
 247 
 248     /**
 249      * Implementation required by ScriptEngine parent<br />
 250      * Get a value from the engine's bindings using a key (scope: engine)
 251      * @param key the key of the pair
 252      * @return the value of the pair
 253      */
 254     public Object get(final String key) {
 255         return getBindings(ScriptContext.ENGINE_SCOPE).get(key);
 256     }
 257 
 258     /**
 259      * Implementation required by ScriptEngine parent<br />
 260      * Passes the Reader argument, as well as the engine's context to a lower evaluation function.<br />
 261      * Prefers FileReader or BufferedReader wrapping FileReader as argument.
 262      * @param reader a Reader to AppleScript source or compiled AppleScript
 263      * @return an Object corresponding to the return value of the script
 264      * @see com.apple.applescript.AppleScriptEngine#eval(Reader, ScriptContext)
 265      */
 266     public Object eval(final Reader reader) throws ScriptException {
 267         return eval(reader, getContext());
 268     }
 269 
 270     /**
 271      * Implementation required by ScriptEngine parent<br />
 272      * Uses the passed bindings as the context for executing the passed script.
 273      * @param reader a stream to AppleScript source or compiled AppleScript
 274      * @param bindings a Bindings object representing the contexts to execute inside
 275      * @return the return value of the script
 276      * @see com.apple.applescript.AppleScriptEngine#eval(Reader, ScriptContext)
 277      */
 278     public Object eval(final Reader reader, final Bindings bindings) throws ScriptException {
 279         final Bindings tmp = getContext().getBindings(ScriptContext.ENGINE_SCOPE);
 280         getContext().setBindings(bindings, ScriptContext.ENGINE_SCOPE);
 281         final Object retval = eval(reader);
 282         getContext().setBindings(tmp, ScriptContext.ENGINE_SCOPE);
 283         return retval;
 284     }
 285 
 286     /**
 287      * Implementation required by ScriptEngine parent<br />
 288      * This function can execute either AppleScript source or compiled AppleScript and functions by writing the
 289      * contents of the Reader to a temporary file and then executing it with the engine's context.
 290      * @param reader
 291      * @param scriptContext
 292      * @return an Object corresponding to the return value of the script
 293      */
 294     public Object eval(final Reader reader, final ScriptContext context) throws ScriptException {
 295         checkSecurity();
 296 
 297         // write our passed reader to a temporary file
 298         File tmpfile;
 299         FileWriter tmpwrite;
 300         try {
 301             tmpfile = Files.createTempFile("AppleScriptEngine.", ".scpt").toFile();
 302             tmpwrite = new FileWriter(tmpfile);
 303 
 304             // read in our input and write directly to tmpfile
 305             /* TODO -- this may or may not be avoidable for certain Readers,
 306              * if a filename can be grabbed, it would be faster to get that and
 307              * use the underlying file than writing a temp file.
 308              */
 309             int data;
 310             while ((data = reader.read()) != -1) {
 311                 tmpwrite.write(data);
 312             }
 313             tmpwrite.close();
 314 
 315             // set up our context business
 316             final long contextptr = scriptContextToNSDictionary(context);
 317             try {
 318                 final long retCtx = evalScriptFromURL("file://" + tmpfile.getCanonicalPath(), contextptr);
 319                 Object retVal = (retCtx == 0) ? null : createObjectFrom(retCtx);
 320                 disposeContext(retCtx);
 321                 return retVal;
 322             } finally {
 323                 disposeContext(contextptr);
 324                 tmpfile.delete();
 325             }
 326         } catch (final IOException e) {
 327             throw new ScriptException(e);
 328         }
 329     }
 330 
 331     /**
 332      * Implementation required by ScriptEngine parent<br />
 333      * Evaluate an AppleScript script passed as a source string. Using the engine's built in context.
 334      * @param script the string to execute.
 335      * @return an Object representing the return value of the script
 336      * @see com.apple.applescript.AppleScriptEngine#eval(String, ScriptContext)
 337      */
 338     public Object eval(final String script) throws ScriptException {
 339         return eval(script, getContext());
 340     }
 341 
 342     /**
 343      * Implementation required by ScriptEngine parent<br />
 344      * Evaluate an AppleScript script passed as a source string with a custom ScriptContext.
 345      * @param script the AppleScript source to compile and execute.
 346      * @param scriptContext the context to execute the script under
 347      * @see com.apple.applescript.AppleScriptEngine#eval(String, ScriptContext)
 348      */
 349     public Object eval(final String script, final Bindings bindings) throws ScriptException {
 350         final Bindings tmp = getContext().getBindings(ScriptContext.ENGINE_SCOPE);
 351         getContext().setBindings(bindings, ScriptContext.ENGINE_SCOPE);
 352 
 353         final Object retval = eval(script);
 354         getContext().setBindings(tmp, ScriptContext.ENGINE_SCOPE);
 355 
 356         return retval;
 357     }
 358 
 359     /**
 360      * Implementation required by ScriptEngine parent
 361      * @param script
 362      * @param scriptContext
 363      */
 364     public Object eval(final String script, final ScriptContext context) throws ScriptException {
 365         checkSecurity();
 366         final long ctxPtr = scriptContextToNSDictionary(context);
 367         try {
 368             final long retCtx = evalScript(script, ctxPtr);
 369             Object retVal = (retCtx == 0) ? null : createObjectFrom(retCtx);
 370             disposeContext(retCtx);
 371             return retVal;
 372         } finally {
 373             disposeContext(ctxPtr);
 374         }
 375     }
 376 
 377     /**
 378      * Converts a ScriptContext into an NSDictionary
 379      * @param context ScriptContext for the engine
 380      * @return a pointer to an NSDictionary
 381      */
 382     private long scriptContextToNSDictionary(final ScriptContext context) throws ScriptException {
 383         final Map<String, Object> contextAsMap = new HashMap<String, Object>();
 384         for (final Entry<String, Object> e : context.getBindings(ScriptContext.ENGINE_SCOPE).entrySet()) {
 385             contextAsMap.put(e.getKey().replaceAll("\\.", "_"), e.getValue());
 386         }
 387         return createContextFrom(contextAsMap);
 388     }
 389 }