org.opensearch.script.Script Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opensearch Show documentation
Show all versions of opensearch Show documentation
OpenSearch subproject :server
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/
/*
* 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.
*/
/*
* Modifications Copyright OpenSearch Contributors. See
* GitHub history for details.
*/
package org.opensearch.script;
import org.opensearch.OpenSearchParseException;
import org.opensearch.common.annotation.PublicApi;
import org.opensearch.common.logging.DeprecationLogger;
import org.opensearch.common.settings.Settings;
import org.opensearch.common.xcontent.LoggingDeprecationHandler;
import org.opensearch.common.xcontent.XContentFactory;
import org.opensearch.common.xcontent.XContentType;
import org.opensearch.common.xcontent.json.JsonXContent;
import org.opensearch.core.ParseField;
import org.opensearch.core.common.bytes.BytesArray;
import org.opensearch.core.common.bytes.BytesReference;
import org.opensearch.core.common.io.stream.StreamInput;
import org.opensearch.core.common.io.stream.StreamOutput;
import org.opensearch.core.common.io.stream.Writeable;
import org.opensearch.core.xcontent.AbstractObjectParser;
import org.opensearch.core.xcontent.MediaTypeRegistry;
import org.opensearch.core.xcontent.NamedXContentRegistry;
import org.opensearch.core.xcontent.ObjectParser;
import org.opensearch.core.xcontent.ObjectParser.ValueType;
import org.opensearch.core.xcontent.ToXContent;
import org.opensearch.core.xcontent.ToXContentObject;
import org.opensearch.core.xcontent.XContentBuilder;
import org.opensearch.core.xcontent.XContentParser;
import org.opensearch.core.xcontent.XContentParser.Token;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.BiConsumer;
/**
* {@link Script} represents used-defined input that can be used to
* compile and execute a script from the {@link ScriptService}
* based on the {@link ScriptType}.
*
* There are three types of scripts specified by {@link ScriptType}.
*
* The following describes the expected parameters for each type of script:
*
*
* - {@link ScriptType#INLINE}
*
* - {@link Script#lang} - specifies the language, defaults to {@link Script#DEFAULT_SCRIPT_LANG}
*
- {@link Script#idOrCode} - specifies the code to be compiled, must not be {@code null}
*
- {@link Script#options} - specifies the compiler options for this script; must not be {@code null},
* use an empty {@link Map} to specify no options
*
- {@link Script#params} - {@link Map} of user-defined parameters; must not be {@code null},
* use an empty {@link Map} to specify no params
*
* - {@link ScriptType#STORED}
*
* - {@link Script#lang} - the language will be specified when storing the script, so this should
* be {@code null}
*
- {@link Script#idOrCode} - specifies the id of the stored script to be looked up, must not be {@code null}
*
- {@link Script#options} - compiler options will be specified when a stored script is stored,
* so they have no meaning here and must be {@code null}
*
- {@link Script#params} - {@link Map} of user-defined parameters; must not be {@code null},
* use an empty {@link Map} to specify no params
*
*
*
* @opensearch.api
*/
@PublicApi(since = "1.0.0")
public final class Script implements ToXContentObject, Writeable {
private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(Script.class);
/**
* The name of the of the default scripting language.
*/
public static final String DEFAULT_SCRIPT_LANG = "painless";
/**
* The name of the default template language.
*/
public static final String DEFAULT_TEMPLATE_LANG = "mustache";
/**
* The default {@link ScriptType}.
*/
public static final ScriptType DEFAULT_SCRIPT_TYPE = ScriptType.INLINE;
/**
* Compiler option for {@link XContentType} used for templates.
*/
public static final String CONTENT_TYPE_OPTION = "content_type";
/**
* Standard {@link ParseField} for outer level of script queries.
*/
public static final ParseField SCRIPT_PARSE_FIELD = new ParseField("script");
/**
* Standard {@link ParseField} for source on the inner level.
*/
public static final ParseField SOURCE_PARSE_FIELD = new ParseField("source");
/**
* Standard {@link ParseField} for lang on the inner level.
*/
public static final ParseField LANG_PARSE_FIELD = new ParseField("lang");
/**
* Standard {@link ParseField} for options on the inner level.
*/
public static final ParseField OPTIONS_PARSE_FIELD = new ParseField("options");
/**
* Standard {@link ParseField} for params on the inner level.
*/
public static final ParseField PARAMS_PARSE_FIELD = new ParseField("params");
/**
* Helper class used by {@link ObjectParser} to store mutable {@link Script} variables and then
* construct an immutable {@link Script} object based on parsed XContent.
*/
private static final class Builder {
private ScriptType type;
private String lang;
private String idOrCode;
private Map options;
private Map params;
private Builder() {
// This cannot default to an empty map because options are potentially added at multiple points.
this.options = new HashMap<>();
this.params = Collections.emptyMap();
}
/**
* Since inline scripts can accept code rather than just an id, they must also be able
* to handle template parsing, hence the need for custom parsing code. Templates can
* consist of either an {@link String} or a JSON object. If a JSON object is discovered
* then the content type option must also be saved as a compiler option.
*/
private void setInline(XContentParser parser) {
try {
if (type != null) {
throwOnlyOneOfType();
}
type = ScriptType.INLINE;
if (parser.currentToken() == Token.START_OBJECT) {
// this is really for search templates, that need to be converted to json format
XContentBuilder builder = XContentFactory.jsonBuilder();
idOrCode = builder.copyCurrentStructure(parser).toString();
options.put(CONTENT_TYPE_OPTION, MediaTypeRegistry.JSON.mediaType());
} else {
idOrCode = parser.text();
}
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
/**
* Set both the id and the type of the stored script.
*/
private void setStored(String idOrCode) {
if (type != null) {
throwOnlyOneOfType();
}
type = ScriptType.STORED;
this.idOrCode = idOrCode;
}
/**
* Helper method to throw an exception if more than one type of {@link Script} is specified.
*/
private void throwOnlyOneOfType() {
throw new IllegalArgumentException(
"must only use one of ["
+ ScriptType.INLINE.getParseField().getPreferredName()
+ ", "
+ ScriptType.STORED.getParseField().getPreferredName()
+ "]"
+ " when specifying a script"
);
}
private void setLang(String lang) {
this.lang = lang;
}
/**
* Options may have already been added if an inline template was specified.
* Appends the user-defined compiler options with the internal compiler options.
*/
private void setOptions(Map options) {
this.options.putAll(options);
}
private void setParams(Map params) {
this.params = params;
}
/**
* Validates the parameters and creates an {@link Script}.
* @param defaultLang The default lang is not a compile-time constant and must be provided
* at run-time this way in case a legacy default language is used from
* previously stored queries.
*/
private Script build(String defaultLang) {
if (type == null) {
throw new IllegalArgumentException("must specify either [source] for an inline script or [id] for a stored script");
}
if (type == ScriptType.INLINE) {
if (lang == null) {
lang = defaultLang;
}
if (idOrCode == null) {
throw new IllegalArgumentException("must specify for an inline script");
}
if (options.size() > 1 || options.size() == 1 && options.get(CONTENT_TYPE_OPTION) == null) {
options.remove(CONTENT_TYPE_OPTION);
throw new IllegalArgumentException("illegal compiler options [" + options + "] specified");
}
} else if (type == ScriptType.STORED) {
if (lang != null) {
throw new IllegalArgumentException("illegally specified for a stored script");
}
if (idOrCode == null) {
throw new IllegalArgumentException("must specify for a stored script");
}
if (options.isEmpty()) {
options = null;
} else {
throw new IllegalArgumentException(
"field [" + OPTIONS_PARSE_FIELD.getPreferredName() + "] " + "cannot be specified using a stored script"
);
}
}
return new Script(type, lang, idOrCode, options, params);
}
}
private static final ObjectParser PARSER = new ObjectParser<>("script", Builder::new);
static {
// Defines the fields necessary to parse a Script as XContent using an ObjectParser.
PARSER.declareField(Builder::setInline, parser -> parser, ScriptType.INLINE.getParseField(), ValueType.OBJECT_OR_STRING);
PARSER.declareString(Builder::setStored, ScriptType.STORED.getParseField());
PARSER.declareString(Builder::setLang, LANG_PARSE_FIELD);
PARSER.declareField(Builder::setOptions, XContentParser::mapStrings, OPTIONS_PARSE_FIELD, ValueType.OBJECT);
PARSER.declareField(Builder::setParams, XContentParser::map, PARAMS_PARSE_FIELD, ValueType.OBJECT);
}
/**
* Declare a script field on an {@link ObjectParser} with the standard name ({@code script}).
* @param Whatever type the {@linkplain ObjectParser} is parsing.
* @param parser the parser itself
* @param consumer the consumer for the script
*/
public static void declareScript(AbstractObjectParser parser, BiConsumer consumer) {
declareScript(parser, consumer, Script.SCRIPT_PARSE_FIELD);
}
/**
* Declare a script field on an {@link ObjectParser}.
* @param Whatever type the {@linkplain ObjectParser} is parsing.
* @param parser the parser itself
* @param consumer the consumer for the script
* @param parseField the field name
*/
public static void declareScript(AbstractObjectParser parser, BiConsumer consumer, ParseField parseField) {
parser.declareField(consumer, (p, c) -> Script.parse(p), parseField, ValueType.OBJECT_OR_STRING);
}
/**
* Convenience method to call {@link Script#parse(XContentParser, String)}
* using the default scripting language.
*/
public static Script parse(XContentParser parser) throws IOException {
return parse(parser, DEFAULT_SCRIPT_LANG);
}
/**
* Parse the script configured in the given settings.
*/
public static Script parse(Settings settings) {
try (XContentBuilder builder = JsonXContent.contentBuilder()) {
builder.startObject();
settings.toXContent(builder, ToXContent.EMPTY_PARAMS);
builder.endObject();
try (
InputStream stream = BytesReference.bytes(builder).streamInput();
XContentParser parser = JsonXContent.jsonXContent.createParser(
NamedXContentRegistry.EMPTY,
LoggingDeprecationHandler.INSTANCE,
stream
)
) {
return parse(parser);
}
} catch (IOException e) {
// it should not happen since we are not actually reading from a stream but an in-memory byte[]
throw new IllegalStateException(e);
}
}
/**
* This will parse XContent into a {@link Script}. The following formats can be parsed:
*
* The simple format defaults to an {@link ScriptType#INLINE} with no compiler options or user-defined params:
*
* Example:
* {@code
* "return Math.log(doc.popularity) * 100;"
* }
*
* The complex format where {@link ScriptType} and idOrCode are required while lang, options and params are not required.
*
* {@code
* {
* // Exactly one of "id" or "source" must be specified
* "id" : "",
* // OR
* "source": "