Scripting: Make ScriptEngine.compile generic on the script context (#24873)

This commit changes the compile method of ScriptEngine to be generic in
the same way it is on ScriptService. This moves the shim of handling the
two existing context classes into each script engine, so that each
engine can be worked on independently to convert to real handling of
contexts.
This commit is contained in:
Ryan Ernst 2017-05-24 20:06:32 -07:00 committed by GitHub
parent 5581a0b2f0
commit 7d03cff820
17 changed files with 145 additions and 194 deletions

View File

@ -20,7 +20,6 @@ package org.elasticsearch.plugins;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.script.ScriptContext;

View File

@ -19,9 +19,6 @@
package org.elasticsearch.script;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.search.lookup.SearchLookup;
import java.io.Closeable;
import java.io.IOException;
import java.util.Map;
@ -40,14 +37,11 @@ public interface ScriptEngine extends Closeable {
* Compiles a script.
* @param name the name of the script. {@code null} if it is anonymous (inline). For a stored script, its the identifier.
* @param code actual source of the script
* @param context the context this script will be used for
* @param params compile-time parameters (such as flags to the compiler)
* @return an opaque compiled script which may be cached and later passed to
* @return A compiled script of the CompiledType from {@link ScriptContext}
*/
Object compile(String name, String code, Map<String, String> params);
ExecutableScript executable(Object compiledScript, @Nullable Map<String, Object> vars);
SearchScript search(Object compiledScript, SearchLookup lookup, @Nullable Map<String, Object> vars);
<CompiledType> CompiledType compile(String name, String code, ScriptContext<CompiledType> context, Map<String, String> params);
@Override
default void close() throws IOException {}

View File

@ -312,14 +312,7 @@ public class ScriptService extends AbstractComponent implements Closeable, Clust
}
// Check whether too many compilations have happened
checkCompilationLimit();
Object engineCompiled = scriptEngine.compile(id, idOrCode, options);
if (context.instanceClazz == ExecutableScript.class) {
compiledScript = (ExecutableScript.Compiled) params -> scriptEngine.executable(engineCompiled, params);
} else if (context.instanceClazz == SearchScript.class) {
compiledScript = (SearchScript.Compiled) (params, lookup) -> scriptEngine.search(engineCompiled, lookup, params);
} else {
throw new IllegalArgumentException("Script context [" + context.name + "] not supported");
}
compiledScript = scriptEngine.compile(id, idOrCode, context, options);
} catch (ScriptException good) {
// TODO: remove this try-catch completely, when all script engines have good exceptions!
throw good; // its already good
@ -439,7 +432,8 @@ public class ScriptService extends AbstractComponent implements Closeable, Clust
throw new IllegalArgumentException(
"cannot put [" + ScriptType.STORED + "] script, no script contexts are enabled");
} else {
Object compiled = scriptEngine.compile(request.id(), source.getCode(), Collections.emptyMap());
// TODO: executable context here is just a placeholder, replace with optional context name passed into PUT stored script req
Object compiled = scriptEngine.compile(request.id(), source.getCode(), ScriptContext.EXECUTABLE, Collections.emptyMap());
if (compiled == null) {
throw new IllegalArgumentException("failed to parse/compile stored script [" + request.id() + "]" +

View File

@ -34,6 +34,7 @@ import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ExplainableSearchScript;
import org.elasticsearch.script.LeafSearchScript;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.ScriptType;
import org.elasticsearch.script.SearchScript;
@ -77,19 +78,10 @@ public class ExplainableScriptIT extends ESIntegTestCase {
}
@Override
public Object compile(String scriptName, String scriptSource, Map<String, String> params) {
public <T> T compile(String scriptName, String scriptSource, ScriptContext<T> context, Map<String, String> params) {
assert scriptSource.equals("explainable_script");
return null;
}
@Override
public ExecutableScript executable(Object compiledScript, @Nullable Map<String, Object> vars) {
throw new UnsupportedOperationException();
}
@Override
public SearchScript search(Object compiledScript, SearchLookup lookup, @Nullable Map<String, Object> vars) {
return new SearchScript() {
assert context == ScriptContext.SEARCH;
SearchScript.Compiled compiled = (p, lookup) -> new SearchScript() {
@Override
public LeafSearchScript getLeafSearchScript(LeafReaderContext context) throws IOException {
return new MyScript(lookup.doc().getLeafDocLookup(context));
@ -99,6 +91,7 @@ public class ExplainableScriptIT extends ESIntegTestCase {
return false;
}
};
return context.compiledClazz.cast(compiled);
}
};
}

View File

@ -31,6 +31,7 @@ import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.ScriptPlugin;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.lookup.SearchLookup;
@ -1025,14 +1026,13 @@ public class SuggestSearchIT extends ESIntegTestCase {
}
@Override
public Object compile(String scriptName, String scriptSource, Map<String, String> params) {
return scriptSource;
public <T> T compile(String scriptName, String scriptSource, ScriptContext<T> context, Map<String, String> params) {
if (context.instanceClazz != ExecutableScript.class) {
throw new UnsupportedOperationException();
}
@Override
public ExecutableScript executable(Object compiledScript, Map<String, Object> params) {
String script = (String) compiledScript;
for (Entry<String, Object> entry : params.entrySet()) {
ExecutableScript.Compiled compiled = p -> {
String script = scriptSource;
for (Entry<String, Object> entry : p.entrySet()) {
script = script.replace("{{" + entry.getKey() + "}}", String.valueOf(entry.getValue()));
}
String result = script;
@ -1047,11 +1047,8 @@ public class SuggestSearchIT extends ESIntegTestCase {
return result;
}
};
}
@Override
public SearchScript search(Object compiledScript, SearchLookup lookup, Map<String, Object> vars) {
throw new UnsupportedOperationException("search script not supported");
};
return context.compiledClazz.cast(compiled);
}
}

View File

@ -23,6 +23,7 @@ import org.apache.lucene.expressions.Expression;
import org.apache.lucene.expressions.SimpleBindings;
import org.apache.lucene.expressions.js.JavascriptCompiler;
import org.apache.lucene.expressions.js.VariableContext;
import org.apache.lucene.index.LeafReaderContext;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.DoubleConstValueSource;
import org.apache.lucene.search.SortField;
@ -38,11 +39,14 @@ import org.elasticsearch.index.mapper.MappedFieldType;
import org.elasticsearch.index.mapper.MapperService;
import org.elasticsearch.script.ClassPermission;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.LeafSearchScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.ScriptException;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.lookup.SearchLookup;
import java.io.IOException;
import java.security.AccessControlContext;
import java.security.AccessController;
import java.security.PrivilegedAction;
@ -69,11 +73,11 @@ public class ExpressionScriptEngine extends AbstractComponent implements ScriptE
}
@Override
public Object compile(String scriptName, String scriptSource, Map<String, String> params) {
public <T> T compile(String scriptName, String scriptSource, ScriptContext<T> context, Map<String, String> params) {
// classloader created here
final SecurityManager sm = System.getSecurityManager();
SpecialPermission.check();
return AccessController.doPrivileged(new PrivilegedAction<Expression>() {
Expression expr = AccessController.doPrivileged(new PrivilegedAction<Expression>() {
@Override
public Expression run() {
try {
@ -100,11 +104,17 @@ public class ExpressionScriptEngine extends AbstractComponent implements ScriptE
}
}
});
if (context.instanceClazz.equals(SearchScript.class)) {
SearchScript.Compiled compiled = (p, lookup) -> newSearchScript(expr, lookup, p);
return context.compiledClazz.cast(compiled);
} else if (context.instanceClazz.equals(ExecutableScript.class)) {
ExecutableScript.Compiled compiled = (p) -> new ExpressionExecutableScript(expr, p);
return context.compiledClazz.cast(compiled);
}
throw new IllegalArgumentException("painless does not know how to handle context [" + context.name + "]");
}
@Override
public SearchScript search(Object compiledScript, SearchLookup lookup, @Nullable Map<String, Object> vars) {
Expression expr = (Expression)compiledScript;
private SearchScript newSearchScript(Expression expr, SearchLookup lookup, @Nullable Map<String, Object> vars) {
MapperService mapper = lookup.doc().mapperService();
// NOTE: if we need to do anything complicated with bindings in the future, we can just extend Bindings,
// instead of complicating SimpleBindings (which should stay simple)
@ -246,9 +256,4 @@ public class ExpressionScriptEngine extends AbstractComponent implements ScriptE
stack.add(pointer.toString());
throw new ScriptException(message, cause, stack, source, NAME);
}
@Override
public ExecutableScript executable(Object compiledScript, Map<String, Object> vars) {
return new ExpressionExecutableScript((Expression) compiledScript, vars);
}
}

View File

@ -21,6 +21,7 @@ package org.elasticsearch.script.expression;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptException;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.lookup.SearchLookup;
@ -42,8 +43,8 @@ public class ExpressionTests extends ESSingleNodeTestCase {
}
private SearchScript compile(String expression) {
Object compiled = service.compile(null, expression, Collections.emptyMap());
return service.search(compiled, lookup, Collections.emptyMap());
SearchScript.Compiled compiled = service.compile(null, expression, ScriptContext.SEARCH, Collections.emptyMap());
return compiled.newInstance(Collections.emptyMap(), lookup);
}
public void testNeedsScores() {

View File

@ -31,6 +31,7 @@ import org.elasticsearch.common.logging.ESLoggerFactory;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.GeneralScriptException;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.lookup.SearchLookup;
@ -63,10 +64,15 @@ public final class MustacheScriptEngine implements ScriptEngine {
* @return a compiled template object for later execution.
* */
@Override
public Object compile(String templateName, String templateSource, Map<String, String> params) {
public <T> T compile(String templateName, String templateSource, ScriptContext<T> context, Map<String, String> params) {
if (context.instanceClazz.equals(ExecutableScript.class) == false) {
throw new IllegalArgumentException("mustache engine does not know how to handle context [" + context.name + "]");
}
final MustacheFactory factory = createMustacheFactory(params);
Reader reader = new FastStringReader(templateSource);
return factory.compile(reader, "query-template");
Mustache template = factory.compile(reader, "query-template");
ExecutableScript.Compiled compiled = p -> new MustacheExecutableScript(template, p);
return context.compiledClazz.cast(compiled);
}
private CustomMustacheFactory createMustacheFactory(Map<String, String> params) {
@ -81,21 +87,11 @@ public final class MustacheScriptEngine implements ScriptEngine {
return NAME;
}
@Override
public ExecutableScript executable(Object compiledScript, @Nullable Map<String, Object> vars) {
return new MustacheExecutableScript((Mustache) compiledScript, vars);
}
@Override
public SearchScript search(Object compiledScript, SearchLookup lookup, @Nullable Map<String, Object> vars) {
throw new UnsupportedOperationException();
}
/**
* Used at query execution time by script service in order to execute a query template.
* */
private class MustacheExecutableScript implements ExecutableScript {
/** Compiled template object wrapper. */
/** Compiled template. */
private Mustache template;
/** Parameters to fill above object with. */
private Map<String, Object> vars;

View File

@ -22,6 +22,7 @@ package org.elasticsearch.script.mustache;
import com.github.mustachejava.Mustache;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.test.ESTestCase;
@ -61,9 +62,9 @@ public class CustomMustacheFactoryTests extends ESTestCase {
final ScriptEngine engine = new MustacheScriptEngine();
final Map<String, String> params = randomBoolean() ? singletonMap(Script.CONTENT_TYPE_OPTION, JSON_MIME_TYPE) : emptyMap();
Mustache script = (Mustache) engine.compile(null, "{\"field\": \"{{value}}\"}", params);
ExecutableScript.Compiled compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", ScriptContext.EXECUTABLE, params);
ExecutableScript executable = engine.executable(script, singletonMap("value", "a \"value\""));
ExecutableScript executable = compiled.newInstance(singletonMap("value", "a \"value\""));
assertThat(executable.run(), equalTo("{\"field\": \"a \\\"value\\\"\"}"));
}
@ -71,9 +72,9 @@ public class CustomMustacheFactoryTests extends ESTestCase {
final ScriptEngine engine = new MustacheScriptEngine();
final Map<String, String> params = singletonMap(Script.CONTENT_TYPE_OPTION, PLAIN_TEXT_MIME_TYPE);
Mustache script = (Mustache) engine.compile(null, "{\"field\": \"{{value}}\"}", params);
ExecutableScript.Compiled compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", ScriptContext.EXECUTABLE, params);
ExecutableScript executable = engine.executable(script, singletonMap("value", "a \"value\""));
ExecutableScript executable = compiled.newInstance(singletonMap("value", "a \"value\""));
assertThat(executable.run(), equalTo("{\"field\": \"a \"value\"\"}"));
}
@ -81,9 +82,9 @@ public class CustomMustacheFactoryTests extends ESTestCase {
final ScriptEngine engine = new MustacheScriptEngine();
final Map<String, String> params = singletonMap(Script.CONTENT_TYPE_OPTION, X_WWW_FORM_URLENCODED_MIME_TYPE);
Mustache script = (Mustache) engine.compile(null, "{\"field\": \"{{value}}\"}", params);
ExecutableScript.Compiled compiled = engine.compile(null, "{\"field\": \"{{value}}\"}", ScriptContext.EXECUTABLE, params);
ExecutableScript executable = engine.executable(script, singletonMap("value", "tilde~ AND date:[2016 FROM*]"));
ExecutableScript executable = compiled.newInstance(singletonMap("value", "tilde~ AND date:[2016 FROM*]"));
assertThat(executable.run(), equalTo("{\"field\": \"tilde%7E+AND+date%3A%5B2016+FROM*%5D\"}"));
}
}

View File

@ -24,6 +24,7 @@ import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.Script;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.test.ESTestCase;
import org.junit.Before;
@ -55,7 +56,7 @@ public class MustacheScriptEngineTests extends ESTestCase {
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}" + "}}, \"negative_boost\": {{boost_val}} } }}";
Map<String, Object> vars = new HashMap<>();
vars.put("boost_val", "0.3");
String o = (String) qe.executable(qe.compile(null, template, compileParams), vars).run();
String o = (String) qe.compile(null, template, ScriptContext.EXECUTABLE, compileParams).newInstance(vars).run();
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"solr\"}}}, \"negative_boost\": 0.3 } }}",
o);
@ -66,7 +67,7 @@ public class MustacheScriptEngineTests extends ESTestCase {
Map<String, Object> vars = new HashMap<>();
vars.put("boost_val", "0.3");
vars.put("body_val", "\"quick brown\"");
String o = (String) qe.executable(qe.compile(null, template, compileParams), vars).run();
String o = (String) qe.compile(null, template, ScriptContext.EXECUTABLE, compileParams).newInstance(vars).run();
assertEquals("GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
+ "\"negative\": {\"term\": {\"body\": {\"value\": \"\\\"quick brown\\\"\"}}}, \"negative_boost\": 0.3 } }}",
o);
@ -81,8 +82,8 @@ public class MustacheScriptEngineTests extends ESTestCase {
+ "}";
XContentParser parser = createParser(JsonXContent.jsonXContent, templateString);
Script script = Script.parse(parser);
Object compiled = qe.compile(null, script.getIdOrCode(), Collections.emptyMap());
ExecutableScript executableScript = qe.executable(compiled, script.getParams());
ExecutableScript.Compiled compiled = qe.compile(null, script.getIdOrCode(), ScriptContext.EXECUTABLE, Collections.emptyMap());
ExecutableScript executableScript = compiled.newInstance(script.getParams());
assertThat(executableScript.run(), equalTo("{\"match_all\":{}}"));
}
@ -96,8 +97,8 @@ public class MustacheScriptEngineTests extends ESTestCase {
+ "}";
XContentParser parser = createParser(JsonXContent.jsonXContent, templateString);
Script script = Script.parse(parser);
Object compiled = qe.compile(null, script.getIdOrCode(), Collections.emptyMap());
ExecutableScript executableScript = qe.executable(compiled, script.getParams());
ExecutableScript.Compiled compiled = qe.compile(null, script.getIdOrCode(), ScriptContext.EXECUTABLE, Collections.emptyMap());
ExecutableScript executableScript = compiled.newInstance(script.getParams());
assertThat(executableScript.run(), equalTo("{ \"match_all\":{} }"));
}

View File

@ -24,6 +24,7 @@ import com.github.mustachejava.MustacheException;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.test.ESTestCase;
import org.hamcrest.Matcher;
@ -61,8 +62,8 @@ public class MustacheTests extends ESTestCase {
+ "}}, \"negative_boost\": {{boost_val}} } }}";
Map<String, Object> params = Collections.singletonMap("boost_val", "0.2");
Mustache mustache = (Mustache) engine.compile(null, template, Collections.emptyMap());
ExecutableScript result = engine.executable(mustache, params);
ExecutableScript.Compiled compiled = engine.compile(null, template, ScriptContext.EXECUTABLE, Collections.emptyMap());
ExecutableScript result = compiled.newInstance(params);
assertEquals(
"Mustache templating broken",
"GET _search {\"query\": {\"boosting\": {\"positive\": {\"match\": {\"body\": \"gift\"}},"
@ -73,27 +74,27 @@ public class MustacheTests extends ESTestCase {
public void testArrayAccess() throws Exception {
String template = "{{data.0}} {{data.1}}";
Object mustache = engine.compile(null, template, Collections.emptyMap());
ExecutableScript.Compiled compiled = engine.compile(null, template, ScriptContext.EXECUTABLE, Collections.emptyMap());
Map<String, Object> vars = new HashMap<>();
Object data = randomFrom(
new String[] { "foo", "bar" },
Arrays.asList("foo", "bar"));
vars.put("data", data);
assertThat(engine.executable(mustache, vars).run(), equalTo("foo bar"));
assertThat(compiled.newInstance(vars).run(), equalTo("foo bar"));
// Sets can come out in any order
Set<String> setData = new HashSet<>();
setData.add("foo");
setData.add("bar");
vars.put("data", setData);
Object output = engine.executable(mustache, vars).run();
Object output = compiled.newInstance(vars).run();
assertThat(output, instanceOf(String.class));
assertThat((String)output, both(containsString("foo")).and(containsString("bar")));
}
public void testArrayInArrayAccess() throws Exception {
String template = "{{data.0.0}} {{data.0.1}}";
Object mustache = engine.compile(null, template, Collections.emptyMap());
ExecutableScript.Compiled compiled = engine.compile(null, template, ScriptContext.EXECUTABLE, Collections.emptyMap());
Map<String, Object> vars = new HashMap<>();
Object data = randomFrom(
new String[][] { new String[] { "foo", "bar" }},
@ -101,25 +102,25 @@ public class MustacheTests extends ESTestCase {
singleton(new String[] { "foo", "bar" })
);
vars.put("data", data);
assertThat(engine.executable(mustache, vars).run(), equalTo("foo bar"));
assertThat(compiled.newInstance(vars).run(), equalTo("foo bar"));
}
public void testMapInArrayAccess() throws Exception {
String template = "{{data.0.key}} {{data.1.key}}";
Object mustache = engine.compile(null, template, Collections.emptyMap());
ExecutableScript.Compiled compiled= engine.compile(null, template, ScriptContext.EXECUTABLE, Collections.emptyMap());
Map<String, Object> vars = new HashMap<>();
Object data = randomFrom(
new Object[] { singletonMap("key", "foo"), singletonMap("key", "bar") },
Arrays.asList(singletonMap("key", "foo"), singletonMap("key", "bar")));
vars.put("data", data);
assertThat(engine.executable(mustache, vars).run(), equalTo("foo bar"));
assertThat(compiled.newInstance(vars).run(), equalTo("foo bar"));
// HashSet iteration order isn't fixed
Set<Object> setData = new HashSet<>();
setData.add(singletonMap("key", "foo"));
setData.add(singletonMap("key", "bar"));
vars.put("data", setData);
Object output = engine.executable(mustache, vars).run();
Object output = compiled.newInstance(vars).run();
assertThat(output, instanceOf(String.class));
assertThat((String)output, both(containsString("foo")).and(containsString("bar")));
}
@ -130,14 +131,14 @@ public class MustacheTests extends ESTestCase {
List<String> randomList = Arrays.asList(generateRandomStringArray(10, 20, false));
String template = "{{data.array.size}} {{data.list.size}}";
Object mustache = engine.compile(null, template, Collections.emptyMap());
ExecutableScript.Compiled compiled = engine.compile(null, template, ScriptContext.EXECUTABLE, Collections.emptyMap());
Map<String, Object> data = new HashMap<>();
data.put("array", randomArrayValues);
data.put("list", randomList);
Map<String, Object> vars = new HashMap<>();
vars.put("data", data);
String expectedString = String.format(Locale.ROOT, "%s %s", randomArrayValues.length, randomList.size());
assertThat(engine.executable(mustache, vars).run(), equalTo(expectedString));
assertThat(compiled.newInstance(vars).run(), equalTo(expectedString));
}
public void testPrimitiveToJSON() throws Exception {
@ -372,12 +373,12 @@ public class MustacheTests extends ESTestCase {
}
private void assertScript(String script, Map<String, Object> vars, Matcher<Object> matcher) {
Object result = engine.executable(compile(script), vars).run();
Object result = compile(script).newInstance(vars).run();
assertThat(result, matcher);
}
private Object compile(String script) {
private ExecutableScript.Compiled compile(String script) {
assertThat("cannot compile null or empty script", script, not(isEmptyOrNullString()));
return engine.compile(null, script, Collections.emptyMap());
return engine.compile(null, script, ScriptContext.EXECUTABLE, Collections.emptyMap());
}
}

View File

@ -26,10 +26,10 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.painless.Compiler.Loader;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.LeafSearchScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.ScriptException;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.lookup.SearchLookup;
import java.io.IOException;
import java.security.AccessControlContext;
@ -98,8 +98,25 @@ public final class PainlessScriptEngine extends AbstractComponent implements Scr
static final String INLINE_NAME = "<inline>";
@Override
public Object compile(String scriptName, final String scriptSource, final Map<String, String> params) {
return compile(GenericElasticsearchScript.class, scriptName, scriptSource, params);
public <T> T compile(String scriptName, String scriptSource, ScriptContext<T> context, Map<String, String> params) {
GenericElasticsearchScript painlessScript = compile(GenericElasticsearchScript.class, scriptName, scriptSource, params);
if (context.instanceClazz.equals(SearchScript.class)) {
SearchScript.Compiled compiled = (p, lookup) -> new SearchScript() {
@Override
public LeafSearchScript getLeafSearchScript(final LeafReaderContext context) throws IOException {
return new ScriptImpl(painlessScript, p, lookup.getLeafSearchLookup(context));
}
@Override
public boolean needsScores() {
return painlessScript.uses$_score();
}
};
return context.compiledClazz.cast(compiled);
} else if (context.instanceClazz.equals(ExecutableScript.class)) {
ExecutableScript.Compiled compiled = (p) -> new ScriptImpl(painlessScript, p, null);
return context.compiledClazz.cast(compiled);
}
throw new IllegalArgumentException("painless does not know how to handle context [" + context.name + "]");
}
<T> T compile(Class<T> iface, String scriptName, final String scriptSource, final Map<String, String> params) {
@ -168,47 +185,6 @@ public final class PainlessScriptEngine extends AbstractComponent implements Scr
}
}
/**
* Retrieve an {@link ExecutableScript} for later use.
* @param compiledScript A previously compiled script.
* @param vars The variables to be used in the script.
* @return An {@link ExecutableScript} with the currently specified variables.
*/
@Override
public ExecutableScript executable(final Object compiledScript, final Map<String, Object> vars) {
return new ScriptImpl((GenericElasticsearchScript) compiledScript, vars, null);
}
/**
* Retrieve a {@link SearchScript} for later use.
* @param compiledScript A previously compiled script.
* @param lookup The object that ultimately allows access to search fields.
* @param vars The variables to be used in the script.
* @return An {@link SearchScript} with the currently specified variables.
*/
@Override
public SearchScript search(final Object compiledScript, final SearchLookup lookup, final Map<String, Object> vars) {
return new SearchScript() {
/**
* Get the search script that will have access to search field values.
* @param context The LeafReaderContext to be used.
* @return A script that will have the search fields from the current context available for use.
*/
@Override
public LeafSearchScript getLeafSearchScript(final LeafReaderContext context) throws IOException {
return new ScriptImpl((GenericElasticsearchScript) compiledScript, vars, lookup.getLeafSearchLookup(context));
}
/**
* Whether or not the score is needed.
*/
@Override
public boolean needsScores() {
return ((GenericElasticsearchScript) compiledScript).uses$_score();
}
};
}
private ScriptException convertToScriptException(String scriptName, String scriptSource, Throwable t) {
// create a script stack: this is just the script portion
List<String> scriptStack = new ArrayList<>();

View File

@ -21,6 +21,7 @@ package org.elasticsearch.painless;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.IndexService;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.lookup.SearchLookup;
import org.elasticsearch.test.ESSingleNodeTestCase;
@ -39,20 +40,20 @@ public class NeedsScoreTests extends ESSingleNodeTestCase {
PainlessScriptEngine service = new PainlessScriptEngine(Settings.EMPTY);
SearchLookup lookup = new SearchLookup(index.mapperService(), index.fieldData(), null);
Object compiled = service.compile(null, "1.2", Collections.emptyMap());
SearchScript ss = service.search(compiled, lookup, Collections.emptyMap());
SearchScript.Compiled compiled = service.compile(null, "1.2", ScriptContext.SEARCH, Collections.emptyMap());
SearchScript ss = compiled.newInstance(Collections.emptyMap(), lookup);
assertFalse(ss.needsScores());
compiled = service.compile(null, "doc['d'].value", Collections.emptyMap());
ss = service.search(compiled, lookup, Collections.emptyMap());
compiled = service.compile(null, "doc['d'].value", ScriptContext.SEARCH, Collections.emptyMap());
ss = compiled.newInstance(Collections.emptyMap(), lookup);
assertFalse(ss.needsScores());
compiled = service.compile(null, "1/_score", Collections.emptyMap());
ss = service.search(compiled, lookup, Collections.emptyMap());
compiled = service.compile(null, "1/_score", ScriptContext.SEARCH, Collections.emptyMap());
ss = compiled.newInstance(Collections.emptyMap(), lookup);
assertTrue(ss.needsScores());
compiled = service.compile(null, "doc['d'].value * _score", Collections.emptyMap());
ss = service.search(compiled, lookup, Collections.emptyMap());
compiled = service.compile(null, "doc['d'].value * _score", ScriptContext.SEARCH, Collections.emptyMap());
ss = compiled.newInstance(Collections.emptyMap(), lookup);
assertTrue(ss.needsScores());
}

View File

@ -20,6 +20,7 @@
package org.elasticsearch.painless;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptContext;
import java.util.Arrays;
import java.util.Collections;
@ -79,9 +80,9 @@ public class ScriptEngineTests extends ScriptTestCase {
Map<String, Object> ctx = new HashMap<>();
vars.put("ctx", ctx);
Object compiledScript = scriptEngine.compile(null,
"return ctx.value;", Collections.emptyMap());
ExecutableScript script = scriptEngine.executable(compiledScript, vars);
ExecutableScript.Compiled compiledScript =
scriptEngine.compile(null, "return ctx.value;", ScriptContext.EXECUTABLE, Collections.emptyMap());
ExecutableScript script = compiledScript.newInstance(vars);
ctx.put("value", 1);
Object o = script.run();
@ -94,9 +95,9 @@ public class ScriptEngineTests extends ScriptTestCase {
public void testChangingVarsCrossExecution2() {
Map<String, Object> vars = new HashMap<>();
Object compiledScript = scriptEngine.compile(null, "return params['value'];", Collections.emptyMap());
ExecutableScript script = scriptEngine.executable(compiledScript, vars);
ExecutableScript.Compiled compiledScript =
scriptEngine.compile(null, "return params['value'];", ScriptContext.EXECUTABLE, Collections.emptyMap());
ExecutableScript script = compiledScript.newInstance(vars);
script.setNextVar("value", 1);
Object value = script.run();

View File

@ -26,6 +26,7 @@ import org.elasticsearch.common.lucene.ScorerAware;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.painless.antlr.Walker;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptException;
import org.elasticsearch.test.ESTestCase;
import org.junit.Before;
@ -85,8 +86,8 @@ public abstract class ScriptTestCase extends ESTestCase {
definition, null);
}
// test actual script execution
Object compiled = scriptEngine.compile(null, script, compileParams);
ExecutableScript executableScript = scriptEngine.executable(compiled, vars);
ExecutableScript.Compiled compiled = scriptEngine.compile(null, script, ScriptContext.EXECUTABLE, compileParams);
ExecutableScript executableScript = compiled.newInstance(vars);
if (scorer != null) {
((ScorerAware)executableScript).setScorer(scorer);
}

View File

@ -33,8 +33,10 @@ import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.plugins.ScriptPlugin;
import org.elasticsearch.script.ExecutableScript;
import org.elasticsearch.script.LeafSearchScript;
import org.elasticsearch.script.ScriptContext;
import org.elasticsearch.script.ScriptEngine;
import org.elasticsearch.script.SearchScript;
import org.elasticsearch.search.internal.SearchContext;
import org.elasticsearch.search.lookup.SearchLookup;
/**
@ -56,10 +58,13 @@ public class ExpertScriptPlugin extends Plugin implements ScriptPlugin {
}
@Override
public Function<Map<String,Object>,SearchScript> compile(String scriptName, String scriptSource, Map<String, String> params) {
public <T> T compile(String scriptName, String scriptSource, ScriptContext<T> context, Map<String, String> params) {
if (context.equals(ScriptContext.SEARCH) == false) {
throw new IllegalArgumentException(getType() + " scripts cannot be used for context [" + context.name + "]");
}
// we use the script "source" as the script identifier
if ("pure_df".equals(scriptSource)) {
return p -> new SearchScript() {
SearchScript.Compiled compiled = (p, lookup) -> new SearchScript() {
final String field;
final String term;
{
@ -114,22 +119,11 @@ public class ExpertScriptPlugin extends Plugin implements ScriptPlugin {
return false;
}
};
return context.compiledClazz.cast(compiled);
}
throw new IllegalArgumentException("Unknown script name " + scriptSource);
}
@Override
@SuppressWarnings("unchecked")
public SearchScript search(Object compiledScript, SearchLookup lookup, @Nullable Map<String, Object> params) {
Function<Map<String,Object>,SearchScript> scriptFactory = (Function<Map<String,Object>,SearchScript>) compiledScript;
return scriptFactory.apply(params);
}
@Override
public ExecutableScript executable(Object compiledScript, @Nullable Map<String, Object> params) {
throw new UnsupportedOperationException();
}
@Override
public void close() {
// optionally close resources

View File

@ -67,7 +67,7 @@ public class MockScriptEngine implements ScriptEngine {
}
@Override
public Object compile(String name, String source, Map<String, String> options) {
public <T> T compile(String name, String source, ScriptContext<T> context, Map<String, String> params) {
// Scripts are always resolved using the script's source. For inline scripts, it's easy because they don't have names and the
// source is always provided. For stored and file scripts, the source of the script must match the key of a predefined script.
Function<Map<String, Object>, Object> script = scripts.get(source);
@ -75,19 +75,15 @@ public class MockScriptEngine implements ScriptEngine {
throw new IllegalArgumentException("No pre defined script matching [" + source + "] for script with name [" + name + "], " +
"did you declare the mocked script?");
}
return new MockCompiledScript(name, options, source, script);
MockCompiledScript mockCompiled = new MockCompiledScript(name, params, source, script);
if (context.instanceClazz.equals(SearchScript.class)) {
SearchScript.Compiled compiled = mockCompiled::createSearchScript;
return context.compiledClazz.cast(compiled);
} else if (context.instanceClazz.equals(ExecutableScript.class)) {
ExecutableScript.Compiled compiled = mockCompiled::createExecutableScript;
return context.compiledClazz.cast(compiled);
}
@Override
public ExecutableScript executable(Object compiledScript, @Nullable Map<String, Object> vars) {
MockCompiledScript compiled = (MockCompiledScript) compiledScript;
return compiled.createExecutableScript(vars);
}
@Override
public SearchScript search(Object compiledScript, SearchLookup lookup, @Nullable Map<String, Object> vars) {
MockCompiledScript compiled = (MockCompiledScript) compiledScript;
return compiled.createSearchScript(vars, lookup);
throw new IllegalArgumentException("mock script engine does not know how to handle context [" + context.name + "]");
}
public class MockCompiledScript {