private Map<String, Object> mergeBindings( final Context context, final Bindings request, final Bindings... scopes) { Set<String> safeAttributes = null != request ? request.keySet() : Collections.EMPTY_SET; Map<String, Object> scope = new HashMap<String, Object>(); for (Map<String, Object> next : scopes) { if (null == next) { continue; } for (Map.Entry<String, Object> entry : next.entrySet()) { if (scope.containsKey(entry.getKey()) || safeAttributes.contains(entry.getKey())) { continue; } scope.put(entry.getKey(), entry.getValue()); } } // Make lazy deep copy if (!scope.isEmpty()) { scope = new LazyMap<String, Object>( new InnerMapFactory( scope, new OperationParameter(context, "DEFAULT", engine.getPersistenceConfig()))); } if (null == request || request.isEmpty()) { return scope; } else if (scope.isEmpty()) { return request; } else { request.putAll(scope); return request; } }
/** * Выполняет скрипт * * @param script Тело скрипта / код * @param language Язык скрипта (JavaScript) * @param bindings Переменные для привязки/передачи данных * @return Результат выполнения * @throws ScriptException Ошибка исполнения скрипта */ public static Object eval( String script, String language, Map<? extends String, ? extends Object> bindings) throws ScriptException { if (script == null) throw new IllegalArgumentException("script==null"); if (language == null) throw new IllegalArgumentException("language==null"); // if( bindings==null )throw new IllegalArgumentException("bindings==null"); ScriptEngine se = null; Map<String, ScriptEngine> map = getMapScriptEngines(); if (map.containsKey(language)) { se = map.get(language); } else { se = getScriptEngineManager().getEngineByName(language); if (se == null) throw new IllegalArgumentException(Text.template("language ({0}) not supported", language)); map.put(language, se); } Bindings seBindings = se.getBindings(javax.script.ScriptContext.ENGINE_SCOPE); if (bindings != null) seBindings.putAll(bindings); return se.eval(script); }
private ScriptEngines createScriptEngines() { // plugins already on the path - ones static to the classpath final List<GremlinPlugin> globalPlugins = new ArrayList<>(); ServiceLoader.load(GremlinPlugin.class).forEach(globalPlugins::add); return new ScriptEngines( se -> { // this first part initializes the scriptengines Map for (Map.Entry<String, EngineSettings> config : settings.entrySet()) { final String language = config.getKey(); se.reload( language, new HashSet<>(config.getValue().getImports()), new HashSet<>(config.getValue().getStaticImports()), config.getValue().getConfig()); } // use grabs dependencies and returns plugins to load final List<GremlinPlugin> pluginsToLoad = new ArrayList<>(globalPlugins); use.forEach( u -> { if (u.size() != 3) logger.warn( "Could not resolve dependencies for [{}]. Each entry for the 'use' configuration must include [groupId, artifactId, version]", u); else { logger.info("Getting dependencies for [{}]", u); pluginsToLoad.addAll(se.use(u.get(0), u.get(1), u.get(2))); } }); // now that all dependencies are in place, the imports can't get messed up if a plugin // tries to execute // a script (as the script engine appends the import list to the top of all scripts passed // to the engine). // only enable those plugins that are configured to be enabled. se.loadPlugins( pluginsToLoad .stream() .filter(plugin -> enabledPlugins.contains(plugin.getName())) .collect(Collectors.toList())); // initialization script eval can now be performed now that dependencies are present with // "use" for (Map.Entry<String, EngineSettings> config : settings.entrySet()) { final String language = config.getKey(); // script engine initialization files that fail will only log warnings - not fail server // initialization final AtomicBoolean hasErrors = new AtomicBoolean(false); config .getValue() .getScripts() .stream() .map(File::new) .filter( f -> { if (!f.exists()) { logger.warn( "Could not initialize {} ScriptEngine with {} as file does not exist", language, f); hasErrors.set(true); } return f.exists(); }) .map( f -> { try { return Pair.with(f, Optional.of(new FileReader(f))); } catch (IOException ioe) { logger.warn( "Could not initialize {} ScriptEngine with {} as file could not be read - {}", language, f, ioe.getMessage()); hasErrors.set(true); return Pair.with(f, Optional.<FileReader>empty()); } }) .filter(p -> p.getValue1().isPresent()) .map(p -> Pair.with(p.getValue0(), p.getValue1().get())) .forEachOrdered( p -> { try { final Bindings bindings = new SimpleBindings(); bindings.putAll(this.globalBindings); // evaluate init scripts with hard reference so as to ensure it doesn't get // garbage collected bindings.put( GremlinGroovyScriptEngine.KEY_REFERENCE_TYPE, GremlinGroovyScriptEngine.REFERENCE_TYPE_HARD); se.eval(p.getValue1(), bindings, language); // re-assign graph bindings back to global bindings and grab TraversalSource // creations. // prevent assignment of non-graph implementations just in case someone // tries to overwrite // them in the init bindings .entrySet() .stream() .filter(promoteBinding) .forEach(kv -> this.globalBindings.put(kv.getKey(), kv.getValue())); logger.info("Initialized {} ScriptEngine with {}", language, p.getValue0()); } catch (ScriptException sx) { hasErrors.set(true); logger.warn( "Could not initialize {} ScriptEngine with {} as script could not be evaluated - {}", language, p.getValue0(), sx.getMessage()); } }); } }); }
/** * Evaluate a script and allow for the submission of both a transform {@link Function} and {@link * Consumer}. The {@link Function} will transform the result after script evaluates but before * transaction commit and before the returned {@link CompletableFuture} is completed. The {@link * Consumer} will take the result for additional processing after the script evaluates and after * the {@link CompletableFuture} is completed, but before the transaction is committed. * * @param script the script to evaluate * @param language the language to evaluate it in * @param boundVars the bindings to evaluate in the context of the script * @param transformResult a {@link Function} that transforms the result - can be {@code null} * @param withResult a {@link Consumer} that accepts the result - can be {@code null} */ public CompletableFuture<Object> eval( final String script, final String language, final Bindings boundVars, final Function<Object, Object> transformResult, final Consumer<Object> withResult) { final String lang = Optional.ofNullable(language).orElse("gremlin-groovy"); logger.debug( "Preparing to evaluate script - {} - in thread [{}]", script, Thread.currentThread().getName()); final Bindings bindings = new SimpleBindings(); bindings.putAll(this.globalBindings); bindings.putAll(boundVars); beforeEval.accept(bindings); final CompletableFuture<Object> evaluationFuture = new CompletableFuture<>(); final FutureTask<Void> f = new FutureTask<>( () -> { try { logger.debug( "Evaluating script - {} - in thread [{}]", script, Thread.currentThread().getName()); final Object o = scriptEngines.eval(script, bindings, lang); // apply a transformation before sending back the result - useful when trying to // force serialization // in the same thread that the eval took place given ThreadLocal nature of graphs as // well as some // transactional constraints final Object result = null == transformResult ? o : transformResult.apply(o); evaluationFuture.complete(result); // a mechanism for taking the final result and doing something with it in the same // thread, but // AFTER the eval and transform are done and that future completed. this provides a // final means // for working with the result in the same thread as it was eval'd if (withResult != null) withResult.accept(result); afterSuccess.accept(bindings); } catch (Exception ex) { final Throwable root = null == ex.getCause() ? ex : ExceptionUtils.getRootCause(ex); // thread interruptions will typically come as the result of a timeout, so in those // cases, // check for that situation and convert to TimeoutException if (root instanceof InterruptedException) evaluationFuture.completeExceptionally( new TimeoutException( String.format( "Script evaluation exceeded the configured threshold of %s ms for request [%s]: %s", scriptEvaluationTimeout, script, root.getMessage()))); else { afterFailure.accept(bindings, root); evaluationFuture.completeExceptionally(root); } } return null; }); executorService.execute(f); if (scriptEvaluationTimeout > 0) { // Schedule a timeout in the thread pool for future execution final ScheduledFuture<?> sf = scheduledExecutorService.schedule( () -> { logger.info( "Timing out script - {} - in thread [{}]", script, Thread.currentThread().getName()); if (!f.isDone()) { afterTimeout.accept(bindings); f.cancel(true); } }, scriptEvaluationTimeout, TimeUnit.MILLISECONDS); // Cancel the scheduled timeout if the eval future is complete or the script evaluation failed // with exception evaluationFuture.handleAsync( (v, t) -> { logger.debug( "Killing scheduled timeout on script evaluation as the eval completed (possibly with exception)."); return sf.cancel(true); }); } return evaluationFuture; }