Repository: jclouds-karaf Updated Branches: refs/heads/fix-script-engine [created] 0b99dad1a
Try to use an OSGi-compliant way of loading JSR 223 script engines Project: http://git-wip-us.apache.org/repos/asf/jclouds-karaf/repo Commit: http://git-wip-us.apache.org/repos/asf/jclouds-karaf/commit/0b99dad1 Tree: http://git-wip-us.apache.org/repos/asf/jclouds-karaf/tree/0b99dad1 Diff: http://git-wip-us.apache.org/repos/asf/jclouds-karaf/diff/0b99dad1 Branch: refs/heads/fix-script-engine Commit: 0b99dad1aa7786eaf11028bf91fc1d272c189f0b Parents: 960a9c7 Author: Andrew Phillips <[email protected]> Authored: Tue Sep 6 23:28:19 2016 -0400 Committer: Andrew Phillips <[email protected]> Committed: Tue Sep 6 23:28:19 2016 -0400 ---------------------------------------------------------------------- .../commands/table/BasicShellTableFactory.java | 9 +- .../table/internal/OSGiScriptEngine.java | 79 ++++++ .../table/internal/OSGiScriptEngineFactory.java | 83 ++++++ .../table/internal/OSGiScriptEngineManager.java | 272 +++++++++++++++++++ .../table/internal/ScriptEngineShellTable.java | 12 +- .../OSGI-INF/blueprint/jclouds-commands.xml | 8 +- feature/src/main/resources/feature.xml | 1 - 7 files changed, 452 insertions(+), 12 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/jclouds-karaf/blob/0b99dad1/commands/src/main/java/org/jclouds/karaf/commands/table/BasicShellTableFactory.java ---------------------------------------------------------------------- diff --git a/commands/src/main/java/org/jclouds/karaf/commands/table/BasicShellTableFactory.java b/commands/src/main/java/org/jclouds/karaf/commands/table/BasicShellTableFactory.java index 1769c7f..afb368a 100644 --- a/commands/src/main/java/org/jclouds/karaf/commands/table/BasicShellTableFactory.java +++ b/commands/src/main/java/org/jclouds/karaf/commands/table/BasicShellTableFactory.java @@ -24,10 +24,13 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import javax.script.ScriptEngineManager; + /** * A basic {@link org.jclouds.karaf.commands.table.ShellTableFactory} implementation which is backed by a {@link Map}. */ public class BasicShellTableFactory implements ShellTableFactory { + private ScriptEngineManager scriptEngineManager; private final Map properties = new HashMap(); /** @@ -49,7 +52,7 @@ public class BasicShellTableFactory implements ShellTableFactory { List<String> headers = Arrays.asList(headersValue.split(delimiter)); List<String> expressions = Arrays.asList(expressionsValue.split(delimiter)); List<String> alignments = Arrays.asList(alignValue.split(delimiter)); - ShellTable shellTable = new ScriptEngineShellTable(engine); + ShellTable shellTable = new ScriptEngineShellTable(scriptEngineManager, engine); shellTable.setType(type); shellTable.setHeaders(headers); @@ -63,4 +66,8 @@ public class BasicShellTableFactory implements ShellTableFactory { public Map getProperties() { return properties; } + + public void setScriptEngineManager(ScriptEngineManager scriptEngineManager) { + this.scriptEngineManager = scriptEngineManager; + } } http://git-wip-us.apache.org/repos/asf/jclouds-karaf/blob/0b99dad1/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngine.java ---------------------------------------------------------------------- diff --git a/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngine.java b/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngine.java new file mode 100644 index 0000000..119cb15 --- /dev/null +++ b/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngine.java @@ -0,0 +1,79 @@ +/* + * Copyright 2005 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +// implementation copied from http://svn.apache.org/repos/asf/felix/trunk/mishell/src/main/java/org/apache/felix/mishell/OSGiScriptEngine.java +package org.jclouds.karaf.commands.table.internal; + +import java.io.Reader; + +import javax.script.Bindings; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptException; + +public class OSGiScriptEngine implements ScriptEngine{ + private ScriptEngine engine; + private OSGiScriptEngineFactory factory; + public OSGiScriptEngine(ScriptEngine engine, OSGiScriptEngineFactory factory){ + this.engine=engine; + this.factory=factory; + } + public Bindings createBindings() { + return engine.createBindings(); + } + public Object eval(Reader reader, Bindings n) throws ScriptException { + return engine.eval(reader, n); + } + public Object eval(Reader reader, ScriptContext context) throws ScriptException { + return engine.eval(reader, context); + } + public Object eval(Reader reader) throws ScriptException { + return engine.eval(reader); + } + public Object eval(String script, Bindings n) throws ScriptException { + return engine.eval(script, n); + } + public Object eval(String script, ScriptContext context) throws ScriptException { + return engine.eval(script, context); + } + public Object eval(String script) throws ScriptException { + return engine.eval(script); + } + public Object get(String key) { + return engine.get(key); + } + public Bindings getBindings(int scope) { + return engine.getBindings(scope); + } + public ScriptContext getContext() { + return engine.getContext(); + } + public ScriptEngineFactory getFactory() { + return factory; + } + public void put(String key, Object value) { + engine.put(key, value); + } + public void setBindings(Bindings bindings, int scope) { + engine.setBindings(bindings, scope); + } + public void setContext(ScriptContext context) { + engine.setContext(context); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/jclouds-karaf/blob/0b99dad1/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngineFactory.java ---------------------------------------------------------------------- diff --git a/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngineFactory.java b/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngineFactory.java new file mode 100644 index 0000000..0281b82 --- /dev/null +++ b/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngineFactory.java @@ -0,0 +1,83 @@ +/* + * Copyright 2005 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.jclouds.karaf.commands.table.internal; + +import java.util.List; + +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; + +/** + * This is a wrapper class for the ScriptEngineFactory class that deals with context class loader issues + * It is necessary because engines (at least ruby) use the context classloader to find their resources (i.e., their "native" classes) + */ + +// implementation copied from http://svn.apache.org/repos/asf/felix/trunk/mishell/src/main/java/org/apache/felix/mishell/OSGiScriptEngineFactory.java +public class OSGiScriptEngineFactory implements ScriptEngineFactory{ + private ScriptEngineFactory factory; + private ClassLoader contextClassLoader; + public OSGiScriptEngineFactory (ScriptEngineFactory factory, ClassLoader contextClassLoader){ + this.factory=factory; + this.contextClassLoader=contextClassLoader; + } + public String getEngineName() { + return factory.getEngineName(); + } + public String getEngineVersion() { + return factory.getEngineVersion(); + } + public List<String> getExtensions() { + return factory.getExtensions(); + } + public String getLanguageName() { + return factory.getLanguageName(); + } + public String getLanguageVersion() { + return factory.getLanguageVersion(); + } + public String getMethodCallSyntax(String obj, String m, String... args) { + return factory.getMethodCallSyntax(obj, m, args); + } + public List<String> getMimeTypes() { + return factory.getMimeTypes(); + } + public List<String> getNames() { + return factory.getNames(); + } + public String getOutputStatement(String toDisplay) { + return factory.getOutputStatement(toDisplay); + } + public Object getParameter(String key) { + return factory.getParameter(key); + } + public String getProgram(String... statements) { + return factory.getProgram(statements); + } + public ScriptEngine getScriptEngine() { + ScriptEngine engine=null; + if(contextClassLoader!=null){ + ClassLoader old=Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(contextClassLoader); + engine=factory.getScriptEngine(); + Thread.currentThread().setContextClassLoader(old); + } + else engine=factory.getScriptEngine(); + return engine; + } + + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/jclouds-karaf/blob/0b99dad1/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngineManager.java ---------------------------------------------------------------------- diff --git a/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngineManager.java b/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngineManager.java new file mode 100644 index 0000000..7448b49 --- /dev/null +++ b/commands/src/main/java/org/jclouds/karaf/commands/table/internal/OSGiScriptEngineManager.java @@ -0,0 +1,272 @@ +/* + * Copyright 2005 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ +package org.jclouds.karaf.commands.table.internal; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.script.Bindings; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineFactory; +import javax.script.ScriptEngineManager; +import javax.script.SimpleBindings; + +import org.osgi.framework.Bundle; +import org.osgi.framework.BundleContext; + +/** + * This class acts as a delegate for all the available ScriptEngineManagers. Unluckily, the standard did not + * define it as an interface, so we need to extend it to allow polymorphism. However, no calls to super are used. + * It wraps all available ScriptEngineManagers in the OSGi ServicePlatform into a merged ScriptEngineManager. + * + * Internally, what this class does is creating ScriptEngineManagers for each bundle + * that contains a ScriptEngineFactory and includes a META-INF/services/javax.script.ScriptEngineFactory file. + * It assumes that the file contains a list of @link ScriptEngineFactory classes. For each bundle, it creates a + * ScriptEngineManager, then merges them. @link ScriptEngineFactory objects are wrapped + * into @link OSGiScriptEngineFactory objects to deal with problems of context class loader: + * Those scripting engines that rely on the ContextClassloader for finding resources need to use this wrapper + * and the @link OSGiScriptFactory. Mainly, jruby does. + * + * Note that even if no context classloader issues arose, it would still be needed to search manually for the + * factories and either use them directly (losing the mimeType/extension/shortName mechanisms for finding engines + * or manually registering them) or still use this class, which would be smarter. In the latter case, + * it would only be needed to remove the hack that temporarily sets the context classloader to the appropriate, + * bundle-related, class loader. + * + * Caveats: + * <ul><li> + * All factories are wrapped with an {@link OSGiScriptEngineFactory}. As Engines are not wrapped, + * calls like + * <code> + * ScriptEngineManager osgiManager=new OSGiScriptEngineManager(context);<br> + * ScriptEngine engine=osgiManager.getEngineByName("ruby"); + * ScriptEngineFactory factory=engine.getFactory() //this does not return the OSGiFactory wrapper + * factory.getScriptEngine(); //this might fail, as it does not use OSGiScriptEngineFactory wrapper + * </code> + * might result in unexpected errors. Future versions may wrap the ScriptEngine with a OSGiScriptEngine to solve this + * issue, but for the moment it is not needed. + * </li> + * + */ + +// implementation copied from http://svn.apache.org/repos/asf/felix/trunk/mishell/src/main/java/org/apache/felix/mishell/OSGiScriptEngineManager.java +public class OSGiScriptEngineManager extends ScriptEngineManager{ + private Bindings bindings; + private Map <ScriptEngineManager, ClassLoader> classLoaders; + private BundleContext context; + + public OSGiScriptEngineManager(BundleContext context){ + this.context=context; + bindings=new SimpleBindings(); + this.classLoaders=findManagers(context); + } + /** + * This method is the only one that is visible and not part of the ScriptEngineManager class. + * Its purpose is to find new managers that weren't available before, but keeping the globalScope bindings + * set. + * If you want to clean the bindings you can either get a fresh instance of OSGiScriptManager or + * setting up a new bindings object. + * This can be done with: + * <code> + * ScriptEngineManager manager=new OSGiScriptEngineManager(context); + * (...)//do stuff + * osgiManager=(OSGiScriptEngineManager)manager;//cast to ease reading + * osgiManager.reloadManagers(); + * + * manager.setBindings(new OSGiBindings());//or you can use your own bindings implementation + * + * </code> + * + */ + public void reloadManagers(){ + this.classLoaders=findManagers(context); + } + + public Object get(String key) { + return bindings.get(key); + } + + public Bindings getBindings() { + return bindings; + } + + public ScriptEngine getEngineByExtension(String extension) { + //TODO this is a hack to deal with context class loader issues + ScriptEngine engine=null; + for(ScriptEngineManager manager: classLoaders.keySet()){ + ClassLoader old=Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoaders.get(manager)); + engine=manager.getEngineByExtension(extension); + Thread.currentThread().setContextClassLoader(old); + if (engine!=null) break; + } + return engine; + } + + public ScriptEngine getEngineByMimeType(String mimeType) { + //TODO this is a hack to deal with context class loader issues + ScriptEngine engine=null; + for(ScriptEngineManager manager: classLoaders.keySet()){ + ClassLoader old=Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoaders.get(manager)); + engine=manager.getEngineByMimeType(mimeType); + Thread.currentThread().setContextClassLoader(old); + if (engine!=null) break; + } + return engine; + } + + public ScriptEngine getEngineByName(String shortName) { + //TODO this is a hack to deal with context class loader issues + for(ScriptEngineManager manager: classLoaders.keySet()){ + ClassLoader old=Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(classLoaders.get(manager)); + ScriptEngine engine=manager.getEngineByName(shortName); + Thread.currentThread().setContextClassLoader(old); + if (engine!=null){ + return new OSGiScriptEngine(engine, new OSGiScriptEngineFactory(engine.getFactory(), classLoaders.get(manager))); + } + } + return null; + } + public List<ScriptEngineFactory> getEngineFactories() { + List<ScriptEngineFactory> osgiFactories=new ArrayList<ScriptEngineFactory>(); + for(ScriptEngineManager engineManager: classLoaders.keySet()){ + for (ScriptEngineFactory factory : engineManager.getEngineFactories()){ + osgiFactories.add(new OSGiScriptEngineFactory(factory, classLoaders.get(engineManager))); + } + } + return osgiFactories; + } + + public void put(String key, Object value) { + bindings.put(key, value); + } + + public void registerEngineExtension(String extension, ScriptEngineFactory factory) { + for(ScriptEngineManager engineManager: classLoaders.keySet()) + engineManager.registerEngineExtension(extension, factory); + } + + public void registerEngineMimeType(String type, ScriptEngineFactory factory) { + for(ScriptEngineManager engineManager: classLoaders.keySet()) + engineManager.registerEngineMimeType(type, factory); + } + + public void registerEngineName(String name, ScriptEngineFactory factory) { + for(ScriptEngineManager engineManager: classLoaders.keySet()) + engineManager.registerEngineName(name, factory); + } + /** + * Follows the same behavior of @link javax.script.ScriptEngineManager#setBindings(Bindings) + * This means that the same bindings are applied to all the underlying managers. + * @param bindings + */ + public void setBindings(Bindings bindings) { + this.bindings=bindings; + for(ScriptEngineManager manager: classLoaders.keySet()){ + manager.setBindings(bindings); + } + } + + private Map<ScriptEngineManager, ClassLoader> findManagers(BundleContext context) { + Map<ScriptEngineManager, ClassLoader> managers=new HashMap<ScriptEngineManager, ClassLoader>(); + for (String factoryName : findFactoryCandidates(context)) { + // we do not really need the class, but we need the classloader + ClassLoader factoryLoader = tryGetClassLoader(factoryName); + if (factoryLoader == null) { + continue; + } + ScriptEngineManager manager = tryCreateScriptEngineManager(factoryName, factoryLoader); + if (manager != null) { + manager.setBindings(bindings); + managers.put(manager, factoryLoader); + } + } + return managers; + } + + private static ClassLoader tryGetClassLoader(String className) { + try { + return Class.forName(className).getClassLoader(); + } catch (ClassNotFoundException exception) { + // TODO: use a logger + System.err.println("WARN: Tried to load class " + + className + " but failed due to: " + exception.getMessage()); + return null; + } + } + + private static ScriptEngineManager tryCreateScriptEngineManager(String factoryName, ClassLoader factoryLoader) { + try { + return new ScriptEngineManager(factoryLoader); + } catch (Exception exception) { + // TODO: use a logger + System.err.println("WARN: Found ScriptEngineFactory candidate " + + factoryName + " but failed to create a factory due to: " + exception.getMessage()); + return null; + } + } + + /** + * Iterates through all bundles to get the available @link ScriptEngineFactory classes + * @return the names of the available ScriptEngineFactory classes + * @throws IOException + */ + private List<String> findFactoryCandidates(BundleContext context) { + Bundle[] bundles = context.getBundles(); + List<String> factoryCandidates = new ArrayList<String>(); + for (Bundle bundle : bundles) { + if (bundle == null) { + continue; + } + + if (bundle.getSymbolicName().equals("system.bundle")) { + continue; + } + Enumeration urls = bundle.findEntries("META-INF/services", + "javax.script.ScriptEngineFactory", false); + if (urls == null) { + continue; + } + + while (urls.hasMoreElements()) { + URL u = (URL) urls.nextElement(); + try { + BufferedReader reader = new BufferedReader( + new InputStreamReader(u.openStream())); + String line; + while ((line = reader.readLine()) != null) { + line = line.trim(); + // ignore comments and empty lines + if (!line.isEmpty() && !line.startsWith("#")) { + factoryCandidates.add(line); + } + } + } catch (IOException ignored) {} + } + } + return factoryCandidates; + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/jclouds-karaf/blob/0b99dad1/commands/src/main/java/org/jclouds/karaf/commands/table/internal/ScriptEngineShellTable.java ---------------------------------------------------------------------- diff --git a/commands/src/main/java/org/jclouds/karaf/commands/table/internal/ScriptEngineShellTable.java b/commands/src/main/java/org/jclouds/karaf/commands/table/internal/ScriptEngineShellTable.java index 9bce9e6..88ad5b8 100644 --- a/commands/src/main/java/org/jclouds/karaf/commands/table/internal/ScriptEngineShellTable.java +++ b/commands/src/main/java/org/jclouds/karaf/commands/table/internal/ScriptEngineShellTable.java @@ -26,18 +26,14 @@ import javax.script.ScriptEngineManager; * A shell table implementation that works with groovy expressions. */ public class ScriptEngineShellTable<D extends Object> extends BasicShellTable<D> { - - private final String engine; - private final ScriptEngineManager scriptEngineFactory = new ScriptEngineManager(); private final ScriptEngine scriptEngine; /** * Constructor * @param engine */ - public ScriptEngineShellTable(String engine) { - this.engine = engine; - this.scriptEngine = scriptEngineFactory.getEngineByName(engine); + public ScriptEngineShellTable(ScriptEngineManager scriptEngineManager, String engine) { + this.scriptEngine = scriptEngineManager.getEngineByName(engine); } /** @@ -51,9 +47,7 @@ public class ScriptEngineShellTable<D extends Object> extends BasicShellTable<D> try { scriptEngine.put(getType(), obj); result = String.valueOf(scriptEngine.eval(expression)); - } catch (Exception ex) { - //Ignore - } + } catch (Exception ignored) {} return result; } } http://git-wip-us.apache.org/repos/asf/jclouds-karaf/blob/0b99dad1/commands/src/main/resources/OSGI-INF/blueprint/jclouds-commands.xml ---------------------------------------------------------------------- diff --git a/commands/src/main/resources/OSGI-INF/blueprint/jclouds-commands.xml b/commands/src/main/resources/OSGI-INF/blueprint/jclouds-commands.xml index 30e3dd0..092740e 100644 --- a/commands/src/main/resources/OSGI-INF/blueprint/jclouds-commands.xml +++ b/commands/src/main/resources/OSGI-INF/blueprint/jclouds-commands.xml @@ -566,7 +566,13 @@ limitations under the License. <bean id="osFamilyCompleter" class="org.jclouds.karaf.commands.compute.completer.OsFamilyCompleter"/> - <bean id="shellTableFactory" class="org.jclouds.karaf.commands.table.internal.ManagedShellTableFactory"/> + <bean id="osgiScriptEngineManager" class="org.jclouds.karaf.commands.table.internal.OSGiScriptEngineManager"> + <argument ref="blueprintBundleContext"/> + </bean> + + <bean id="shellTableFactory" class="org.jclouds.karaf.commands.table.internal.ManagedShellTableFactory"> + <property name="scriptEngineManager" ref="osgiScriptEngineManager"/> + </bean> <service ref="shellTableFactory"> <interfaces> http://git-wip-us.apache.org/repos/asf/jclouds-karaf/blob/0b99dad1/feature/src/main/resources/feature.xml ---------------------------------------------------------------------- diff --git a/feature/src/main/resources/feature.xml b/feature/src/main/resources/feature.xml index 44c536d..b53cda8 100644 --- a/feature/src/main/resources/feature.xml +++ b/feature/src/main/resources/feature.xml @@ -418,7 +418,6 @@ limitations under the License. <feature name="jclouds-commands" description="Karaf Commands for jclouds" version="${project.version}" resolver="(obr)"> <configfile finalname="/etc/org.apache.jclouds.shell.cfg">mvn:org.apache.jclouds.karaf/jclouds-karaf/${project.version}/cfg/shell</configfile> <feature version='${project.version}'>jclouds-services</feature> - <bundle>mvn:org.apache.servicemix.specs/org.apache.servicemix.specs.scripting-api-1.0/${scripting.api.bundle.version}</bundle> <bundle dependency="true">mvn:org.codehaus.groovy/groovy/${groovy.version}</bundle> <bundle dependency="true">mvn:org.codehaus.groovy/groovy-jsr223/${groovy.version}</bundle> <bundle>mvn:org.apache.jclouds.karaf/core/${project.version}</bundle>
