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

org.apache.solr.update.processor.StatelessScriptUpdateProcessorFactory Maven / Gradle / Ivy

There is a newer version: 9.7.0
Show newest version
/*
 * 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.solr.update.processor;

import org.apache.solr.common.SolrException;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.core.SolrCore;
import org.apache.solr.core.SolrResourceLoader;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.request.LocalSolrQueryRequest;
import org.apache.solr.response.SolrQueryResponse;
import org.apache.solr.update.*;
import org.apache.solr.util.plugin.SolrCoreAware;
import org.apache.commons.io.IOUtils;
import org.apache.commons.io.FilenameUtils;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptEngineFactory;
import javax.script.ScriptException;

import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.LinkedHashSet;
import java.util.ArrayList;
import java.util.List;
import java.util.Collection;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 

* An update request processor factory that enables the use of update * processors implemented as scripts which can be loaded by the * {@link SolrResourceLoader} (usually via the conf dir for * the SolrCore). *

*

* This factory requires at least one configuration parameter named * script which may be the name of a script file as a string, * or an array of multiple script files. If multiple script files are * specified, they are executed sequentially in the order specified in the * configuration -- as if multiple factories were configured sequentially *

*

* Each script file is expected to declare functions with the same name * as each method in {@link UpdateRequestProcessor}, using the same * arguments. One slight deviation is in the optional return value from * these functions: If a script function has a boolean return * value, and that value is false then the processor will * cleanly terminate processing of the command and return, without forwarding * the command on to the next script or processor in the chain. * Due to limitations in the {@link ScriptEngine} API used by * this factory, it can not enforce that all functions exist on initialization, * so errors from missing functions will only be generated at runtime when * the chain attempts to use them. *

*

* The factory may also be configured with an optional "params" argument, * which can be an {@link NamedList} (or array, or any other simple Java * object) which will be put into the global scope for each script. *

*

* The following variables are define as global variables for each script: *

    *
  • req - The {@link SolrQueryRequest}
  • *
  • rsp - The {@link SolrQueryResponse}
  • *
  • logger - A {@link Logger} that can be used for logging purposes in the script
  • *
  • params - The "params" init argument in the factory configuration (if any)
  • *
*

* Internally this update processor uses JDK 6 scripting engine support, * and any {@link Invocable} implementations of ScriptEngine * that can be loaded using the Solr Plugin ClassLoader may be used. * By default, the engine used for each script is determined by the filed * extension (ie: a *.js file will be treated as a JavaScript script) but * this can be overridden by specifying an explicit "engine" name init * param for the factory, which identifies a registered name of a * {@link ScriptEngineFactory}. * (This may be particularly useful if multiple engines are available for * the same scripting language, and you wish to force the usage of a * particular engine because of known quirks) *

*

* A new {@link ScriptEngineManager} is created for each * SolrQueryRequest defining a "global" scope for the script(s) * which is request specific. Separate ScriptEngine instances * are then used to evaluate the script files, resulting in an "engine" scope * that is specific to each script. *

*

* A simple example... *

*
 * <processor class="solr.StatelessScriptUpdateProcessorFactory">
 *   <str name="script">updateProcessor.js</str>
 * </processor>
 * 
*

* A more complex example involving multiple scripts in different languages, * and a "params" NamedList that will be put into the global * scope of each script... *

*
 * <processor class="solr.StatelessScriptUpdateProcessorFactory">
 *   <arr name="script">
 *     <str name="script">first-processor.js</str>
 *     <str name="script">second-processor.py</str>
 *   </arr>
 *   <lst name="params">
 *     <bool name="a_bool_value">true</bool>
 *     <int name="and_int_value">3</int>
 *   </lst>
 * </processor>
 * 
*

* An example where the script file extensions are ignored, and an * explicit script engine is used.... *

*
 * <processor class="solr.StatelessScriptUpdateProcessorFactory">
 *   <arr name="script">
 *     <str name="script">first-processor.txt</str>
 *     <str name="script">second-processor.txt</str>
 *   </arr>
 *   <str name="engine">rhino</str>
 * </processor>
 * 
* * @since 4.0.0 */ public class StatelessScriptUpdateProcessorFactory extends UpdateRequestProcessorFactory implements SolrCoreAware { private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); private final static String SCRIPT_ARG = "script"; private final static String PARAMS_ARG = "params"; private final static String ENGINE_NAME_ARG = "engine"; private List scriptFiles; /** if non null, this is an override for the engine for all scripts */ private String engineName = null; private Object params = null; private SolrResourceLoader resourceLoader; private ScriptEngineCustomizer scriptEngineCustomizer; @Override public void init(NamedList args) { Collection scripts = args.removeConfigArgs(SCRIPT_ARG); if (scripts.isEmpty()) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "StatelessScriptUpdateProcessorFactory must be " + "initialized with at least one " + SCRIPT_ARG); } scriptFiles = new ArrayList<>(); for (String script : scripts) { scriptFiles.add(new ScriptFile(script)); } params = args.remove(PARAMS_ARG); Object engine = args.remove(ENGINE_NAME_ARG); if (engine != null) { if (engine instanceof String) { engineName = (String)engine; } else { throw new SolrException (SolrException.ErrorCode.SERVER_ERROR, "'" + ENGINE_NAME_ARG + "' init param must be a String (found: " + engine.getClass() + ")"); } } super.init(args); } @Override public UpdateRequestProcessor getInstance(SolrQueryRequest req, SolrQueryResponse rsp, UpdateRequestProcessor next) { List scriptEngines = null; scriptEngines = initEngines(req, rsp); return new ScriptUpdateProcessor(req, rsp, scriptEngines, next); } // TODO: Make this useful outside of tests, such that a ScriptEngineCustomizer could be looked up through the resource loader void setScriptEngineCustomizer(ScriptEngineCustomizer scriptEngineCustomizer) { this.scriptEngineCustomizer = scriptEngineCustomizer; } @Override public void inform(SolrCore core) { if (!core.getCoreDescriptor().isConfigSetTrusted()) { throw new SolrException(ErrorCode.UNAUTHORIZED, "The configset for this collection was uploaded without any authentication in place," + " and this operation is not available for collections with untrusted configsets. To use this component, re-upload the configset" + " after enabling authentication and authorization."); } resourceLoader = core.getResourceLoader(); // test that our engines & scripts are valid SolrQueryResponse rsp = new SolrQueryResponse(); SolrQueryRequest req = new LocalSolrQueryRequest(core, new ModifiableSolrParams()); try { initEngines(req, rsp); } catch (Exception e) { String msg = "Unable to initialize scripts: " + e.getMessage(); log.error(msg, e); throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, msg, e); } finally { req.close(); } } //================================================ Helper Methods ================================================== /** * Initializes a list of script engines - an engine per script file. * * @param req The solr request. * @param rsp The solr response * @return The list of initialized script engines. */ private List initEngines(SolrQueryRequest req, SolrQueryResponse rsp) throws SolrException { List scriptEngines = new ArrayList<>(); ScriptEngineManager scriptEngineManager = new ScriptEngineManager(resourceLoader.getClassLoader()); scriptEngineManager.put("logger", log); scriptEngineManager.put("req", req); scriptEngineManager.put("rsp", rsp); if (params != null) { scriptEngineManager.put("params", params); } for (ScriptFile scriptFile : scriptFiles) { ScriptEngine engine = null; if (null != engineName) { engine = scriptEngineManager.getEngineByName(engineName); if (engine == null) { String details = getSupportedEngines(scriptEngineManager, false); throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "No ScriptEngine found by name: " + engineName + (null != details ? " -- supported names: " + details : "")); } } else { engine = scriptEngineManager.getEngineByExtension (scriptFile.getExtension()); if (engine == null) { String details = getSupportedEngines(scriptEngineManager, true); throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "No ScriptEngine found by file extension: " + scriptFile.getFileName() + (null != details ? " -- supported extensions: " + details : "")); } } if (! (engine instanceof Invocable)) { String msg = "Engine " + ((null != engineName) ? engineName : ("for script " + scriptFile.getFileName())) + " does not support function invocation (via Invocable): " + engine.getClass().toString() + " (" + engine.getFactory().getEngineName() + ")"; log.error(msg); throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, msg); } if (scriptEngineCustomizer != null) { scriptEngineCustomizer.customize(engine); } scriptEngines.add(new EngineInfo((Invocable)engine, scriptFile)); try { Reader scriptSrc = scriptFile.openReader(resourceLoader); try { engine.eval(scriptSrc); } catch (ScriptException e) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unable to evaluate script: " + scriptFile.getFileName(), e); } finally { IOUtils.closeQuietly(scriptSrc); } } catch (IOException ioe) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unable to evaluate script: " + scriptFile.getFileName(), ioe); } } return scriptEngines; } /** * For error messages - returns null if there are any exceptions of any * kind building the string (or of the list is empty for some unknown reason). * @param ext - if true, list of extensions, otherwise a list of engine names */ private static String getSupportedEngines(ScriptEngineManager mgr, boolean ext) { String result = null; try { List factories = mgr.getEngineFactories(); if (null == factories) return result; Set engines = new LinkedHashSet<>(factories.size()); for (ScriptEngineFactory f : factories) { if (ext) { engines.addAll(f.getExtensions()); } else { engines.addAll(f.getNames()); } } result = String.join(", ", engines); } catch (RuntimeException e) { /* :NOOP: */ } return result; } //================================================= Inner Classes ================================================== /** * The actual update processor. All methods delegate to scripts. */ private static class ScriptUpdateProcessor extends UpdateRequestProcessor { private List engines; private ScriptUpdateProcessor(SolrQueryRequest req, SolrQueryResponse res, List engines, UpdateRequestProcessor next) { super(next); this.engines = engines; } @Override public void processAdd(AddUpdateCommand cmd) throws IOException { if (invokeFunction("processAdd", cmd)) { super.processAdd(cmd); } } @Override public void processDelete(DeleteUpdateCommand cmd) throws IOException { if (invokeFunction("processDelete", cmd)) { super.processDelete(cmd); } } @Override public void processMergeIndexes(MergeIndexesCommand cmd) throws IOException { if (invokeFunction("processMergeIndexes", cmd)) { super.processMergeIndexes(cmd); } } @Override public void processCommit(CommitUpdateCommand cmd) throws IOException { if (invokeFunction("processCommit", cmd)) { super.processCommit(cmd); } } @Override public void processRollback(RollbackUpdateCommand cmd) throws IOException { if (invokeFunction("processRollback", cmd)) { super.processRollback(cmd); } } @Override public void finish() throws IOException { if (invokeFunction("finish")) { super.finish(); } } /** * returns true if processing should continue, or false if the * request should be ended now. Result value is computed from the return * value of the script function if: it exists, is non-null, and can be * cast to a java Boolean. */ private boolean invokeFunction(String name, Object... cmd) { for (EngineInfo engine : engines) { try { Object result = engine.getEngine().invokeFunction(name, cmd); if (null != result && result instanceof Boolean) { if (! ((Boolean)result).booleanValue() ) { return false; } } } catch (ScriptException | NoSuchMethodException e) { throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Unable to invoke function " + name + " in script: " + engine.getScriptFile().getFileName() + ": " + e.getMessage(), e); } } return true; } } /** * Holds the script engine and its associated script file. */ private static class EngineInfo { private final Invocable engine; private final ScriptFile scriptFile; private EngineInfo(Invocable engine, ScriptFile scriptFile) { this.engine = engine; this.scriptFile = scriptFile; } public Invocable getEngine() { return engine; } public ScriptFile getScriptFile() { return scriptFile; } } /** * Represents a script file. */ private static class ScriptFile { private final String fileName; private final String extension; private ScriptFile(String fileName) { this.fileName = fileName; this.extension = FilenameUtils.getExtension(fileName); } public String getFileName() { return fileName; } public String getExtension() { return extension; } public Reader openReader(SolrResourceLoader resourceLoader) throws IOException { InputStream input = resourceLoader.openResource(fileName); return org.apache.lucene.util.IOUtils.getDecodingReader (input, StandardCharsets.UTF_8); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy