diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/scripts/20_get_script_context.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/scripts/20_get_script_context.yml index 881748ef13d79..64efeb7dfb4ba 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/scripts/20_get_script_context.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/scripts/20_get_script_context.yml @@ -4,24 +4,7 @@ reason: "get_all_contexts introduced in 7.6.0" - do: get_script_context: {} - - match: { contexts.aggregation_selector: {} } - - match: { contexts.aggs: {} } - - match: { contexts.aggs_combine: {} } - - match: { contexts.aggs_init: {} } - - match: { contexts.aggs_map: {} } - - match: { contexts.aggs_reduce: {} } - - match: { contexts.bucket_aggregation: {} } - - match: { contexts.field: {} } - - match: { contexts.filter: {} } - - match: { contexts.ingest: {} } - - match: { contexts.interval: {} } - - match: { contexts.number_sort: {} } - - match: { contexts.processor_conditional: {} } - - match: { contexts.score: {} } - - match: { contexts.script_heuristic: {} } - - match: { contexts.similarity: {} } - - match: { contexts.similarity_weight: {} } - - match: { contexts.string_sort: {} } - - match: { contexts.template: {} } - - match: { contexts.terms_set: {} } - - match: { contexts.update: {} } + + - is_true: contexts.0.name + - is_true: contexts.0.methods.0.return_type + - match: { contexts.0.methods.0.name: "execute" } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptContextResponse.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptContextResponse.java index fcbf2dc0fee1c..a478e234636be 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptContextResponse.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptContextResponse.java @@ -28,69 +28,72 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.script.ScriptContextInfo; import java.io.IOException; -import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; -import static org.elasticsearch.common.xcontent.XContentParser.Token.END_OBJECT; -import static org.elasticsearch.common.xcontent.XContentParser.Token.START_OBJECT; - public class GetScriptContextResponse extends ActionResponse implements StatusToXContentObject { private static final ParseField CONTEXTS = new ParseField("contexts"); - private final List contextNames; + final Map contexts; @SuppressWarnings("unchecked") public static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("get_script_context", true, (a) -> { - Map contexts = ((List) a[0]).stream().collect(Collectors.toMap( - name -> name, name -> new Object() - )); + Map contexts = ((List)a[0]).stream().collect( + Collectors.toMap(ScriptContextInfo::getName, c -> c) + ); return new GetScriptContextResponse(contexts); } ); static { - PARSER.declareNamedObjects( - ConstructingObjectParser.constructorArg(), - (p, c, n) -> - { - // advance empty object - assert(p.nextToken() == START_OBJECT); - assert(p.nextToken() == END_OBJECT); - return n; - }, - CONTEXTS - ); + PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(), + (parser, ctx) -> ScriptContextInfo.PARSER.apply(parser, ctx), CONTEXTS); } GetScriptContextResponse(StreamInput in) throws IOException { super(in); int size = in.readInt(); - ArrayList contextNames = new ArrayList<>(size); + HashMap contexts = new HashMap<>(size); for (int i = 0; i < size; i++) { - contextNames.add(in.readString()); + ScriptContextInfo info = new ScriptContextInfo(in); + contexts.put(info.name, info); } - this.contextNames = Collections.unmodifiableList(contextNames); + this.contexts = Collections.unmodifiableMap(contexts); + } + + // TransportAction constructor + GetScriptContextResponse(Set contexts) { + this.contexts = Map.copyOf(contexts.stream().collect( + Collectors.toMap(ScriptContextInfo::getName, Function.identity()) + )); + } + + // Parser constructor + private GetScriptContextResponse(Map contexts) { + this.contexts = Map.copyOf(contexts); } - GetScriptContextResponse(Map contexts) { - List contextNames = new ArrayList<>(contexts.keySet()); - contextNames.sort(String::compareTo); - this.contextNames = Collections.unmodifiableList(contextNames); + private List byName() { + return contexts.values().stream().sorted(Comparator.comparing(ScriptContextInfo::getName)).collect(Collectors.toList()); } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeInt(this.contextNames.size()); - for (String context: this.contextNames) { - out.writeString(context); + out.writeInt(contexts.size()); + for (ScriptContextInfo context: contexts.values()) { + context.writeTo(out); } } @@ -101,11 +104,11 @@ public RestStatus status() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject().startObject(CONTEXTS.getPreferredName()); - for (String contextName: this.contextNames) { - builder.startObject(contextName).endObject(); + builder.startObject().startArray(CONTEXTS.getPreferredName()); + for (ScriptContextInfo context: byName()) { + context.toXContent(builder, params); } - builder.endObject().endObject(); // CONTEXTS + builder.endArray().endObject(); // CONTEXTS return builder; } @@ -122,11 +125,11 @@ public boolean equals(Object o) { return false; } GetScriptContextResponse that = (GetScriptContextResponse) o; - return contextNames.equals(that.contextNames); + return contexts.equals(that.contexts); } @Override public int hashCode() { - return Objects.hash(contextNames); + return Objects.hash(contexts); } } diff --git a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/TransportGetScriptContextAction.java b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/TransportGetScriptContextAction.java index ea6c1fb37ef21..341d8973f37e4 100644 --- a/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/TransportGetScriptContextAction.java +++ b/server/src/main/java/org/elasticsearch/action/admin/cluster/storedscripts/TransportGetScriptContextAction.java @@ -22,12 +22,12 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.script.ScriptContextInfo; import org.elasticsearch.script.ScriptService; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; -import java.util.Map; -import java.util.stream.Collectors; +import java.util.Set; public class TransportGetScriptContextAction extends HandledTransportAction { @@ -41,9 +41,7 @@ public TransportGetScriptContextAction(TransportService transportService, Action @Override protected void doExecute(Task task, GetScriptContextRequest request, ActionListener listener) { - Map contexts = scriptService.getContextNames().stream().collect( - Collectors.toMap(name -> name, name -> new Object()) - ); + Set contexts = scriptService.getContextInfos(); listener.onResponse(new GetScriptContextResponse(contexts)); } } diff --git a/server/src/main/java/org/elasticsearch/script/ScriptContextInfo.java b/server/src/main/java/org/elasticsearch/script/ScriptContextInfo.java new file mode 100644 index 0000000000000..f72b933ae886c --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/ScriptContextInfo.java @@ -0,0 +1,384 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.script; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + +public class ScriptContextInfo implements ToXContentObject, Writeable { + public final String name; + public final ScriptMethodInfo execute; + public final Set getters; + + private static final String NAME_FIELD = "name"; + private static final String METHODS_FIELD = "methods"; + + // ScriptService constructor + ScriptContextInfo(String name, Class clazz) { + this.name = name; + this.execute = ScriptMethodInfo.executeFromContext(clazz); + this.getters = Collections.unmodifiableSet(ScriptMethodInfo.gettersFromContext(clazz)); + } + + // Deserialization constructor + ScriptContextInfo(String name, List methods) { + this.name = Objects.requireNonNull(name); + Objects.requireNonNull(methods); + + String executeName = "execute"; + String getName = "get"; + // ignored instead of error, so future implementations can add methods. Same as ScriptContextInfo(String, Class). + String otherName = "other"; + Map> methodTypes = methods.stream().collect(Collectors.groupingBy( + m -> { + if (m.name.equals(executeName)) { + return executeName; + } else if (m.name.startsWith(getName) && m.parameters.size() == 0) { + return getName; + } + return otherName; + } + )); + + if (methodTypes.containsKey(executeName) == false) { + throw new IllegalArgumentException("Could not find required method [" + executeName + "] in [" + name + "], found " + + methods.stream().map(m -> m.name).sorted().collect(Collectors.joining(", ", "[", "]"))); + } else if ((methodTypes.get(executeName).size() != 1)) { + throw new IllegalArgumentException("Cannot have multiple [execute] methods in [" + name + "], found [" + + methodTypes.get(executeName).size() + "]" + ); + } + this.execute = methodTypes.get(executeName).get(0); + + if (methodTypes.containsKey(getName)) { + this.getters = Set.copyOf(methodTypes.get(getName)); + } else { + this.getters = Collections.emptySet(); + } + } + + // Test constructor + public ScriptContextInfo(String name, ScriptMethodInfo execute, Set getters) { + this.name = Objects.requireNonNull(name); + this.execute = Objects.requireNonNull(execute); + this.getters = Objects.requireNonNull(getters); + } + + public ScriptContextInfo(StreamInput in) throws IOException { + this.name = in.readString(); + this.execute = new ScriptMethodInfo(in); + int numGetters = in.readInt(); + Set getters = new HashSet<>(numGetters); + for (int i = 0; i < numGetters; i++) { + getters.add(new ScriptMethodInfo(in)); + } + this.getters = Collections.unmodifiableSet(getters); + } + + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + execute.writeTo(out); + out.writeInt(getters.size()); + for (ScriptMethodInfo getter: getters) { + getter.writeTo(out); + } + } + + public String getName() { + return this.name; + } + + public List methods() { + ArrayList methods = new ArrayList<>(); + methods.add(this.execute); + methods.addAll(this.getters); + return Collections.unmodifiableList(methods); + } + + @SuppressWarnings("unchecked") + public static ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("script_context_info", true, + (m, name) -> new ScriptContextInfo((String) m[0], (List) m[1]) + ); + + static { + PARSER.declareString(constructorArg(), new ParseField(NAME_FIELD)); + PARSER.declareObjectArray(constructorArg(), + (parser, ctx) -> ScriptMethodInfo.PARSER.apply(parser, ctx), + new ParseField(METHODS_FIELD)); + } + + public static ScriptContextInfo fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ScriptContextInfo that = (ScriptContextInfo) o; + return Objects.equals(name, that.name) && + Objects.equals(execute, that.execute) && + Objects.equals(getters, that.getters); + } + + @Override + public int hashCode() { + return Objects.hash(name, execute, getters); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject().field(NAME_FIELD, name).startArray(METHODS_FIELD); + execute.toXContent(builder, params); + for (ScriptMethodInfo method: getters.stream().sorted(Comparator.comparing(g -> g.name)).collect(Collectors.toList())) { + method.toXContent(builder, params); + } + return builder.endArray().endObject(); + } + + public static class ScriptMethodInfo implements ToXContentObject, Writeable { + public final String name, returnType; + public final List parameters; + + static final String RETURN_TYPE_FIELD = "return_type"; + static final String PARAMETERS_FIELD = "params"; + + public ScriptMethodInfo(String name, String returnType, List parameters) { + this.name = Objects.requireNonNull(name); + this.returnType = Objects.requireNonNull(returnType); + this.parameters = Collections.unmodifiableList(Objects.requireNonNull(parameters)); + } + + public ScriptMethodInfo(StreamInput in) throws IOException { + this.name = in.readString(); + this.returnType = in.readString(); + int numParameters = in.readInt(); + ArrayList parameters = new ArrayList<>(numParameters); + for (int i = 0; i < numParameters; i++) { + parameters.add(new ParameterInfo(in)); + } + this.parameters = Collections.unmodifiableList(parameters); + } + + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeString(returnType); + out.writeInt(parameters.size()); + for (ParameterInfo parameter: parameters) { + parameter.writeTo(out); + } + } + + @SuppressWarnings("unchecked") + private static ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("method", true, + (m, name) -> new ScriptMethodInfo((String) m[0], (String) m[1], (List) m[2]) + ); + + static { + PARSER.declareString(constructorArg(), new ParseField(NAME_FIELD)); + PARSER.declareString(constructorArg(), new ParseField(RETURN_TYPE_FIELD)); + PARSER.declareObjectArray(constructorArg(), + (parser, ctx) -> ParameterInfo.PARSER.apply(parser, ctx), + new ParseField(PARAMETERS_FIELD)); + } + + public static ScriptMethodInfo fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ScriptMethodInfo that = (ScriptMethodInfo) o; + return Objects.equals(name, that.name) && + Objects.equals(returnType, that.returnType) && + Objects.equals(parameters, that.parameters); + } + + @Override + public int hashCode() { + return Objects.hash(name, returnType, parameters); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject().field(NAME_FIELD, name).field(RETURN_TYPE_FIELD, returnType).startArray(PARAMETERS_FIELD); + for (ParameterInfo parameter: parameters) { + parameter.toXContent(builder, params); + } + return builder.endArray().endObject(); + } + + public static class ParameterInfo implements ToXContentObject, Writeable { + public final String type, name; + + public static final String TYPE_FIELD = "type"; + + public ParameterInfo(String type, String name) { + this.type = Objects.requireNonNull(type); + this.name = Objects.requireNonNull(name); + } + + public ParameterInfo(StreamInput in) throws IOException { + this.type = in.readString(); + this.name = in.readString(); + } + + public void writeTo(StreamOutput out) throws IOException { + out.writeString(type); + out.writeString(name); + } + + private static ConstructingObjectParser PARSER = + new ConstructingObjectParser<>("parameters", true, + (p) -> new ParameterInfo((String)p[0], (String)p[1]) + ); + + static { + PARSER.declareString(constructorArg(), new ParseField(TYPE_FIELD)); + PARSER.declareString(constructorArg(), new ParseField(NAME_FIELD)); + } + + public static ParameterInfo fromXContent(XContentParser parser) throws IOException { + return PARSER.parse(parser, null); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field(TYPE_FIELD, this.type).field(NAME_FIELD, this.name).endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ParameterInfo that = (ParameterInfo) o; + return Objects.equals(type, that.type) && + Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(type, name); + } + } + + static ScriptMethodInfo executeFromContext(Class clazz) { + Method execute = null; + String name = "execute"; + + // See ScriptContext.findMethod + for (Method method : clazz.getMethods()) { + if (method.getName().equals(name)) { + if (execute != null) { + throw new IllegalArgumentException("Cannot have multiple [" + name + "] methods on class [" + + clazz.getName() + "]"); + } + execute = method; + } + } + if (execute == null) { + throw new IllegalArgumentException("Could not find required method [" + name + "] on class [" + clazz.getName() + "]"); + } + + Class returnTypeClazz = execute.getReturnType(); + String returnType = returnTypeClazz.getTypeName(); + + Class[] parameterTypes = execute.getParameterTypes(); + List parameters = new ArrayList<>(); + if (parameterTypes.length > 0) { + // TODO(stu): ensure empty/no PARAMETERS if parameterTypes.length == 0? + String parametersFieldName = "PARAMETERS"; + + // See ScriptClassInfo.readArgumentNamesConstant + Field parameterNamesField; + try { + parameterNamesField = clazz.getField(parametersFieldName); + } catch (NoSuchFieldException e) { + throw new IllegalArgumentException("Could not find field [" + parametersFieldName + "] on instance class [" + + clazz.getName() + "] but method [" + name + "] has [" + parameterTypes.length + "] parameters"); + } + if (!parameterNamesField.getType().equals(String[].class)) { + throw new IllegalArgumentException("Expected a constant [String[] PARAMETERS] on instance class [" + + clazz.getName() + "] for method [" + name + "] with [" + parameterTypes.length + "] parameters, found [" + + parameterNamesField.getType().getTypeName() + "]"); + } + + String[] argumentNames; + try { + argumentNames = (String[]) parameterNamesField.get(null); + } catch (IllegalArgumentException | IllegalAccessException e) { + throw new IllegalArgumentException("Error trying to read [" + clazz.getName() + "#ARGUMENTS]", e); + } + + if (argumentNames.length != parameterTypes.length) { + throw new IllegalArgumentException("Expected argument names [" + argumentNames.length + + "] to have the same arity [" + parameterTypes.length + "] for method [" + name + + "] of class [" + clazz.getName() + "]"); + } + + for (int i = 0; i < argumentNames.length; i++) { + parameters.add(new ParameterInfo(parameterTypes[i].getTypeName(), argumentNames[i])); + } + } + return new ScriptMethodInfo(name, returnType, parameters); + } + + static Set gettersFromContext(Class clazz) { + // See ScriptClassInfo(PainlessLookup painlessLookup, Class baseClass) + HashSet getters = new HashSet<>(); + for (java.lang.reflect.Method m : clazz.getMethods()) { + if (!m.isDefault() && + m.getName().startsWith("get") && + !m.getName().equals("getClass") && + !Modifier.isStatic(m.getModifiers()) && + m.getParameters().length == 0) { + getters.add(new ScriptMethodInfo(m.getName(), m.getReturnType().getTypeName(), new ArrayList<>())); + } + } + return getters; + } + } +} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptService.java b/server/src/main/java/org/elasticsearch/script/ScriptService.java index a59d22bcd5aea..fa2b15857c169 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptService.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptService.java @@ -538,8 +538,12 @@ public StoredScriptSource getStoredScript(ClusterState state, GetStoredScriptReq } } - public Set getContextNames() { - return contexts.keySet(); + public Set getContextInfos() { + Set infos = new HashSet(contexts.size()); + for (ScriptContext context : contexts.values()) { + infos.add(new ScriptContextInfo(context.name, context.instanceClazz)); + } + return infos; } public ScriptStats stats() { diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptContextResponseTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptContextResponseTests.java index 310469d7ae9fe..6896de4bc2fce 100644 --- a/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptContextResponseTests.java +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/GetScriptContextResponseTests.java @@ -24,24 +24,15 @@ import java.io.IOException; import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import static com.carrotsearch.randomizedtesting.RandomizedTest.randomAsciiLettersOfLengthBetween; public class GetScriptContextResponseTests extends AbstractSerializingTestCase { @Override protected GetScriptContextResponse createTestInstance() { if (randomBoolean()) { - return new GetScriptContextResponse(Collections.emptyMap()); - } - Map items = new HashMap<>(); - for (int i = randomIntBetween(1, 10); i > 0; i--) { - items.put(randomAsciiLettersOfLengthBetween(1, 16), new Object()); + return new GetScriptContextResponse(Collections.emptySet()); } - return new GetScriptContextResponse(items); - + return new GetScriptContextResponse(ScriptContextInfoSerializingTests.randomInstances()); } @Override @@ -56,10 +47,6 @@ protected GetScriptContextResponse doParseInstance(XContentParser parser) throws @Override protected GetScriptContextResponse mutateInstance(GetScriptContextResponse instance) throws IOException { - Map items = new HashMap<>(); - for (int i = randomIntBetween(1, 10); i > 0; i--) { - items.put(randomAsciiLettersOfLengthBetween(1, 16), new Object()); - } - return new GetScriptContextResponse(items); + return new GetScriptContextResponse(ScriptContextInfoSerializingTests.mutateOne(instance.contexts.values())); } } diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptContextInfoSerializingTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptContextInfoSerializingTests.java new file mode 100644 index 0000000000000..3a9fb4d3baa39 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptContextInfoSerializingTests.java @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.action.admin.cluster.storedscripts; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.ScriptContextInfo; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Collectors; + +public class ScriptContextInfoSerializingTests extends AbstractSerializingTestCase { + private static final int MIN_LENGTH = 1; + private static final int MAX_LENGTH = 16; + + @Override + protected ScriptContextInfo doParseInstance(XContentParser parser) throws IOException { + return ScriptContextInfo.fromXContent(parser); + } + + @Override + protected ScriptContextInfo createTestInstance() { + return randomInstance(); + } + + @Override + protected Writeable.Reader instanceReader() { return ScriptContextInfo::new; } + + + @Override + protected ScriptContextInfo mutateInstance(ScriptContextInfo instance) throws IOException { + return mutate(instance, null); + } + + private static ScriptContextInfo mutate(ScriptContextInfo instance, Set names) { + if (names == null) { names = new HashSet<>(); } + switch (randomIntBetween(0, 2)) { + case 0: + return new ScriptContextInfo( + randomValueOtherThanMany(names::contains, () -> randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH)), + instance.execute, + instance.getters + ); + case 1: + return new ScriptContextInfo( + instance.name, + ScriptMethodInfoSerializingTests.mutate(instance.execute), + instance.getters + ); + default: + return new ScriptContextInfo( + instance.name, + instance.execute, + ScriptMethodInfoSerializingTests.mutateOneGetter(instance.getters) + ); + } + } + + static Set mutateOne(Collection instances) { + if (instances.size() == 0) { + return Collections.unmodifiableSet(Set.of(randomInstance())); + } + ArrayList mutated = new ArrayList<>(instances); + int mutateIndex = randomIntBetween(0, instances.size() - 1); + mutated.set(mutateIndex, mutate(mutated.get(mutateIndex), instances.stream().map(i -> i.name).collect(Collectors.toSet()))); + return Set.copyOf(mutated); + } + + static ScriptContextInfo randomInstance() { + return new ScriptContextInfo( + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + ScriptMethodInfoSerializingTests.randomInstance(ScriptMethodInfoSerializingTests.NameType.EXECUTE), + ScriptMethodInfoSerializingTests.randomGetterInstances() + ); + } + + static Set randomInstances() { + Set names = new HashSet<>(); + int size = randomIntBetween(0, MAX_LENGTH); + HashSet instances = new HashSet<>(size); + for (int i = 0; i < size; i++) { + String name = randomValueOtherThanMany(names::contains, () -> randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH)); + names.add(name); + instances.add(new ScriptContextInfo( + name, + ScriptMethodInfoSerializingTests.randomInstance(ScriptMethodInfoSerializingTests.NameType.EXECUTE), + ScriptMethodInfoSerializingTests.randomGetterInstances() + )); + } + return Collections.unmodifiableSet(instances); + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptMethodInfoSerializingTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptMethodInfoSerializingTests.java new file mode 100644 index 0000000000000..4661c106ca952 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptMethodInfoSerializingTests.java @@ -0,0 +1,164 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.action.admin.cluster.storedscripts; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.ScriptContextInfo.ScriptMethodInfo; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class ScriptMethodInfoSerializingTests extends AbstractSerializingTestCase { + private static final String EXECUTE = "execute"; + private static final String GET_PREFIX = "get"; + private static final int MIN_LENGTH = 1; + private static final int MAX_LENGTH = 16; + + enum NameType { + EXECUTE, + GETTER, + OTHER; + static NameType fromName(String name) { + if (name.equals(ScriptMethodInfoSerializingTests.EXECUTE)) { + return EXECUTE; + } else if (name.startsWith(GET_PREFIX)) { + return GETTER; + } + return OTHER; + } + } + + @Override + protected ScriptMethodInfo doParseInstance(XContentParser parser) throws IOException { + return ScriptMethodInfo.fromXContent(parser); + } + + @Override + protected ScriptMethodInfo createTestInstance() { + return randomInstance(NameType.OTHER); + } + + @Override + protected Writeable.Reader instanceReader() { return ScriptMethodInfo::new; } + + @Override + protected ScriptMethodInfo mutateInstance(ScriptMethodInfo instance) throws IOException { + return mutate(instance); + } + + static ScriptMethodInfo randomInstance(NameType type) { + switch (type) { + case EXECUTE: + return new ScriptMethodInfo( + EXECUTE, + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + ScriptParameterInfoSerializingTests.randomInstances() + ); + case GETTER: + return new ScriptMethodInfo( + GET_PREFIX + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + Collections.unmodifiableList(new ArrayList<>()) + ); + default: + return new ScriptMethodInfo( + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + ScriptParameterInfoSerializingTests.randomInstances() + ); + } + } + + static ScriptMethodInfo mutate(ScriptMethodInfo instance) { + switch (NameType.fromName(instance.name)) { + case EXECUTE: + if (randomBoolean()) { + return new ScriptMethodInfo( + instance.name, + instance.returnType + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + instance.parameters + ); + } + return new ScriptMethodInfo( + instance.name, + instance.returnType, + ScriptParameterInfoSerializingTests.mutateOne(instance.parameters) + ); + case GETTER: + return new ScriptMethodInfo( + instance.name, + instance.returnType + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + instance.parameters + ); + default: + switch (randomIntBetween(0, 2)) { + case 0: + return new ScriptMethodInfo( + instance.name + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + instance.returnType, + instance.parameters + ); + case 1: + return new ScriptMethodInfo( + instance.name, + instance.returnType + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + instance.parameters + ); + default: + return new ScriptMethodInfo( + instance.name, + instance.returnType, + ScriptParameterInfoSerializingTests.mutateOne(instance.parameters) + ); + } + } + } + + static Set mutateOneGetter(Set instances) { + if (instances.size() == 0) { + return Set.of(randomInstance(NameType.GETTER)); + } + ArrayList mutated = new ArrayList<>(instances); + int mutateIndex = randomIntBetween(0, instances.size() - 1); + mutated.set(mutateIndex, mutate(mutated.get(mutateIndex))); + return Set.copyOf(mutated); + } + + static Set randomGetterInstances() { + Set suffixes = new HashSet<>(); + int numGetters = randomIntBetween(0, MAX_LENGTH); + Set getters = new HashSet<>(numGetters); + for (int i = 0; i < numGetters; i++) { + String suffix = randomValueOtherThanMany(suffixes::contains, () -> randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH)); + suffixes.add(suffix); + getters.add(new ScriptMethodInfo( + GET_PREFIX + suffix, + randomAlphaOfLengthBetween(MIN_LENGTH, MAX_LENGTH), + Collections.unmodifiableList(new ArrayList<>()) + )); + } + return Collections.unmodifiableSet(getters); + } +} diff --git a/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptParameterInfoSerializingTests.java b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptParameterInfoSerializingTests.java new file mode 100644 index 0000000000000..29728d86d8f02 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/action/admin/cluster/storedscripts/ScriptParameterInfoSerializingTests.java @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.action.admin.cluster.storedscripts; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.script.ScriptContextInfo.ScriptMethodInfo.ParameterInfo; +import org.elasticsearch.test.AbstractSerializingTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class ScriptParameterInfoSerializingTests extends AbstractSerializingTestCase { + private static int minLength = 1; + private static int maxLength = 8; + private static String baseType = "type-"; + private static String baseName = "name-"; + + @Override + protected ParameterInfo doParseInstance(XContentParser parser) throws IOException { + return ParameterInfo.fromXContent(parser); + } + + @Override + protected ParameterInfo createTestInstance() { + return randomInstance(); + } + + @Override + protected Writeable.Reader instanceReader() { + return ParameterInfo::new; + } + + @Override + protected ParameterInfo mutateInstance(ParameterInfo instance) throws IOException { + return mutate(instance); + } + + private static ParameterInfo mutate(ParameterInfo instance) { + if (randomBoolean()) { + return new ParameterInfo(instance.type + randomAlphaOfLengthBetween(minLength, maxLength), instance.name); + } + return new ParameterInfo(instance.type, instance.name + randomAlphaOfLengthBetween(minLength, maxLength)); + } + + static List mutateOne(List instances) { + if (instances.size() == 0) { + return Collections.unmodifiableList(List.of(randomInstance())); + } + ArrayList mutated = new ArrayList<>(instances); + int mutateIndex = randomIntBetween(0, instances.size() - 1); + mutated.set(mutateIndex, mutate(instances.get(mutateIndex))); + return Collections.unmodifiableList(mutated); + } + + static ParameterInfo randomInstance() { + return new ParameterInfo( + baseType + randomAlphaOfLengthBetween(minLength, maxLength), + baseName + randomAlphaOfLengthBetween(minLength, maxLength) + ); + } + + static List randomInstances() { + Set suffixes = new HashSet<>(); + int size = randomIntBetween(0, maxLength); + ArrayList instances = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + String suffix = randomValueOtherThanMany(suffixes::contains, () -> randomAlphaOfLengthBetween(minLength, maxLength)); + suffixes.add(suffix); + instances.add(new ParameterInfo( + baseType + randomAlphaOfLengthBetween(minLength, maxLength), + baseName + suffix + )); + } + return Collections.unmodifiableList(instances); + } +} diff --git a/server/src/test/java/org/elasticsearch/script/ScriptContextInfoTests.java b/server/src/test/java/org/elasticsearch/script/ScriptContextInfoTests.java new file mode 100644 index 0000000000000..df7cd96da08c8 --- /dev/null +++ b/server/src/test/java/org/elasticsearch/script/ScriptContextInfoTests.java @@ -0,0 +1,351 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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.elasticsearch.script; + +import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.xcontent.DeprecationHandler; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.script.ScriptContextInfo.ScriptMethodInfo; +import org.elasticsearch.script.ScriptContextInfo.ScriptMethodInfo.ParameterInfo; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class ScriptContextInfoTests extends ESTestCase { + public interface MinimalContext { + void execute(); + } + + public void testMinimalContext() { + String name = "minimal_context"; + ScriptContextInfo info = new ScriptContextInfo(name, MinimalContext.class); + assertEquals(name, info.name); + assertEquals("execute", info.execute.name); + assertEquals("void", info.execute.returnType); + assertEquals(0, info.execute.parameters.size()); + assertEquals(0, info.getters.size()); + } + + public static class PrimitiveContext { + public int execute(boolean foo, long bar, short baz, float qux) {return 0;} + public static final String[] PARAMETERS = {"foo", "bar", "baz", "qux"}; + public byte getByte() {return 0x00;} + public char getChar() {return 'a';} + } + + public void testPrimitiveContext() { + String name = "primitive_context"; + ScriptContextInfo info = new ScriptContextInfo(name, PrimitiveContext.class); + assertEquals(name, info.name); + assertEquals("execute", info.execute.name); + assertEquals("int", info.execute.returnType); + assertEquals(4, info.execute.parameters.size()); + List> eparams = new ArrayList<>(); + eparams.add(new Tuple<>("boolean", "foo")); + eparams.add(new Tuple<>("long", "bar")); + eparams.add(new Tuple<>("short", "baz")); + eparams.add(new Tuple<>("float", "qux")); + for (int i=0; i < info.execute.parameters.size(); i++) { + assertEquals(eparams.get(i).v1(), info.execute.parameters.get(i).type); + assertEquals(eparams.get(i).v2(), info.execute.parameters.get(i).name); + } + assertEquals(2, info.getters.size()); + HashMap getters = new HashMap(Map.of("getByte","byte", "getChar","char")); + for (ScriptContextInfo.ScriptMethodInfo getter: info.getters) { + assertEquals(0, getter.parameters.size()); + String returnType = getters.remove(getter.name); + assertNotNull(returnType); + assertEquals(returnType, getter.returnType); + } + assertEquals(0, getters.size()); + } + + + public static class CustomType0 {} + public static class CustomType1 {} + public static class CustomType2 {} + + public static class CustomTypeContext { + public CustomType0 execute(CustomType1 custom1, CustomType2 custom2) {return new CustomType0();} + public static final String[] PARAMETERS = {"custom1", "custom2"}; + public CustomType1 getCustom1() {return new CustomType1();} + public CustomType2 getCustom2() {return new CustomType2();} + } + + public void testCustomTypeContext() { + String ct = "org.elasticsearch.script.ScriptContextInfoTests$CustomType"; + String ct0 = ct + 0; + String ct1 = ct + 1; + String ct2 = ct + 2; + String name = "custom_type_context"; + ScriptContextInfo info = new ScriptContextInfo(name, CustomTypeContext.class); + assertEquals(name, info.name); + assertEquals("execute", info.execute.name); + assertEquals(ct0, info.execute.returnType); + assertEquals(2, info.execute.parameters.size()); + List> eparams = new ArrayList<>(); + eparams.add(new Tuple<>(ct1, "custom1")); + eparams.add(new Tuple<>(ct2, "custom2")); + for (int i=0; i < info.execute.parameters.size(); i++) { + assertEquals(eparams.get(i).v1(), info.execute.parameters.get(i).type); + assertEquals(eparams.get(i).v2(), info.execute.parameters.get(i).name); + } + assertEquals(2, info.getters.size()); + HashMap getters = new HashMap(Map.of("getCustom1",ct1, "getCustom2",ct2)); + for (ScriptContextInfo.ScriptMethodInfo getter: info.getters) { + assertEquals(0, getter.parameters.size()); + String returnType = getters.remove(getter.name); + assertNotNull(returnType); + assertEquals(returnType, getter.returnType); + } + assertEquals(0, getters.size()); + + HashMap methods = new HashMap(Map.of("getCustom1",ct1, "getCustom2",ct2, "execute",ct0)); + for (ScriptContextInfo.ScriptMethodInfo method: info.methods()) { + String returnType = methods.remove(method.name); + assertNotNull(returnType); + assertEquals(returnType, method.returnType); + } + assertEquals(0, methods.size()); + } + + public static class TwoExecute { + public void execute(int foo) {} + public boolean execute(boolean foo) {return foo;} + public static final String[] PARAMETERS = {"foo"}; + } + + public void testTwoExecute() { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> + new ScriptContextInfo("two_execute", TwoExecute.class)); + assertEquals("Cannot have multiple [execute] methods on class [" + TwoExecute.class.getName() + "]", e.getMessage()); + } + + public static class NoExecute {} + + public void testNoExecute() { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> + new ScriptContextInfo("no_execute", NoExecute.class)); + assertEquals("Could not find required method [execute] on class [" + NoExecute.class.getName() + "]", e.getMessage()); + } + + public static class NoParametersField { + public void execute(int foo) {} + } + + public void testNoParametersField() { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> + new ScriptContextInfo("no_parameters_field", NoParametersField.class)); + assertEquals("Could not find field [PARAMETERS] on instance class [" + NoParametersField.class.getName() + + "] but method [execute] has [1] parameters", e.getMessage()); + } + + public static class BadParametersFieldType { + public void execute(int foo) {} + public static final int[] PARAMETERS = {1}; + } + + public void testBadParametersFieldType() { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> + new ScriptContextInfo("bad_parameters_field_type", BadParametersFieldType.class)); + assertEquals("Expected a constant [String[] PARAMETERS] on instance class [" + BadParametersFieldType.class.getName() + + "] for method [execute] with [1] parameters, found [int[]]", e.getMessage()); + } + + public static class WrongNumberOfParameters { + public void execute(int foo) {} + public static final String[] PARAMETERS = {"foo", "bar"}; + } + + public void testWrongNumberOfParameters() { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> + new ScriptContextInfo("wrong_number_of_parameters", WrongNumberOfParameters.class)); + assertEquals("Expected argument names [2] to have the same arity [1] for method [execute] of class [" + + WrongNumberOfParameters.class.getName() + "]", e.getMessage()); + } + + public interface Default { + default int getDefault() {return 1;} + boolean getNonDefault1(); + } + + public static class GetterConditional implements Default { + public void execute() {} + public boolean getNonDefault1() {return true;} + public float getNonDefault2() {return 0.1f;} + public static long getStatic() {return 2L;} + public char getChar(char ch) { return ch;} + } + + public void testGetterConditional() { + Set getters = + new ScriptContextInfo("getter_conditional", GetterConditional.class).getters; + assertEquals(2, getters.size()); + HashMap methods = new HashMap(Map.of("getNonDefault1","boolean", "getNonDefault2","float")); + for (ScriptContextInfo.ScriptMethodInfo method: getters) { + String returnType = methods.remove(method.name); + assertNotNull(returnType); + assertEquals(returnType, method.returnType); + } + assertEquals(0, methods.size()); + } + + public void testParameterInfoParser() throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + + XContentParser parser = XContentType.JSON.xContent() + .createParser( + NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + new BytesArray("{\"type\":\"foo\", \"name\": \"bar\"}").streamInput()); + ScriptContextInfo.ScriptMethodInfo.ParameterInfo info = ScriptContextInfo.ScriptMethodInfo.ParameterInfo.fromXContent(parser); + assertEquals(new ScriptContextInfo.ScriptMethodInfo.ParameterInfo("foo", "bar"), info); + } + + public void testScriptMethodInfoParser() throws IOException { + String json = "{\"name\": \"fooFunc\", \"return_type\": \"int\", \"params\": [{\"type\": \"int\", \"name\": \"fooParam\"}, " + + "{\"type\": \"java.util.Map\", \"name\": \"barParam\"}]}"; + XContentParser parser = XContentType.JSON.xContent() + .createParser( + NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + new BytesArray(json).streamInput()); + ScriptContextInfo.ScriptMethodInfo info = ScriptContextInfo.ScriptMethodInfo.fromXContent(parser); + assertEquals(new ScriptContextInfo.ScriptMethodInfo("fooFunc", "int", new ArrayList<>( + Arrays.asList(new ScriptContextInfo.ScriptMethodInfo.ParameterInfo("int", "fooParam"), + new ScriptContextInfo.ScriptMethodInfo.ParameterInfo("java.util.Map", "barParam")) + )), info); + } + + public void testScriptContextInfoParser() throws IOException { + String json = "{" + + " \"name\": \"similarity\"," + + " \"methods\": [" + + " {" + + " \"name\": \"execute\"," + + " \"return_type\": \"double\"," + + " \"params\": [" + + " {" + + " \"type\": \"double\"," + + " \"name\": \"weight\"" + + " }," + + " {" + + " \"type\": \"org.elasticsearch.index.similarity.ScriptedSimilarity$Query\"," + + " \"name\": \"query\"" + + " }," + + " {" + + " \"type\": \"org.elasticsearch.index.similarity.ScriptedSimilarity$Field\"," + + " \"name\": \"field\"" + + " }," + + " {" + + " \"type\": \"org.elasticsearch.index.similarity.ScriptedSimilarity$Term\"," + + " \"name\": \"term\"" + + " }," + + " {" + + " \"type\": \"org.elasticsearch.index.similarity.ScriptedSimilarity$Doc\"," + + " \"name\": \"doc\"" + + " }" + + " ]" + + " }," + + " {" + + " \"name\": \"getParams\"," + + " \"return_type\": \"java.util.Map\"," + + " \"params\": []" + + " }," + + " {" + + " \"name\": \"getDoc\"," + + " \"return_type\": \"java.util.Map\"," + + " \"params\": []" + + " }," + + " {" + + " \"name\": \"get_score\"," + + " \"return_type\": \"double\"," + + " \"params\": []" + + " }" + + " ]" + + "}"; + XContentParser parser = XContentType.JSON.xContent() + .createParser( + NamedXContentRegistry.EMPTY, DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + new BytesArray(json).streamInput()); + ScriptContextInfo parsed = ScriptContextInfo.fromXContent(parser); + ScriptContextInfo expected = new ScriptContextInfo( + "similarity", + new ScriptMethodInfo( + "execute", + "double", + List.of( + new ParameterInfo("double", "weight"), + new ParameterInfo("org.elasticsearch.index.similarity.ScriptedSimilarity$Query", "query"), + new ParameterInfo("org.elasticsearch.index.similarity.ScriptedSimilarity$Field", "field"), + new ParameterInfo("org.elasticsearch.index.similarity.ScriptedSimilarity$Term", "term"), + new ParameterInfo("org.elasticsearch.index.similarity.ScriptedSimilarity$Doc", "doc") + ) + ), + Set.of( + new ScriptMethodInfo("getParams", "java.util.Map", new ArrayList<>()), + new ScriptMethodInfo("getDoc", "java.util.Map", new ArrayList<>()), + new ScriptMethodInfo("get_score", "double", new ArrayList<>()) + ) + ); + assertEquals(expected, parsed); + } + + public void testIgnoreOtherMethodsInListConstructor() { + ScriptContextInfo constructed = new ScriptContextInfo("otherNames", List.of( + new ScriptMethodInfo("execute", "double", Collections.emptyList()), + new ScriptMethodInfo("otherName", "bool", Collections.emptyList()) + )); + ScriptContextInfo expected = new ScriptContextInfo("otherNames", + new ScriptMethodInfo("execute", "double", Collections.emptyList()), + Collections.emptySet() + ); + assertEquals(expected, constructed); + } + + public void testNoExecuteInListConstructor() { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> + new ScriptContextInfo("noExecute", List.of( + new ScriptMethodInfo("getSomeOther", "int", Collections.emptyList()), + new ScriptMethodInfo("getSome", "bool", Collections.emptyList()) + ))); + assertEquals("Could not find required method [execute] in [noExecute], found [getSome, getSomeOther]", e.getMessage()); + } + + public void testMultipleExecuteInListConstructor() { + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () -> + new ScriptContextInfo("multiexecute", List.of( + new ScriptMethodInfo("execute", "double", Collections.emptyList()), + new ScriptMethodInfo("execute", "double", List.of( + new ParameterInfo("double", "weight") + ))))); + assertEquals("Cannot have multiple [execute] methods in [multiexecute], found [2]", e.getMessage()); + } +}