/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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.apache.tinkerpop.gremlin.jsr223; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.script.Bindings; import javax.script.ScriptContext; import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.ServiceConfigurationError; import java.util.ServiceLoader; import java.util.stream.Collectors; import java.util.stream.Stream; /** * The {@code ScriptEngineManager} implements a discovery, instantiation and configuration mechanism for * {@link GremlinScriptEngine} classes and also maintains a collection of key/value pairs storing state shared by all * engines created by it. This class uses the {@code ServiceProvider} mechanism to enumerate all the * implementations of <code>GremlinScriptEngineFactory</code>. The <code>ScriptEngineManager</code> provides a method * to return a list of all these factories as well as utility methods which look up factories on the basis of language * name, file extension and mime type. * <p/> * The {@code Bindings} of key/value pairs, referred to as the "Global Scope" maintained by the manager is available * to all instances of @code ScriptEngine} created by the {@code GremlinScriptEngineManager}. The values * in the {@code Bindings} are generally exposed in all scripts. * <p/> * This class is based quite heavily on the workings of the {@code ScriptEngineManager} supplied in the * {@code javax.script} packages, but adds some additional features that are specific to Gremlin and TinkerPop. * Unfortunately, it's not easily possible to extend {@code ScriptEngineManager} directly as there certain behaviors * don't appear to be be straightforward to implement and member variables are all private. It is important to note * that this class is designed to provide support for "Gremlin-enabled" {@code ScriptEngine} instances (i.e. those * that extend from {@link GremlinScriptEngine}) and is not meant to manage just any {@code ScriptEngine} instance * that may be on the path. * <p/> * As this is a "Gremlin" {@code ScriptEngine}, certain common imports are automatically applied when a * {@link GremlinScriptEngine} is instantiated via the {@link GremlinScriptEngineFactory}.. Initial imports from * gremlin-core come from the {@link CoreImports}. * * @author Stephen Mallette (http://stephen.genoprime.com) */ public class DefaultGremlinScriptEngineManager implements GremlinScriptEngineManager { private static final Logger logger = LoggerFactory.getLogger(DefaultGremlinScriptEngineManager.class); /** * Set of script engine factories discovered. */ private final HashSet<GremlinScriptEngineFactory> engineSpis = new HashSet<>(); /** * Map of engine name to script engine factory. */ private final HashMap<String, GremlinScriptEngineFactory> nameAssociations = new HashMap<>(); /** * Map of script file extension to script engine factory. */ private final HashMap<String, GremlinScriptEngineFactory> extensionAssociations = new HashMap<>(); /** * Map of script script MIME type to script engine factory. */ private final HashMap<String, GremlinScriptEngineFactory> mimeTypeAssociations = new HashMap<>(); /** * Global bindings associated with script engines created by this manager. */ private Bindings globalScope = new ConcurrentBindings(); /** * List of extensions for the {@link GremlinScriptEngineManager} which will be used to supply * {@link Customizer} instances to {@link GremlinScriptEngineFactory} that are instantiated. */ private List<GremlinPlugin> plugins = new ArrayList<>(); /** * The effect of calling this constructor is the same as calling * {@code DefaultGremlinScriptEngineManager(Thread.currentThread().getContextClassLoader())}. */ public DefaultGremlinScriptEngineManager() { final ClassLoader ctxtLoader = Thread.currentThread().getContextClassLoader(); initEngines(ctxtLoader); } /** * This constructor loads the implementations of {@link GremlinScriptEngineFactory} visible to the given * {@code ClassLoader} using the {@code ServiceLoader} mechanism. If loader is <code>null</code>, the script * engine factories that are bundled with the platform and that are in the usual extension directories * (installed extensions) are loaded. */ public DefaultGremlinScriptEngineManager(final ClassLoader loader) { initEngines(loader); } @Override public List<Customizer> getCustomizers(final String scriptEngineName) { final List<Customizer> pluginCustomizers = plugins.stream().flatMap(plugin -> { final Optional<Customizer[]> customizers = plugin.getCustomizers(scriptEngineName); return Stream.of(customizers.orElse(new Customizer[0])); }).collect(Collectors.toList()); return pluginCustomizers; } @Override public void addPlugin(final GremlinPlugin plugin) { // TODO: should modules be a set based on "name" to ensure uniqueness? not sure what bad stuff can happen with dupes if (plugin != null) plugins.add(plugin); } /** * Stores the specified {@code Bindings} as a global for all {@link GremlinScriptEngine} objects created by it. * If the bindings are to be updated by multiple threads it is recommended that a {@link ConcurrentBindings} * instance is supplied. * * @throws IllegalArgumentException if bindings is null. */ @Override public synchronized void setBindings(final Bindings bindings) { if (null == bindings) throw new IllegalArgumentException("Global scope cannot be null."); globalScope = bindings; } /** * Gets the bindings of the {@code Bindings} in global scope. */ @Override public Bindings getBindings() { return globalScope; } /** * Sets the specified key/value pair in the global scope. The key may not be null or empty. * * @throws IllegalArgumentException if key is null or empty. */ @Override public void put(final String key, final Object value) { if (null == key) throw new IllegalArgumentException("key may not be null"); if (key.isEmpty()) throw new IllegalArgumentException("key may not be empty"); globalScope.put(key, value); } /** * Gets the value for the specified key in the global scope. */ @Override public Object get(final String key) { return globalScope.get(key); } /** * Looks up and creates a {@link GremlinScriptEngine} for a given name. The algorithm first searches for a * {@link GremlinScriptEngineFactory} that has been registered as a handler for the specified name using the * {@link #registerEngineExtension(String, GremlinScriptEngineFactory)} method. If one is not found, it searches * the set of {@code GremlinScriptEngineFactory} instances stored by the constructor for one with the specified * name. If a {@code ScriptEngineFactory} is found by either method, it is used to create instance of * {@link GremlinScriptEngine}. * * @param shortName The short name of the {@link GremlinScriptEngine} implementation returned by the * {@link GremlinScriptEngineFactory#getNames} method. * @return A {@link GremlinScriptEngine} created by the factory located in the search. Returns {@code null} * if no such factory was found. The global scope of this manager is applied to the newly created * {@link GremlinScriptEngine} * @throws NullPointerException if shortName is {@code null}. */ @Override public GremlinScriptEngine getEngineByName(final String shortName) { if (null == shortName) throw new NullPointerException(); //look for registered name first Object obj; if (null != (obj = nameAssociations.get(shortName))) { final GremlinScriptEngineFactory spi = (GremlinScriptEngineFactory) obj; try { return createGremlinScriptEngine(spi); } catch (Exception exp) { logger.error(String.format("Could not create GremlinScriptEngine for %s", shortName), exp); } } for (GremlinScriptEngineFactory spi : engineSpis) { List<String> names = null; try { names = spi.getNames(); } catch (Exception exp) { logger.error("Could not get GremlinScriptEngine names", exp); } if (names != null) { for (String name : names) { if (shortName.equals(name)) { try { return createGremlinScriptEngine(spi); } catch (Exception exp) { logger.error(String.format("Could not create GremlinScriptEngine for %s", shortName), exp); } } } } } return null; } /** * Look up and create a {@link GremlinScriptEngine} for a given extension. The algorithm * used by {@link #getEngineByName(String)} is used except that the search starts by looking for a * {@link GremlinScriptEngineFactory} registered to handle the given extension using * {@link #registerEngineExtension(String, GremlinScriptEngineFactory)}. * * @return The engine to handle scripts with this extension. Returns {@code null} if not found. * @throws NullPointerException if extension is {@code null}. */ @Override public GremlinScriptEngine getEngineByExtension(final String extension) { if (null == extension) throw new NullPointerException(); //look for registered extension first Object obj; if (null != (obj = extensionAssociations.get(extension))) { final GremlinScriptEngineFactory spi = (GremlinScriptEngineFactory) obj; try { return createGremlinScriptEngine(spi); } catch (Exception exp) { logger.error(String.format("Could not create GremlinScriptEngine for %s", extension), exp); } } for (GremlinScriptEngineFactory spi : engineSpis) { List<String> exts = null; try { exts = spi.getExtensions(); } catch (Exception exp) { logger.error("Could not get GremlinScriptEngine extensions", exp); } if (exts == null) continue; for (String ext : exts) { if (extension.equals(ext)) { try { return createGremlinScriptEngine(spi); } catch (Exception exp) { logger.error(String.format("Could not create GremlinScriptEngine for %s", extension), exp); } } } } return null; } /** * Look up and create a {@link GremlinScriptEngine} for a given mime type. The algorithm used by * {@link #getEngineByName(String)} is used except that the search starts by looking for a * {@link GremlinScriptEngineFactory} registered to handle the given mime type using * {@link #registerEngineMimeType(String, GremlinScriptEngineFactory)}. * * @param mimeType The given mime type * @return The engine to handle scripts with this mime type. Returns {@code null} if not found. * @throws NullPointerException if mime-type is {@code null}. */ @Override public GremlinScriptEngine getEngineByMimeType(final String mimeType) { if (null == mimeType) throw new NullPointerException(); //look for registered types first Object obj; if (null != (obj = mimeTypeAssociations.get(mimeType))) { final GremlinScriptEngineFactory spi = (GremlinScriptEngineFactory) obj; try { return createGremlinScriptEngine(spi); } catch (Exception exp) { logger.error(String.format("Could not create GremlinScriptEngine for %s", mimeType), exp); } } for (GremlinScriptEngineFactory spi : engineSpis) { List<String> types = null; try { types = spi.getMimeTypes(); } catch (Exception exp) { logger.error("Could not get GremlinScriptEngine mimetypes", exp); } if (types == null) continue; for (String type : types) { if (mimeType.equals(type)) { try { return createGremlinScriptEngine(spi); } catch (Exception exp) { logger.error(String.format("Could not create GremlinScriptEngine for %s", mimeType), exp); } } } } return null; } /** * Returns a list whose elements are instances of all the {@link GremlinScriptEngineFactory} classes * found by the discovery mechanism. * * @return List of all discovered {@link GremlinScriptEngineFactory} objects. */ @Override public List<GremlinScriptEngineFactory> getEngineFactories() { final List<GremlinScriptEngineFactory> res = new ArrayList<>(engineSpis.size()); res.addAll(engineSpis.stream().collect(Collectors.toList())); return Collections.unmodifiableList(res); } /** * Registers a {@link GremlinScriptEngineFactory} to handle a language name. Overrides any such association found * using the discovery mechanism. * * @param name The name to be associated with the {@link GremlinScriptEngineFactory} * @param factory The class to associate with the given name. * @throws NullPointerException if any of the parameters is null. */ @Override public void registerEngineName(final String name, final GremlinScriptEngineFactory factory) { if (null == name || null == factory) throw new NullPointerException(); nameAssociations.put(name, factory); } /** * Registers a {@link GremlinScriptEngineFactory} to handle a mime type. Overrides any such association found using * the discovery mechanism. * * @param type The mime type to be associated with the {@link GremlinScriptEngineFactory}. * @param factory The class to associate with the given mime type. * @throws NullPointerException if any of the parameters is null. */ @Override public void registerEngineMimeType(final String type, final GremlinScriptEngineFactory factory) { if (null == type || null == factory) throw new NullPointerException(); mimeTypeAssociations.put(type, factory); } /** * Registers a {@link GremlinScriptEngineFactory} to handle an extension. Overrides any such association found * using the discovery mechanism. * * @param extension The extension type to be associated with the {@link GremlinScriptEngineFactory} * @param factory The class to associate with the given extension. * @throws NullPointerException if any of the parameters is null. */ @Override public void registerEngineExtension(final String extension, final GremlinScriptEngineFactory factory) { if (null == extension || null == factory) throw new NullPointerException(); extensionAssociations.put(extension, factory); } private ServiceLoader<GremlinScriptEngineFactory> getServiceLoader(final ClassLoader loader) { if (loader != null) { return ServiceLoader.load(GremlinScriptEngineFactory.class, loader); } else { return ServiceLoader.loadInstalled(GremlinScriptEngineFactory.class); } } private void initEngines(final ClassLoader loader) { Iterator<GremlinScriptEngineFactory> itty; try { final ServiceLoader<GremlinScriptEngineFactory> sl = AccessController.doPrivileged( (PrivilegedAction<ServiceLoader<GremlinScriptEngineFactory>>) () -> getServiceLoader(loader)); itty = sl.iterator(); } catch (ServiceConfigurationError err) { logger.error("Can't find GremlinScriptEngineFactory providers: " + err.getMessage(), err); // do not throw any exception here. user may want to manager their own factories using this manager // by explicit registration (by registerXXX) methods. return; } try { while (itty.hasNext()) { try { final GremlinScriptEngineFactory factory = itty.next(); factory.setCustomizerManager(this); engineSpis.add(factory); } catch (ServiceConfigurationError err) { logger.error("GremlinScriptEngineManager providers.next(): " + err.getMessage(), err); } } } catch (ServiceConfigurationError err) { logger.error("GremlinScriptEngineManager providers.hasNext(): " + err.getMessage(), err); // do not throw any exception here. user may want to manage their own factories using this manager // by explicit registration (by registerXXX) methods. } } private GremlinScriptEngine createGremlinScriptEngine(final GremlinScriptEngineFactory spi) { final GremlinScriptEngine engine = spi.getScriptEngine(); // merge in bindings that are marked with global scope. these get applied to all GremlinScriptEngine instances getCustomizers(spi.getEngineName()).stream() .filter(p -> p instanceof BindingsCustomizer) .map(p -> ((BindingsCustomizer) p)) .filter(bc -> bc.getScope() == ScriptContext.GLOBAL_SCOPE) .flatMap(bc -> bc.getBindings().entrySet().stream()) .forEach(kv -> { if (globalScope.containsKey(kv.getKey())) { logger.warn("Overriding the global binding [{}] - was [{}] and is now [{}]", kv.getKey(), globalScope.get(kv.getKey()), kv.getValue()); } globalScope.put(kv.getKey(), kv.getValue()); }); engine.setBindings(getBindings(), ScriptContext.GLOBAL_SCOPE); // merge in bindings that are marked with engine scope. there typically won't be any of these but it's just // here for completeness. bindings will typically apply with global scope only as engine scope will generally // be overridden at the time of eval() with the bindings that are supplied to it getCustomizers(spi.getEngineName()).stream() .filter(p -> p instanceof BindingsCustomizer) .map(p -> ((BindingsCustomizer) p)) .filter(bc -> bc.getScope() == ScriptContext.ENGINE_SCOPE) .forEach(bc -> engine.getBindings(ScriptContext.ENGINE_SCOPE).putAll(bc.getBindings())); final List<ScriptCustomizer> scriptCustomizers = getCustomizers(spi.getEngineName()).stream() .filter(p -> p instanceof ScriptCustomizer) .map(p -> ((ScriptCustomizer) p)) .collect(Collectors.toList()); // since the bindings aren't added until after the ScriptEngine is constructed, running init scripts that // require bindings creates a problem. as a result, init scripts are applied here scriptCustomizers.stream().flatMap(sc -> sc.getScripts().stream()). map(l -> String.join(System.lineSeparator(), l)).forEach(initScript -> { try { // need to apply global bindings here as part of the engine or else they don't get their binding types // registered by certain GremlinScriptEngine instances (pretty much talking about gremlin-groovy here) // where type checking is made important. this may not be a good generic way to handled this in the // long run, but for now we only have two GremlinScriptEngines to be concerned about so thus far it // presents no real pains. global bindings are applied automatically to the context via // AbstractScriptEngine.getScriptContext() - passing them again here to eval() will just make the // global bindings behave as engine bindings and then you get weird things happening (like local vars // becoming global). final Object initializedBindings = engine.eval(initScript); if (initializedBindings != null && initializedBindings instanceof Map) ((Map<String,Object>) initializedBindings).forEach((k,v) -> put(k,v)); } catch (Exception ex) { throw new IllegalStateException(ex); } }); return engine; } }