diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index a9d9e9bf7f4..477cbfbebc3 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -60,6 +60,10 @@ New Features * SOLR-3460: Add cloud-scripts directory and a zkcli.sh|bat tool for easy scripting and interaction with ZooKeeper. (Mark Miller) +* SOLR-1725: StatelessScriptUpdateProcessorFactory allows users to implement + the full ScriptUpdateProcessor API using any scripting language with a + javax.script.ScriptEngineFactory + (Uri Boness, ehatcher, Simon Rosenthal, hossman) Bug Fixes diff --git a/solr/core/src/java/org/apache/solr/core/SolrCore.java b/solr/core/src/java/org/apache/solr/core/SolrCore.java index e726d14db71..e1ff65b26a7 100644 --- a/solr/core/src/java/org/apache/solr/core/SolrCore.java +++ b/solr/core/src/java/org/apache/solr/core/SolrCore.java @@ -664,7 +664,8 @@ public final class SolrCore implements SolrInfoMBean { latch.countDown();//release the latch, otherwise we block trying to do the close. This should be fine, since counting down on a latch of 0 is still fine //close down the searcher and any other resources, if it exists, as this is not recoverable close(); - throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, null, e); + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + e.getMessage(), e); } finally { // allow firstSearcher events to fire and make sure it is released latch.countDown(); diff --git a/solr/core/src/java/org/apache/solr/update/processor/ScriptEngineCustomizer.java b/solr/core/src/java/org/apache/solr/update/processor/ScriptEngineCustomizer.java new file mode 100644 index 00000000000..9f59954c8f8 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/update/processor/ScriptEngineCustomizer.java @@ -0,0 +1,29 @@ +/** + * 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 javax.script.ScriptEngine; + +/** + * Enables customization of a script engine. Will mostly be used to register engine scoped variables. + */ +public interface ScriptEngineCustomizer { + + void customize(ScriptEngine engine); + +} diff --git a/solr/core/src/java/org/apache/solr/update/processor/StatelessScriptUpdateProcessorFactory.java b/solr/core/src/java/org/apache/solr/update/processor/StatelessScriptUpdateProcessorFactory.java new file mode 100644 index 00000000000..80061c67276 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/update/processor/StatelessScriptUpdateProcessorFactory.java @@ -0,0 +1,494 @@ +package org.apache.solr.update.processor; + +/* + * 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. + */ + +import org.apache.solr.common.SolrException; +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.lang.StringUtils; +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.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: + *

+ *

+ *

+ * 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>
+ * 
+ * + */ +public class StatelessScriptUpdateProcessorFactory extends UpdateRequestProcessorFactory implements SolrCoreAware { + + public static Logger log = LoggerFactory.getLogger(StatelessScriptUpdateProcessorFactory.class); + + 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 = + FieldMutatingUpdateProcessorFactory.oneOrMany(args, 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) { + 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)); + 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); + } + } + 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 = StringUtils.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 e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Unable to invoke function " + name + + " in script: " + + engine.getScriptFile().getFileName() + + ": " + e.getMessage(), e); + } catch (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) { + InputStream input = resourceLoader.openResource(fileName); + return org.apache.lucene.util.IOUtils.getDecodingReader + (input, org.apache.lucene.util.IOUtils.CHARSET_UTF_8); + } + } +} diff --git a/solr/core/src/test-files/solr/collection1/conf/addfields.updateprocessor.js b/solr/core/src/test-files/solr/collection1/conf/addfields.updateprocessor.js new file mode 100644 index 00000000000..1b3c9fc2d6e --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/addfields.updateprocessor.js @@ -0,0 +1,26 @@ +function processAdd(cmd) { + // Integer.valueOf is needed here to get a tru java object, because + // all javascript numbers are floating point (ie: java.lang.Double) + cmd.getSolrInputDocument().addField("script_added_i", + java.lang.Integer.valueOf(42)); + cmd.getSolrInputDocument().addField("script_added_d", 42.3); + +} + +// // // + +function processDelete() { + // NOOP +} +function processCommit() { + // NOOP +} +function processRollback() { + // NOOP +} +function processMergeIndexes() { + // NOOP +} +function finish() { + // NOOP +} diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-solrconfig-bogus-scriptengine-name.xml b/solr/core/src/test-files/solr/collection1/conf/bad-solrconfig-bogus-scriptengine-name.xml new file mode 100644 index 00000000000..fc9e108bee3 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-solrconfig-bogus-scriptengine-name.xml @@ -0,0 +1,32 @@ + + + + + + + ${tests.luceneMatchVersion:LUCENE_CURRENT} + + + + giberish + missleading.extension.updateprocessor.js.txt + + + + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-solrconfig-invalid-scriptfile.xml b/solr/core/src/test-files/solr/collection1/conf/bad-solrconfig-invalid-scriptfile.xml new file mode 100644 index 00000000000..dbadbb5c2c0 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-solrconfig-invalid-scriptfile.xml @@ -0,0 +1,33 @@ + + + + + + + ${tests.luceneMatchVersion:LUCENE_CURRENT} + + + + javascript + + currency.xml + + + + + diff --git a/solr/core/src/test-files/solr/collection1/conf/bad-solrconfig-missing-scriptfile.xml b/solr/core/src/test-files/solr/collection1/conf/bad-solrconfig-missing-scriptfile.xml new file mode 100644 index 00000000000..4dee70ce08f --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/bad-solrconfig-missing-scriptfile.xml @@ -0,0 +1,31 @@ + + + + + + + ${tests.luceneMatchVersion:LUCENE_CURRENT} + + + + a-file-name-that-does-not-exist.js + + + + + diff --git a/solr/core/src/test-files/solr/collection1/conf/conditional.updateprocessor.js b/solr/core/src/test-files/solr/collection1/conf/conditional.updateprocessor.js new file mode 100644 index 00000000000..5ec9487c150 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/conditional.updateprocessor.js @@ -0,0 +1,25 @@ +function processAdd(cmd) { + if (req.getParams().getBool("go-for-it",false)) { + cmd.getSolrInputDocument().addField("script_added_s", "i went for it"); + return true; + } + return false; +} + +// // // + +function processDelete() { + // NOOP +} +function processCommit() { + // NOOP +} +function processRollback() { + // NOOP +} +function processMergeIndexes() { + // NOOP +} +function finish() { + // NOOP +} diff --git a/solr/core/src/test-files/solr/collection1/conf/missing.functions.updateprocessor.js b/solr/core/src/test-files/solr/collection1/conf/missing.functions.updateprocessor.js new file mode 100644 index 00000000000..6e8728a0d77 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/missing.functions.updateprocessor.js @@ -0,0 +1,3 @@ +function doSomeStuff() { + return "This script doesn't contain any update processor functions"; +} diff --git a/solr/core/src/test-files/solr/collection1/conf/missleading.extension.updateprocessor.js.txt b/solr/core/src/test-files/solr/collection1/conf/missleading.extension.updateprocessor.js.txt new file mode 100644 index 00000000000..984e1d82f10 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/missleading.extension.updateprocessor.js.txt @@ -0,0 +1,23 @@ +function processAdd(cmd) { + // Integer.valueOf is needed here to get a tru java object, because + // all javascript numbers are floating point (ie: java.lang.Double) + cmd.getSolrInputDocument().addField("script_added_i", + java.lang.Integer.valueOf(42)); + cmd.getSolrInputDocument().addField("script_added_d", 42.3); + +} +function processDelete() { + // NOOP +} +function processCommit() { + // NOOP +} +function processRollback() { + // NOOP +} +function processMergeIndexes() { + // NOOP +} +function finish() { + // NOOP +} diff --git a/solr/core/src/test-files/solr/collection1/conf/solrconfig-script-updateprocessor.xml b/solr/core/src/test-files/solr/collection1/conf/solrconfig-script-updateprocessor.xml new file mode 100644 index 00000000000..bf0d93c0c4e --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/solrconfig-script-updateprocessor.xml @@ -0,0 +1,112 @@ + + + + + + + ${tests.luceneMatchVersion:LUCENE_CURRENT} + + + + + + javascript + missleading.extension.updateprocessor.js.txt + + + + + + + + + + + + + trivial.updateprocessor0.js + + true + 1 + + + + + + + + + trivial.updateprocessor0.js + trivial.updateprocessor1.js + + + true + 1 + + + + + + + + trivial.updateprocessor0.js + trivial.updateprocessor1.js + + true + 1 + + + + + + + + + conditional.updateprocessor.js + addfields.updateprocessor.js + + + + + + + conditional.updateprocessor.js + + + addfields.updateprocessor.js + + + + + + throw.error.on.add.updateprocessor.js + + + + + missing.functions.updateprocessor.js + + + + diff --git a/solr/core/src/test-files/solr/collection1/conf/throw.error.on.add.updateprocessor.js b/solr/core/src/test-files/solr/collection1/conf/throw.error.on.add.updateprocessor.js new file mode 100644 index 00000000000..ca56fe35cfe --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/throw.error.on.add.updateprocessor.js @@ -0,0 +1,21 @@ +function processAdd() { + throw "guess what? no-soup-fo-you !!!"; +} + +// // // + +function processDelete() { + // NOOP +} +function processCommit() { + // NOOP +} +function processRollback() { + // NOOP +} +function processMergeIndexes() { + // NOOP +} +function finish() { + // NOOP +} diff --git a/solr/core/src/test-files/solr/collection1/conf/trivial.updateprocessor0.js b/solr/core/src/test-files/solr/collection1/conf/trivial.updateprocessor0.js new file mode 100644 index 00000000000..72bb102e0c0 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/trivial.updateprocessor0.js @@ -0,0 +1,57 @@ +function processAdd(cmd) { + functionMessages.add("processAdd0"); + testCase.assertNotNull(req); + testCase.assertNotNull(rsp); + testCase.assertNotNull(logger); + testCase.assertNotNull(cmd); + testCase.assertNotNull(params); + testCase.assertTrue(1 == params.get('intValue').intValue()); // had issues with assertTrue(1, params.get('intValue').intValue()) casting to wrong variant + testCase.assertTrue(params.get('boolValue').booleanValue()); + + // Integer.valueOf is needed here to get a tru java object, because + // all javascript numbers are floating point (ie: java.lang.Double) + cmd.getSolrInputDocument().addField("script_added_i", + java.lang.Integer.valueOf(42)); + cmd.getSolrInputDocument().addField("script_added_d", 42.3); + +} + +function processDelete(cmd) { + functionMessages.add("processDelete0"); + testCase.assertNotNull(req); + testCase.assertNotNull(rsp); + testCase.assertNotNull(logger); + testCase.assertNotNull(cmd); +} + +function processMergeIndexes(cmd) { + functionMessages.add("processMergeIndexes0"); + testCase.assertNotNull(req); + testCase.assertNotNull(rsp); + testCase.assertNotNull(logger); + testCase.assertNotNull(cmd); +} + +function processCommit(cmd) { + functionMessages.add("processCommit0"); + testCase.assertNotNull(req); + testCase.assertNotNull(rsp); + testCase.assertNotNull(logger); + testCase.assertNotNull(cmd); +} + +function processRollback(cmd) { + functionMessages.add("processRollback0"); + testCase.assertNotNull(req); + testCase.assertNotNull(rsp); + testCase.assertNotNull(logger); + testCase.assertNotNull(cmd); +} + +function finish() { + functionMessages.add("finish0"); + testCase.assertNotNull(req); + testCase.assertNotNull(rsp); + testCase.assertNotNull(logger); +} + diff --git a/solr/core/src/test-files/solr/collection1/conf/trivial.updateprocessor1.js b/solr/core/src/test-files/solr/collection1/conf/trivial.updateprocessor1.js new file mode 100644 index 00000000000..98bdf2ab060 --- /dev/null +++ b/solr/core/src/test-files/solr/collection1/conf/trivial.updateprocessor1.js @@ -0,0 +1,25 @@ +function processAdd(cmd) { + functionMessages.add("processAdd1"); + +} + +function processDelete(cmd) { + functionMessages.add("processDelete1"); +} + +function processMergeIndexes(cmd) { + functionMessages.add("processMergeIndexes1"); +} + +function processCommit(cmd) { + functionMessages.add("processCommit1"); +} + +function processRollback(cmd) { + functionMessages.add("processRollback1"); +} + +function finish() { + functionMessages.add("finish1"); +} + diff --git a/solr/core/src/test/org/apache/solr/core/AbstractBadConfigTestBase.java b/solr/core/src/test/org/apache/solr/core/AbstractBadConfigTestBase.java new file mode 100644 index 00000000000..09ef9352508 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/core/AbstractBadConfigTestBase.java @@ -0,0 +1,66 @@ +/* + * 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.core; + +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.SolrException.ErrorCode; + +import java.util.regex.Pattern; + +import javax.script.ScriptEngineManager; + +import org.junit.Assume; + +public abstract class AbstractBadConfigTestBase extends SolrTestCaseJ4 { + + /** + * Given a solrconfig.xml file name, a schema file name, and an + * expected errString, asserts that initializing a core with these + * files causes an error matching the specified errString ot be thrown. + */ + protected final void assertConfigs(final String solrconfigFile, + final String schemaFile, + final String errString) + throws Exception { + + ignoreException(Pattern.quote(errString)); + try { + initCore( solrconfigFile, schemaFile ); + } catch (Exception e) { + // short circuit out if we found what we expected + if (-1 != e.getMessage().indexOf(errString)) return; + // Test the cause too in case the expected error is wrapped by the TestHarness + // (NOTE: we don't go all the way down. Either errString should be changed, + // or some error wrapping should use a better message or both) + if (null != e.getCause() && + null != e.getCause().getMessage() && + -1 != e.getCause().getMessage().indexOf(errString)) return; + + // otherwise, rethrow it, possibly completley unrelated + throw new SolrException + (ErrorCode.SERVER_ERROR, + "Unexpected error, expected error matching: " + errString, e); + } finally { + deleteCore(); + resetExceptionIgnores(); + } + fail("Did not encounter any exception from: " + solrconfigFile + " using " + schemaFile); + } + +} diff --git a/solr/core/src/test/org/apache/solr/core/TestBadConfig.java b/solr/core/src/test/org/apache/solr/core/TestBadConfig.java index ba812422362..9daf0580826 100644 --- a/solr/core/src/test/org/apache/solr/core/TestBadConfig.java +++ b/solr/core/src/test/org/apache/solr/core/TestBadConfig.java @@ -17,31 +17,36 @@ package org.apache.solr.core; -import org.apache.solr.util.AbstractSolrTestCase; +import javax.script.ScriptEngineManager; -public class TestBadConfig extends AbstractSolrTestCase { +import org.junit.Assume; - @Override - public String getSchemaFile() { return "schema.xml"; } - @Override - public String getSolrConfigFile() { return "bad_solrconfig.xml"; } +public class TestBadConfig extends AbstractBadConfigTestBase { - @Override - public void setUp() throws Exception { - ignoreException("unset.sys.property"); - try { - super.setUp(); - fail("Exception should have been thrown"); - } catch (Exception e) { - assertTrue(e.getMessage().contains("unset.sys.property")); - } finally { - resetExceptionIgnores(); - } + public void testUnsetSysProperty() throws Exception { + assertConfigs("bad_solrconfig.xml","schema.xml","unset.sys.property"); } - - public void testNothing() { - // Empty test case as the real test is that the initialization of the TestHarness fails - assertTrue(true); + public void testBogusScriptEngine() throws Exception { + // sanity check + Assume.assumeTrue(null == (new ScriptEngineManager()).getEngineByName("giberish")); + + assertConfigs("bad-solrconfig-bogus-scriptengine-name.xml", + "schema.xml","giberish"); } -} \ No newline at end of file + + public void testMissingScriptFile() throws Exception { + // sanity check + Assume.assumeNotNull((new ScriptEngineManager()).getEngineByExtension("js")); + assertConfigs("bad-solrconfig-missing-scriptfile.xml", + "schema.xml","a-file-name-that-does-not-exist.js"); + } + + public void testInvalidScriptFile() throws Exception { + // sanity check + Assume.assumeNotNull((new ScriptEngineManager()).getEngineByName("javascript")); + assertConfigs("bad-solrconfig-invalid-scriptfile.xml", + "schema.xml","currency.xml"); + } + +} diff --git a/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java b/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java index 8bf96e4a25c..46d44bc9b7c 100644 --- a/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java +++ b/solr/core/src/test/org/apache/solr/schema/BadIndexSchemaTest.java @@ -17,38 +17,15 @@ package org.apache.solr.schema; -import org.apache.solr.SolrTestCaseJ4; -import org.apache.solr.common.SolrException; -import org.apache.solr.common.SolrException.ErrorCode; -import org.apache.solr.core.SolrConfig; +import org.apache.solr.core.AbstractBadConfigTestBase; import java.util.regex.Pattern; -import org.junit.Test; - -public class BadIndexSchemaTest extends SolrTestCaseJ4 { +public class BadIndexSchemaTest extends AbstractBadConfigTestBase { private void doTest(final String schema, final String errString) throws Exception { - - ignoreException(Pattern.quote(errString)); - try { - initCore( "solrconfig.xml", schema ); - } catch (Exception e) { - // short circuit out if we found what we expected - if (-1 != e.getMessage().indexOf(errString)) return; - // Test the cause too in case the expected error is wrapped - if (null != e.getCause() && - -1 != e.getCause().getMessage().indexOf(errString)) return; - - // otherwise, rethrow it, possibly completley unrelated - throw new SolrException - (ErrorCode.SERVER_ERROR, - "Unexpected error, expected error matching: " + errString, e); - } finally { - deleteCore(); - } - fail("Did not encounter any exception from: " + schema); + assertConfigs("solrconfig.xml", schema, errString); } public void testSevereErrorsForInvalidFieldOptions() throws Exception { diff --git a/solr/core/src/test/org/apache/solr/update/processor/FieldMutatingUpdateProcessorTest.java b/solr/core/src/test/org/apache/solr/update/processor/FieldMutatingUpdateProcessorTest.java index de9e21f1681..c7029f9094b 100644 --- a/solr/core/src/test/org/apache/solr/update/processor/FieldMutatingUpdateProcessorTest.java +++ b/solr/core/src/test/org/apache/solr/update/processor/FieldMutatingUpdateProcessorTest.java @@ -53,7 +53,7 @@ import org.junit.Test; * (mainly via TrimFieldUpdateProcessor) and the logic of other various * subclasses. */ -public class FieldMutatingUpdateProcessorTest extends SolrTestCaseJ4 { +public class FieldMutatingUpdateProcessorTest extends UpdateProcessorTestBase { @BeforeClass public static void beforeClass() throws Exception { @@ -816,64 +816,4 @@ public class FieldMutatingUpdateProcessorTest extends SolrTestCaseJ4 { 3.0F, d.getField("foo_s").getBoost(), 0.0F); } - /** - * Convenience method for building up SolrInputDocuments - */ - SolrInputDocument doc(SolrInputField... fields) { - SolrInputDocument d = new SolrInputDocument(); - for (SolrInputField f : fields) { - d.put(f.getName(), f); - } - return d; - } - - /** - * Convenience method for building up SolrInputFields - */ - SolrInputField field(String name, float boost, Object... values) { - SolrInputField f = new SolrInputField(name); - for (Object v : values) { - f.addValue(v, 1.0F); - } - f.setBoost(boost); - return f; - } - - /** - * Convenience method for building up SolrInputFields with default boost - */ - SolrInputField f(String name, Object... values) { - return field(name, 1.0F, values); - } - - - /** - * Runs a document through the specified chain, and returns the final - * document used when the chain is completed (NOTE: some chains may - * modify the document in place - */ - SolrInputDocument processAdd(final String chain, - final SolrInputDocument docIn) - throws IOException { - - SolrCore core = h.getCore(); - UpdateRequestProcessorChain pc = core.getUpdateProcessingChain(chain); - assertNotNull("No Chain named: " + chain, pc); - - SolrQueryResponse rsp = new SolrQueryResponse(); - - SolrQueryRequest req = new LocalSolrQueryRequest - (core, new ModifiableSolrParams()); - try { - AddUpdateCommand cmd = new AddUpdateCommand(req); - cmd.solrDoc = docIn; - - UpdateRequestProcessor processor = pc.createProcessor(req, rsp); - processor.processAdd(cmd); - - return cmd.solrDoc; - } finally { - req.close(); - } - } } diff --git a/solr/core/src/test/org/apache/solr/update/processor/ScriptEngineTest.java b/solr/core/src/test/org/apache/solr/update/processor/ScriptEngineTest.java new file mode 100644 index 00000000000..4e8891a1b98 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/update/processor/ScriptEngineTest.java @@ -0,0 +1,95 @@ +/* + * 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.lucene.util.LuceneTestCase; + +import javax.script.Invocable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import java.io.StringReader; + +/** + * Sanity tests basic functionality of {@link ScriptEngineManager} and + * {@link ScriptEngine} w/o excercising any Lucene specific code. + */ +public class ScriptEngineTest extends LuceneTestCase { + + private ScriptEngineManager manager; + + @Override + public void setUp() throws Exception { + super.setUp(); + manager = new ScriptEngineManager(); + } + + public void testGetEngineByName() { + ScriptEngine engine = manager.getEngineByName("JavaScript"); + assertNotNull(engine); + engine = manager.getEngineByName("DummyScript"); + assertNull(engine); + } + + public void testGetEngineByExtension() { + ScriptEngine engine = manager.getEngineByExtension("js"); + assertNotNull(engine); + engine = manager.getEngineByExtension("foobar"); + assertNull(engine); + } + + public void testEvalText() throws ScriptException, NoSuchMethodException { + ScriptEngine engine = manager.getEngineByName("JavaScript"); + assertNotNull(engine); + engine.eval("function add(a,b) { return a + b }"); + Double result = (Double) ((Invocable)engine).invokeFunction("add", 1, 2); + assertNotNull(result); + assertEquals(3, result.intValue()); + } + + public void testEvalReader() throws ScriptException, NoSuchMethodException { + ScriptEngine engine = manager.getEngineByName("JavaScript"); + assertNotNull(engine); + StringReader reader = new StringReader("function add(a,b) { return a + b }"); + engine.eval(reader); + Double result = (Double) ((Invocable)engine).invokeFunction("add", 1, 2); + assertNotNull(result); + assertEquals(3, result.intValue()); + } + + public void testPut() throws ScriptException, NoSuchMethodException { + manager.put("a", 1); + ScriptEngine engine = manager.getEngineByName("JavaScript"); + engine.put("b", 2); + assertNotNull(engine); + engine.eval("function add() { return a + b }"); + Double result = (Double) ((Invocable)engine).invokeFunction("add", 1, 2); + assertNotNull(result); + assertEquals(3, result.intValue()); + } + +// public void testJRuby() throws ScriptException, NoSuchMethodException { // Simply adding jruby.jar to Solr's lib/ directory gets this test passing +// ScriptEngine engine = manager.getEngineByName("jruby"); +// assertNotNull(engine); +// engine.eval("def add(a,b); a + b; end"); +// Long result = (Long) ((Invocable)engine).invokeFunction("add", 1, 2); +// assertNotNull(result); +// assertEquals(3, result.intValue()); +// } + +} diff --git a/solr/core/src/test/org/apache/solr/update/processor/StatelessScriptUpdateProcessorFactoryTest.java b/solr/core/src/test/org/apache/solr/update/processor/StatelessScriptUpdateProcessorFactoryTest.java new file mode 100644 index 00000000000..1415378f993 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/update/processor/StatelessScriptUpdateProcessorFactoryTest.java @@ -0,0 +1,272 @@ +package org.apache.solr.update.processor; + +/* + * 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. + */ + +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.params.UpdateParams; +import org.apache.solr.common.SolrException; + +import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.core.SolrCore; +import org.apache.solr.search.SolrIndexSearcher; +import org.apache.solr.util.RefCounted; + +import org.junit.Assume; +import org.junit.BeforeClass; + +import javax.script.ScriptEngineManager; +import javax.script.ScriptEngine; + +import java.util.ArrayList; +import java.util.List; + +/** + * Tests {@link StatelessScriptUpdateProcessorFactory}. + * + * TODO: This test, to run from an IDE, requires a working directory of /solr/core/src/test-files. Fix! + */ +public class StatelessScriptUpdateProcessorFactoryTest extends UpdateProcessorTestBase { + + @BeforeClass + public static void beforeClass() throws Exception { + initCore("solrconfig-script-updateprocessor.xml", "schema12.xml"); + + Assume.assumeNotNull((new ScriptEngineManager()).getEngineByExtension("js")); + } + + /** + * simple test of a basic script processor chain using the full + * RequestHandler + UpdateProcessorChain flow + */ + public void testFullRequestHandlerFlow() throws Exception { + + assertU("Simple assertion that adding a document works", + adoc("id", "4055", + "subject", "Hoss")); + assertU(commit()); + + assertQ("couldn't find hoss using script added field", + req("q","script_added_i:[40 TO 45]", + "fq","id:4055") + ,"//result[@numFound=1]" + ,"//str[@name='id'][.='4055']" + ); + + // clean up + processDeleteById("run-no-scripts","4055"); + processCommit("run-no-scripts"); + + } + + public void testSingleScript() throws Exception { + SolrCore core = h.getCore(); + UpdateRequestProcessorChain chained = core.getUpdateProcessingChain("single-script"); + final StatelessScriptUpdateProcessorFactory factory = ((StatelessScriptUpdateProcessorFactory) chained.getFactories()[0]); + final List functionMessages = new ArrayList(); + factory.setScriptEngineCustomizer(new ScriptEngineCustomizer() { + public void customize(ScriptEngine engine) { + engine.put("testCase", StatelessScriptUpdateProcessorFactoryTest.this); + engine.put("functionMessages", functionMessages); + } + }); + assertNotNull(chained); + + SolrInputDocument d = processAdd("single-script", + doc(f("id", "1"), + f("name", " foo "), + f("subject", "bar"))); + + processCommit("run-no-scripts"); + + assertQ("couldn't find doc by id", + req("q","id:1") + , "//result[@numFound=1]"); + + processDeleteById("single-script","1"); + processCommit("single-script"); + + assertQ("found deleted doc", + req("q","id:1") + , "//result[@numFound=0]"); + + + assertEquals(3, functionMessages.size()); + + assertTrue(functionMessages.contains("processAdd0")); + assertTrue(functionMessages.contains("processDelete0")); + assertTrue(functionMessages.contains("processCommit0")); + + } + + public void testMultipleScripts() throws Exception { + SolrCore core = h.getCore(); + + for (final String chain : new String[] {"dual-scripts-arr", + "dual-scripts-strs"}) { + + UpdateRequestProcessorChain chained = core.getUpdateProcessingChain(chain); + final StatelessScriptUpdateProcessorFactory factory = + ((StatelessScriptUpdateProcessorFactory) chained.getFactories()[0]); + final List functionMessages = new ArrayList(); + ScriptEngineCustomizer customizer = new ScriptEngineCustomizer() { + public void customize(ScriptEngine engine) { + engine.put("testCase", StatelessScriptUpdateProcessorFactoryTest.this); + engine.put("functionMessages", functionMessages); + } + }; + factory.setScriptEngineCustomizer(customizer); + assertNotNull(chained); + + SolrInputDocument d = processAdd(chain, + doc(f("id", "2"), + f("name", " foo "), + f("subject", "bar"))); + + assertEquals(chain + " didn't add Double field", + 42.3d, d.getFieldValue("script_added_d")); + assertEquals(chain + " didn't add integer field", + new Integer(42), d.getFieldValue("script_added_i")); + + processCommit("run-no-scripts"); + + assertQ(chain + ": couldn't find doc by id", + req("q","id:2") + , "//result[@numFound=1]"); + + processDeleteById(chain, "2"); + processCommit(chain); + + assertEquals(chain, 6, functionMessages.size()); + assertTrue(chain, functionMessages.contains("processAdd0")); + assertTrue(chain, functionMessages.contains("processAdd1")); + assertTrue(chain + ": script order doesn't match conf order", + functionMessages.indexOf("processAdd0") + < functionMessages.indexOf("processAdd1")); + + assertTrue(chain, functionMessages.contains("processDelete0")); + assertTrue(chain, functionMessages.contains("processDelete1")); + assertTrue(chain + ": script order doesn't match conf order", + functionMessages.indexOf("processDelete0") + < functionMessages.indexOf("processDelete1")); + + assertTrue(chain, functionMessages.contains("processCommit0")); + assertTrue(chain, functionMessages.contains("processCommit1")); + assertTrue(chain + ": script order doesn't match conf order", + functionMessages.indexOf("processCommit0") + < functionMessages.indexOf("processCommit1")); + + finish(chain); + + assertEquals(chain, 8, functionMessages.size()); + + assertTrue(chain, functionMessages.contains("finish0")); + assertTrue(chain, functionMessages.contains("finish1")); + assertTrue(chain + ": script order doesn't match conf order", + functionMessages.indexOf("finish0") + < functionMessages.indexOf("finish1")); + + assertQ(chain + ": found deleted doc", + req("q","id:2") + , "//result[@numFound=0]"); + + } + } + + + public void testConditionalExecution() throws Exception { + for (String chain : new String[] {"conditional-script", + "conditional-scripts"}) { + + ModifiableSolrParams reqParams = new ModifiableSolrParams(); + + SolrInputDocument d = processAdd(chain, + reqParams, + doc(f("id", "3"), + f("name", " foo "), + f("subject", "bar"))); + + assertFalse(chain + " added String field despite condition", + d.containsKey("script_added_s")); + assertFalse(chain + " added Double field despite condition", + d.containsKey("script_added_d")); + + reqParams.add("go-for-it", "true"); + + d = processAdd(chain, + reqParams, + doc(f("id", "4"), + f("name", " foo "), + f("subject", "bar"))); + + assertEquals(chain + " didn't add String field", + "i went for it", d.getFieldValue("script_added_s")); + assertEquals(chain +" didn't add Double field", + 42.3d, d.getFieldValue("script_added_d")); + assertEquals(chain + " didn't add integer field", + new Integer(42), d.getFieldValue("script_added_i")); + } + } + + public void testForceEngine() throws Exception { + Assume.assumeNotNull((new ScriptEngineManager()).getEngineByName("javascript")); + + final String chain = "force-script-engine"; + SolrInputDocument d = processAdd(chain, + doc(f("id", "5"), + f("name", " foo "), + f("subject", "bar"))); + + assertEquals(chain +" didn't add Double field", + 42.3d, d.getFieldValue("script_added_d")); + assertEquals(chain + " didn't add integer field", + new Integer(42), d.getFieldValue("script_added_i")); + } + + public void testPropogatedException() throws Exception { + final String chain = "error-on-add"; + try { + SolrInputDocument d = processAdd(chain, + doc(f("id", "5"), + f("name", " foo "), + f("subject", "bar"))); + } catch (SolrException e) { + assertTrue("Exception doesn't contain script error string: " + e.getMessage(), + 0 < e.getMessage().indexOf("no-soup-fo-you")); + return; + } + fail("Did not get exception from script"); + + } + + public void testMissingFunctions() throws Exception { + final String chain = "missing-functions"; + try { + SolrInputDocument d = processAdd(chain, + doc(f("id", "5"), + f("name", " foo "), + f("subject", "bar"))); + } catch (SolrException e) { + assertTrue("Exception doesn't contain expected error: " + e.getMessage(), + 0 < e.getMessage().indexOf("processAdd")); + return; + } + fail("Did not get exception from script"); + } + +} diff --git a/solr/core/src/test/org/apache/solr/update/processor/UpdateProcessorTestBase.java b/solr/core/src/test/org/apache/solr/update/processor/UpdateProcessorTestBase.java new file mode 100644 index 00000000000..5ddff4555d0 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/update/processor/UpdateProcessorTestBase.java @@ -0,0 +1,163 @@ +package org.apache.solr.update.processor; + +/* + * 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. + */ + +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.SolrInputDocument; +import org.apache.solr.common.SolrInputField; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.core.SolrCore; +import org.apache.solr.request.LocalSolrQueryRequest; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.update.AddUpdateCommand; +import org.apache.solr.update.CommitUpdateCommand; +import org.apache.solr.update.DeleteUpdateCommand; + +import java.io.IOException; + +public class UpdateProcessorTestBase extends SolrTestCaseJ4 { + + /** + * Runs a document through the specified chain, and returns the final + * document used when the chain is completed (NOTE: some chains may + * modify the document in place + */ + protected SolrInputDocument processAdd(final String chain, + final SolrInputDocument docIn) + throws IOException { + + return processAdd(chain, new ModifiableSolrParams(), docIn); + } + + /** + * Runs a document through the specified chain, and returns the final + * document used when the chain is completed (NOTE: some chains may + * modify the document in place + */ + protected SolrInputDocument processAdd(final String chain, + final SolrParams requestParams, + final SolrInputDocument docIn) + throws IOException { + + SolrCore core = h.getCore(); + UpdateRequestProcessorChain pc = core.getUpdateProcessingChain(chain); + assertNotNull("No Chain named: " + chain, pc); + + SolrQueryResponse rsp = new SolrQueryResponse(); + + SolrQueryRequest req = new LocalSolrQueryRequest(core, requestParams); + try { + AddUpdateCommand cmd = new AddUpdateCommand(req); + cmd.solrDoc = docIn; + + UpdateRequestProcessor processor = pc.createProcessor(req, rsp); + processor.processAdd(cmd); + + return cmd.solrDoc; + } finally { + req.close(); + } + } + + protected void processCommit(final String chain) throws IOException { + SolrCore core = h.getCore(); + UpdateRequestProcessorChain pc = core.getUpdateProcessingChain(chain); + assertNotNull("No Chain named: " + chain, pc); + + SolrQueryResponse rsp = new SolrQueryResponse(); + + SolrQueryRequest req = new LocalSolrQueryRequest(core, new ModifiableSolrParams()); + + CommitUpdateCommand cmd = new CommitUpdateCommand(req,false); + UpdateRequestProcessor processor = pc.createProcessor(req, rsp); + try { + processor.processCommit(cmd); + } finally { + req.close(); + } + } + + protected void processDeleteById(final String chain, String id) throws IOException { + SolrCore core = h.getCore(); + UpdateRequestProcessorChain pc = core.getUpdateProcessingChain(chain); + assertNotNull("No Chain named: " + chain, pc); + + SolrQueryResponse rsp = new SolrQueryResponse(); + + SolrQueryRequest req = new LocalSolrQueryRequest(core, new ModifiableSolrParams()); + + DeleteUpdateCommand cmd = new DeleteUpdateCommand(req); + cmd.setId(id); + UpdateRequestProcessor processor = pc.createProcessor(req, rsp); + try { + processor.processDelete(cmd); + } finally { + req.close(); + } + } + + protected void finish(final String chain) throws IOException { + SolrCore core = h.getCore(); + UpdateRequestProcessorChain pc = core.getUpdateProcessingChain(chain); + assertNotNull("No Chain named: " + chain, pc); + + SolrQueryResponse rsp = new SolrQueryResponse(); + SolrQueryRequest req = new LocalSolrQueryRequest(core, new ModifiableSolrParams()); + + UpdateRequestProcessor processor = pc.createProcessor(req, rsp); + try { + processor.finish(); + } finally { + req.close(); + } + } + + + /** + * Convenience method for building up SolrInputDocuments + */ + final SolrInputDocument doc(SolrInputField... fields) { + SolrInputDocument d = new SolrInputDocument(); + for (SolrInputField f : fields) { + d.put(f.getName(), f); + } + return d; + } + + /** + * Convenience method for building up SolrInputFields + */ + final SolrInputField field(String name, float boost, Object... values) { + SolrInputField f = new SolrInputField(name); + for (Object v : values) { + f.addValue(v, 1.0F); + } + f.setBoost(boost); + return f; + } + + /** + * Convenience method for building up SolrInputFields with default boost + */ + final SolrInputField f(String name, Object... values) { + return field(name, 1.0F, values); + } +}