All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.apache.tinkerpop.gremlin.jsr223.DefaultGremlinScriptEngineManager Maven / Gradle / Ivy

/*
 * 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 GremlinScriptEngineFactory. The ScriptEngineManager 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.
 * 

* 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. *

* 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. *

* 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 engineSpis = new HashSet<>(); /** * Map of engine name to script engine factory. */ private final HashMap nameAssociations = new HashMap<>(); /** * Map of script file extension to script engine factory. */ private final HashMap extensionAssociations = new HashMap<>(); /** * Map of script script MIME type to script engine factory. */ private final HashMap 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 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 null, 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 getCustomizers(final String scriptEngineName) { final List pluginCustomizers = plugins.stream().flatMap(plugin -> { final Optional 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 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 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 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 getEngineFactories() { final List 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 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 itty; try { final ServiceLoader sl = AccessController.doPrivileged( (PrivilegedAction>) () -> 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 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) initializedBindings).forEach((k,v) -> put(k,v)); } catch (Exception ex) { throw new IllegalStateException(ex); } }); return engine; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy