From 1b1eecaa4b940983c6eef282565e1608ca3f7d90 Mon Sep 17 00:00:00 2001 From: thzero Date: Sat, 2 Jul 2022 13:55:09 -0500 Subject: [PATCH] refactor the graaljs integration to allow Java code to be called; updated the simulation extension and scripting to due to a change in the FlightConfiguration --- core/OpenRocket Core.iml | 27 ++ .../scripting/GraalJSScriptEngineFactory.java | 99 +++++ .../scripting/ScriptEngineManagerRedux.java | 412 ++++++------------ .../extension/impl/ScriptingUtil.java | 43 +- .../Simulation extensions and scripting.ork | Bin 19459 -> 19489 bytes 5 files changed, 290 insertions(+), 291 deletions(-) create mode 100644 core/src/net/sf/openrocket/scripting/GraalJSScriptEngineFactory.java diff --git a/core/OpenRocket Core.iml b/core/OpenRocket Core.iml index 8a5fd8008..dce24f46b 100644 --- a/core/OpenRocket Core.iml +++ b/core/OpenRocket Core.iml @@ -270,5 +270,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/net/sf/openrocket/scripting/GraalJSScriptEngineFactory.java b/core/src/net/sf/openrocket/scripting/GraalJSScriptEngineFactory.java new file mode 100644 index 000000000..324fb1aef --- /dev/null +++ b/core/src/net/sf/openrocket/scripting/GraalJSScriptEngineFactory.java @@ -0,0 +1,99 @@ +package net.sf.openrocket.scripting; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import java.util.*; + +import com.oracle.truffle.js.scriptengine.GraalJSScriptEngine; + +import org.graalvm.polyglot.Context; +import org.graalvm.polyglot.HostAccess; + +public class GraalJSScriptEngineFactory implements ScriptEngineFactory { + private static final String ENGINE_NAME = "Graal.js"; + private static final String LANGUAGE = "ECMAScript"; + private static final String LANGUAGE_VERSION = "ECMAScript 262 Edition 11"; + private static final String[] NAMES = new String[]{"js", "JS", "JavaScript", "javascript", LANGUAGE, LANGUAGE.toLowerCase(), ENGINE_NAME, ENGINE_NAME.toLowerCase(), "Graal-js", "graal-js", "Graal.JS", "Graal-JS", "GraalJS", "GraalJSPolyglot"}; + private static final String[] MIME_TYPES = new String[]{"application/javascript", "application/ecmascript", "text/javascript", "text/ecmascript"}; + private static final String[] EXTENSIONS = new String[]{"js", "mjs"}; + private static final List names; + private static final List mimeTypes; + private static final List extensions; + + public GraalJSScriptEngineFactory() { + } + + public ScriptEngine getScriptEngine() { + return GraalJSScriptEngine.create(null, + Context.newBuilder("js") + .allowHostAccess(HostAccess.ALL) + .allowHostClassLookup(s -> true) + .option("js.ecmascript-version", "2021")); + } + + public String getEngineName() { + return ENGINE_NAME; + } + + public String getEngineVersion() { + return ((GraalJSScriptEngine)getScriptEngine()).getPolyglotEngine().getVersion(); + } + + public List getExtensions() { + return extensions; + } + + public String getLanguageVersion() { + return LANGUAGE_VERSION; + } + + public String getLanguageName() { + return LANGUAGE; + } + + public List getMimeTypes() { + return mimeTypes; + } + + public List getNames() { + return names; + } + + public String getMethodCallSyntax(final String obj, final String method, final String... args) { + return null; + } + + public String getOutputStatement(final String toDisplay) { + return "print(" + toDisplay + ")"; + } + + public Object getParameter(String key) { + switch (key) { + case "javax.script.name": + return "javascript"; + case "javax.script.engine": + return this.getEngineName(); + case "javax.script.engine_version": + return this.getEngineVersion(); + case "javax.script.language": + return this.getLanguageName(); + case "javax.script.language_version": + return this.getLanguageVersion(); + default: + return null; + } + } + + public String getProgram(final String... statements) { + return null; + } + + static { + List nameList = Arrays.asList(NAMES); + List mimeTypeList = Arrays.asList(MIME_TYPES); + List extensionList = Arrays.asList(EXTENSIONS); + names = Collections.unmodifiableList(nameList); + mimeTypes = Collections.unmodifiableList(mimeTypeList); + extensions = Collections.unmodifiableList(extensionList); + } +} diff --git a/core/src/net/sf/openrocket/scripting/ScriptEngineManagerRedux.java b/core/src/net/sf/openrocket/scripting/ScriptEngineManagerRedux.java index 1fa3081f7..42566c8a1 100644 --- a/core/src/net/sf/openrocket/scripting/ScriptEngineManagerRedux.java +++ b/core/src/net/sf/openrocket/scripting/ScriptEngineManagerRedux.java @@ -66,8 +66,7 @@ public class ScriptEngineManagerRedux { * @see java.lang.Thread#getContextClassLoader */ public ScriptEngineManagerRedux() { - ClassLoader ctxtLoader = Thread.currentThread().getContextClassLoader(); - init(ctxtLoader); + init(Thread.currentThread().getContextClassLoader()); } /** @@ -83,96 +82,13 @@ public class ScriptEngineManagerRedux { init(loader); } - private void init(final ClassLoader loader) { - globalScope = new SimpleBindings(); - engineSpis = new TreeSet(Comparator.comparing( - ScriptEngineFactory::getEngineName, - Comparator.nullsLast(Comparator.naturalOrder())) - ); - nameAssociations = new HashMap(); - extensionAssociations = new HashMap(); - mimeTypeAssociations = new HashMap(); - initEngines(loader); - } - - private ServiceLoader getServiceLoader(final ClassLoader loader) { - if (loader != null) { - return ServiceLoader.load(ScriptEngineFactory.class, loader); - } else { - return ServiceLoader.loadInstalled(ScriptEngineFactory.class); - } - } - - private void initEngines(final ClassLoader loader) { - Iterator itr = null; - try { - ServiceLoader sl = AccessController.doPrivileged( - new PrivilegedAction>() { - @Override - public ServiceLoader run() { - return getServiceLoader(loader); - } - }); - - itr = sl.iterator(); - } catch (ServiceConfigurationError err) { - // } catch (Exception err) { - System.err.println("Can't find ScriptEngineFactory providers: " + - err.getMessage()); - if (DEBUG) { - err.printStackTrace(); - } - // do not throw any exception here. user may want to - // manage his/her own factories using this manager - // by explicit registratation (by registerXXX) methods. - return; - } - - try { - while (itr.hasNext()) { - try { - ScriptEngineFactory fact = itr.next(); - engineSpis.add(fact); - } catch (ServiceConfigurationError err) { - // } catch (Exception err) { - System.err.println("ScriptEngineManager providers.next(): " - + err.getMessage()); - if (DEBUG) { - err.printStackTrace(); - } - // one factory failed, but check other factories... - continue; - } - } - } catch (ServiceConfigurationError err) { - // } catch (Exception err) { - System.err.println("ScriptEngineManager providers.hasNext(): " - + err.getMessage()); - if (DEBUG) { - err.printStackTrace(); - } - // do not throw any exception here. user may want to - // manage his/her own factories using this manager - // by explicit registratation (by registerXXX) methods. - return; - } - } - /** - * setBindings stores the specified Bindings - * in the globalScope field. ScriptEngineManager sets this - * Bindings as global bindings for ScriptEngine - * objects created by it. - * - * @param bindings The specified Bindings - * @throws IllegalArgumentException if bindings is null. + * Gets the value for the specified key in the Global Scope + * @param key The key whose value is to be returned. + * @return The value for the specified key. */ - public void setBindings(Bindings bindings) { - if (bindings == null) { - throw new IllegalArgumentException("Global scope cannot be null."); - } - - globalScope = bindings; + public Object get(String key) { + return _globalScope.get(key); } /** @@ -183,27 +99,7 @@ public class ScriptEngineManagerRedux { * @return The globalScope field. */ public Bindings getBindings() { - return globalScope; - } - - /** - * Sets the specified key/value pair in the Global Scope. - * @param key Key to set - * @param value Value to set. - * @throws NullPointerException if key is null. - * @throws IllegalArgumentException if key is empty string. - */ - public void put(String key, Object value) { - globalScope.put(key, value); - } - - /** - * Gets the value for the specified key in the Global Scope - * @param key The key whose value is to be returned. - * @return The value for the specified key. - */ - public Object get(String key) { - return globalScope.get(key); + return _globalScope; } /** @@ -222,140 +118,46 @@ public class ScriptEngineManagerRedux { * created ScriptEngine. * @throws NullPointerException if shortName is null. */ - public ScriptEngine getEngineByName(String shortName) { - if (shortName == null) throw new NullPointerException(); - //look for registered name first - Object obj; - if (null != (obj = nameAssociations.get(shortName))) { - ScriptEngineFactory spi = (ScriptEngineFactory)obj; - try { - ScriptEngine engine = spi.getScriptEngine(); - engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE); - return engine; - } catch (Exception exp) { - if (DEBUG) exp.printStackTrace(); - } + private Map _factoriesByName = new HashMap<>(); + public synchronized ScriptEngine getEngineByName(String shortName) { + if (shortName == null) { + throw new NullPointerException(); } - for (ScriptEngineFactory spi : engineSpis) { - List names = null; - try { - names = spi.getNames(); - } catch (Exception exp) { - if (DEBUG) exp.printStackTrace(); - } + String key = shortName.toLowerCase(); + if (_factoriesByName.containsKey(key)) { + return getEngineByFactory(_factoriesByName.get(key)); + } - if (names != null) { - for (String name : names) { - if (shortName.equals(name)) { - try { - ScriptEngine engine = spi.getScriptEngine(); - engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE); - return engine; - } catch (Exception exp) { - if (DEBUG) exp.printStackTrace(); - } - } + // Look for registered name first + ScriptEngineFactory factoryNamed; + if (null != (factoryNamed = _nameAssociations.get(key))) { + try { + _factoriesByName.put(key, factoryNamed); + return getEngineByFactory(factoryNamed); + } catch (Exception exp) { + if (DEBUG) { + exp.printStackTrace(); } } } - return null; - } - - /** - * Look up and create a ScriptEngine for a given extension. The algorithm - * used by getEngineByName is used except that the search starts - * by looking for a ScriptEngineFactory registered to handle the - * given extension using registerEngineExtension. - * @param extension The given extension - * @return The engine to handle scripts with this extension. Returns null - * if not found. - * @throws NullPointerException if extension is null. - */ - public ScriptEngine getEngineByExtension(String extension) { - if (extension == null) throw new NullPointerException(); - //look for registered extension first - Object obj; - if (null != (obj = extensionAssociations.get(extension))) { - ScriptEngineFactory spi = (ScriptEngineFactory)obj; + Optional factoryName; + List names; + for (ScriptEngineFactory factory : _scriptEngineFactories) { try { - ScriptEngine engine = spi.getScriptEngine(); - engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE); - return engine; + factoryName = factory.getNames().stream().filter(l -> l.equalsIgnoreCase(shortName)).findFirst(); + if (factoryName.isPresent()) { + _factoriesByName.put(key, factory); + return getEngineByFactory(factory); + } } catch (Exception exp) { - if (DEBUG) exp.printStackTrace(); - } - } - - for (ScriptEngineFactory spi : engineSpis) { - List exts = null; - try { - exts = spi.getExtensions(); - } catch (Exception exp) { - if (DEBUG) exp.printStackTrace(); - } - if (exts == null) continue; - for (String ext : exts) { - if (extension.equals(ext)) { - try { - ScriptEngine engine = spi.getScriptEngine(); - engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE); - return engine; - } catch (Exception exp) { - if (DEBUG) exp.printStackTrace(); - } + if (DEBUG) { + exp.printStackTrace(); } } } - return null; - } - /** - * Look up and create a ScriptEngine for a given mime type. The algorithm - * used by getEngineByName is used except that the search starts - * by looking for a ScriptEngineFactory registered to handle the - * given mime type using registerEngineMimeType. - * @param mimeType The given mime type - * @return The engine to handle scripts with this mime type. Returns null - * if not found. - * @throws NullPointerException if mimeType is null. - */ - public ScriptEngine getEngineByMimeType(String mimeType) { - if (mimeType == null) throw new NullPointerException(); - //look for registered types first - Object obj; - if (null != (obj = mimeTypeAssociations.get(mimeType))) { - ScriptEngineFactory spi = (ScriptEngineFactory)obj; - try { - ScriptEngine engine = spi.getScriptEngine(); - engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE); - return engine; - } catch (Exception exp) { - if (DEBUG) exp.printStackTrace(); - } - } - - for (ScriptEngineFactory spi : engineSpis) { - List types = null; - try { - types = spi.getMimeTypes(); - } catch (Exception exp) { - if (DEBUG) exp.printStackTrace(); - } - if (types == null) continue; - for (String type : types) { - if (mimeType.equals(type)) { - try { - ScriptEngine engine = spi.getScriptEngine(); - engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE); - return engine; - } catch (Exception exp) { - if (DEBUG) exp.printStackTrace(); - } - } - } - } return null; } @@ -364,12 +166,19 @@ public class ScriptEngineManagerRedux { * found by the discovery mechanism. * @return List of all discovered ScriptEngineFactorys. */ - public List getEngineFactories() { - List res = new ArrayList(engineSpis.size()); - for (ScriptEngineFactory spi : engineSpis) { - res.add(spi); - } - return Collections.unmodifiableList(res); + public synchronized List getEngineFactories() { + return List.copyOf(_scriptEngineFactories); + } + + /** + * Sets the specified key/value pair in the Global Scope. + * @param key Key to set + * @param value Value to set. + * @throws NullPointerException if key is null. + * @throws IllegalArgumentException if key is empty string. + */ + public void put(String key, Object value) { + _globalScope.put(key, value); } /** @@ -380,51 +189,112 @@ public class ScriptEngineManagerRedux { * @throws NullPointerException if any of the parameters is null. */ public void registerEngineName(String name, ScriptEngineFactory factory) { - if (name == null || factory == null) throw new NullPointerException(); - nameAssociations.put(name, factory); + if (name == null || factory == null) { + throw new NullPointerException(); + } + + _nameAssociations.put(name.toLowerCase(), factory); } /** - * Registers a ScriptEngineFactory to handle a mime type. - * Overrides any such association found using the Discovery mechanism. + * setBindings stores the specified Bindings + * in the globalScope field. ScriptEngineManager sets this + * Bindings as global bindings for ScriptEngine + * objects created by it. * - * @param type The mime type to be associated with the - * ScriptEngineFactory. - * - * @param factory The class to associate with the given mime type. - * @throws NullPointerException if any of the parameters is null. + * @param bindings The specified Bindings + * @throws IllegalArgumentException if bindings is null. */ - public void registerEngineMimeType(String type, ScriptEngineFactory factory) { - if (type == null || factory == null) throw new NullPointerException(); - mimeTypeAssociations.put(type, factory); + public void setBindings(Bindings bindings) { + if (bindings == null) { + throw new IllegalArgumentException("Global scope cannot be null."); + } + + _globalScope = bindings; } - /** - * Registers a ScriptEngineFactory to handle an extension. - * Overrides any such association found using the Discovery mechanism. - * - * @param extension The extension type to be associated with the - * ScriptEngineFactory. - * @param factory The class to associate with the given extension. - * @throws NullPointerException if any of the parameters is null. - */ - public void registerEngineExtension(String extension, ScriptEngineFactory factory) { - if (extension == null || factory == null) throw new NullPointerException(); - extensionAssociations.put(extension, factory); + private ScriptEngine getEngineByFactory(ScriptEngineFactory factory) { + if (factory == null) { + return null; + } + + ScriptEngine engine = factory.getScriptEngine(); + engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE); + return engine; + } + + private ServiceLoader getServiceLoader(final ClassLoader loader) { + if (loader != null) { + return ServiceLoader.load(ScriptEngineFactory.class, loader); + } else { + return ServiceLoader.loadInstalled(ScriptEngineFactory.class); + } + } + + private void init(final ClassLoader loader) { + _scriptEngineFactories = new TreeSet<>(Comparator.comparing( + ScriptEngineFactory::getEngineName, + Comparator.nullsLast(Comparator.naturalOrder())) + ); + initEngines(loader); + } + + private void initEngines(final ClassLoader loader) { + Iterator itr; + try { + ServiceLoader loaders = AccessController.doPrivileged( + new PrivilegedAction>() { + @Override + public ServiceLoader run() { + return getServiceLoader(loader); + } + }); + + itr = loaders.iterator(); + } catch (ServiceConfigurationError err) { + // } catch (Exception err) { + System.err.println("Can't find ScriptEngineFactory providers: " + err.getMessage()); + if (DEBUG) { + err.printStackTrace(); + } + // do not throw any exception here. user may want to + // manage his/her own factories using this manager + // by explicit registration (by registerXXX) methods. + return; + } + + try { + while (itr.hasNext()) { + try { + ScriptEngineFactory factory = itr.next(); + _scriptEngineFactories.add(factory); + } catch (ServiceConfigurationError err) { + // } catch (Exception err) { + System.err.println("ScriptEngineManager providers.next(): " + err.getMessage()); + if (DEBUG) { + err.printStackTrace(); + } + // one factory failed, but check other factories... + } + } + } catch (ServiceConfigurationError err) { + // } catch (Exception err) { + System.err.println("ScriptEngineManager providers.hasNext(): " + err.getMessage()); + if (DEBUG) { + err.printStackTrace(); + } + // do not throw any exception here. user may want to + // manage his/her own factories using this manager + // by explicit registratation (by registerXXX) methods. + } } /** Set of script engine factories discovered. */ - private TreeSet engineSpis; + private TreeSet _scriptEngineFactories; /** Map of engine name to script engine factory. */ - private HashMap nameAssociations; - - /** Map of script file extension to script engine factory. */ - private HashMap extensionAssociations; - - /** Map of script MIME type to script engine factory. */ - private HashMap mimeTypeAssociations; + private HashMap _nameAssociations = new HashMap<>(); /** Global bindings associated with script engines created by this manager. */ - private Bindings globalScope; + private Bindings _globalScope = new SimpleBindings(); } diff --git a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java index a74f27d37..ef215ae3e 100644 --- a/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java +++ b/core/src/net/sf/openrocket/simulation/extension/impl/ScriptingUtil.java @@ -4,7 +4,6 @@ import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.util.Arrays; import java.util.List; import java.util.prefs.BackingStoreException; @@ -12,6 +11,7 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineFactory; import net.sf.openrocket.scripting.ScriptEngineManagerRedux; +import net.sf.openrocket.scripting.GraalJSScriptEngineFactory; import net.sf.openrocket.startup.Preferences; import net.sf.openrocket.util.ArrayList; import net.sf.openrocket.util.BugException; @@ -31,19 +31,25 @@ public class ScriptingUtil { /** The name to be chosen from a list of alternatives. If not found, will use the default name. */ private static final List PREFERRED_LANGUAGE_NAMES = List.of("JavaScript"); + + private static ScriptEngineManagerRedux manager; @Inject Preferences prefs; - - private static ScriptEngineManagerRedux manager; public ScriptingUtil() { if (manager == null) { // using the ScriptEngineManger from javax.script package pulls in the sun.misc.ServiceConfigurationError // which is removed in Java 9+ which causes a ClassNotFoundException to be thrown. manager = new ScriptEngineManagerRedux(); + + manager.registerEngineName("Javascript", new GraalJSScriptEngineFactory()); } } + + public ScriptEngine getEngineByName(String shortName) { + return manager.getEngineByName(shortName); + } /** * Return the preferred internal language name based on a script language name. @@ -59,29 +65,16 @@ public class ScriptingUtil { if (engine == null) { return null; } - return getLanguage(engine.getFactory()); - } - public ScriptEngine getEngineByName(String shortName) { - return manager.getEngineByName(shortName); + return getLanguageByFactory(engine.getFactory()); } public List getLanguages() { - List langs = new ArrayList<>(); + List languages = new ArrayList<>(); for (ScriptEngineFactory factory : manager.getEngineFactories()) { - langs.add(getLanguage(factory)); + languages.add(getLanguageByFactory(factory)); } - return langs; - } - - private String getLanguage(ScriptEngineFactory factory) { - for (String name : factory.getNames()) { - if (PREFERRED_LANGUAGE_NAMES.contains(name)) { - return name; - } - } - - return factory.getLanguageName(); + return languages; } /** @@ -126,6 +119,16 @@ public class ScriptingUtil { throw new BugException(e); } } + + private String getLanguageByFactory(ScriptEngineFactory factory) { + for (String name : factory.getNames()) { + if (PREFERRED_LANGUAGE_NAMES.contains(name)) { + return name; + } + } + + return factory.getLanguageName(); + } static String normalize(String script) { return script.replaceAll("\r", "").trim(); diff --git a/swing/resources/datafiles/examples/Simulation extensions and scripting.ork b/swing/resources/datafiles/examples/Simulation extensions and scripting.ork index a1592fc1166e071b01d92edfef27eb4a1cda4099..e9983fbf9c98ea487de0a527177183eb3d8a0ef6 100644 GIT binary patch delta 3926 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 delta 3894 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 E04QQxvj6}9