/* * 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.apache.logging.log4j.Logger; import org.elasticsearch.Version; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.bytes.BytesArray; 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.logging.DeprecationLogger; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.xcontent.ObjectParser; import org.elasticsearch.common.xcontent.ObjectParser.ValueType; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentFactory; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentParser.Token; import org.elasticsearch.common.xcontent.XContentType; import java.io.IOException; import java.io.UncheckedIOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; /** * {@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: * *
for an [" + ScriptType.STORED.getParseField().getPreferredName() + "] script");
}
if (options.isEmpty()) {
options = null;
} else {
throw new IllegalArgumentException("field [" + OPTIONS_PARSE_FIELD.getPreferredName() + "] " +
"cannot be specified using a [" + ScriptType.STORED.getParseField().getPreferredName() + "] script");
}
} else if (type == ScriptType.FILE) {
if (lang == null) {
lang = defaultLang;
}
if (idOrCode == null) {
throw new IllegalArgumentException(
"must specify for an [" + ScriptType.FILE.getParseField().getPreferredName() + "] script");
}
if (options.isEmpty()) {
options = null;
} else {
throw new IllegalArgumentException("field [" + OPTIONS_PARSE_FIELD.getPreferredName() + "] " +
"cannot be specified using a [" + ScriptType.FILE.getParseField().getPreferredName() + "] 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::setFile, ScriptType.FILE.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);
}
/**
* 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);
}
/**
* 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
* {
* "" : "",
* "lang" : "",
* "options" : {
* "option0" : "",
* "option1" : "",
* ...
* },
* "params" : {
* "param0" : "",
* "param1" : "",
* ...
* }
* }
* }
*
* Example:
* {@code
* {
* "inline" : "return Math.log(doc.popularity) * params.multiplier",
* "lang" : "painless",
* "params" : {
* "multiplier" : 100.0
* }
* }
* }
*
* This also handles templates in a special way. If a complexly formatted query is specified as another complex
* JSON object the query is assumed to be a template, and the format will be preserved.
*
* {@code
* {
* "inline" : { "query" : ... },
* "lang" : "",
* "options" : {
* "option0" : "",
* "option1" : "",
* ...
* },
* "params" : {
* "param0" : "",
* "param1" : "",
* ...
* }
* }
* }
*
* @param parser The {@link XContentParser} to be used.
* @param defaultLang The default language to use if no language is specified. The default language isn't necessarily
* the one defined by {@link Script#DEFAULT_SCRIPT_LANG} due to backwards compatiblity requirements
* related to stored queries using previously default languauges.
*
* @return The parsed {@link Script}.
*/
public static Script parse(XContentParser parser, String defaultLang) throws IOException {
Objects.requireNonNull(defaultLang);
Token token = parser.currentToken();
if (token == null) {
token = parser.nextToken();
}
if (token == Token.VALUE_STRING) {
return new Script(ScriptType.INLINE, defaultLang, parser.text(), Collections.emptyMap());
}
return PARSER.apply(parser, null).build(defaultLang);
}
private final ScriptType type;
private final String lang;
private final String idOrCode;
private final Map options;
private final Map params;
/**
* Constructor for simple script using the default language and default type.
* @param idOrCode The id or code to use dependent on the default script type.
*/
public Script(String idOrCode) {
this(DEFAULT_SCRIPT_TYPE, DEFAULT_SCRIPT_LANG, idOrCode, Collections.emptyMap(), Collections.emptyMap());
}
/**
* Constructor for a script that does not need to use compiler options.
* @param type The {@link ScriptType}.
* @param lang The language for this {@link Script} if the {@link ScriptType} is {@link ScriptType#INLINE} or
* {@link ScriptType#FILE}. For {@link ScriptType#STORED} scripts this should be null, but can
* be specified to access scripts stored as part of the stored scripts deprecated API.
* @param idOrCode The id for this {@link Script} if the {@link ScriptType} is {@link ScriptType#FILE} or {@link ScriptType#STORED}.
* The code for this {@link Script} if the {@link ScriptType} is {@link ScriptType#INLINE}.
* @param params The user-defined params to be bound for script execution.
*/
public Script(ScriptType type, String lang, String idOrCode, Map params) {
this(type, lang, idOrCode, type == ScriptType.INLINE ? Collections.emptyMap() : null, params);
}
/**
* Constructor for a script that requires the use of compiler options.
* @param type The {@link ScriptType}.
* @param lang The language for this {@link Script} if the {@link ScriptType} is {@link ScriptType#INLINE} or
* {@link ScriptType#FILE}. For {@link ScriptType#STORED} scripts this should be null, but can
* be specified to access scripts stored as part of the stored scripts deprecated API.
* @param idOrCode The id for this {@link Script} if the {@link ScriptType} is {@link ScriptType#FILE} or {@link ScriptType#STORED}.
* The code for this {@link Script} if the {@link ScriptType} is {@link ScriptType#INLINE}.
* @param options The map of compiler options for this {@link Script} if the {@link ScriptType}
* is {@link ScriptType#INLINE}, {@code null} otherwise.
* @param params The user-defined params to be bound for script execution.
*/
public Script(ScriptType type, String lang, String idOrCode, Map options, Map params) {
this.type = Objects.requireNonNull(type);
this.idOrCode = Objects.requireNonNull(idOrCode);
this.params = Collections.unmodifiableMap(Objects.requireNonNull(params));
if (type == ScriptType.INLINE) {
this.lang = Objects.requireNonNull(lang);
this.options = Collections.unmodifiableMap(Objects.requireNonNull(options));
} else if (type == ScriptType.STORED) {
this.lang = lang;
if (options != null) {
throw new IllegalStateException(
"options must be null for [" + ScriptType.STORED.getParseField().getPreferredName() + "] scripts");
}
this.options = null;
} else if (type == ScriptType.FILE) {
this.lang = Objects.requireNonNull(lang);
if (options != null) {
throw new IllegalStateException(
"options must be null for [" + ScriptType.FILE.getParseField().getPreferredName() + "] scripts");
}
this.options = null;
} else {
throw new IllegalStateException("unknown script type [" + type.getName() + "]");
}
}
/**
* Creates a {@link Script} read from an input stream.
*/
public Script(StreamInput in) throws IOException {
// Version 5.3 allows lang to be an optional parameter for stored scripts and expects
// options to be null for stored and file scripts.
if (in.getVersion().onOrAfter(Version.V_5_3_0_UNRELEASED)) {
this.type = ScriptType.readFrom(in);
this.lang = in.readOptionalString();
this.idOrCode = in.readString();
@SuppressWarnings("unchecked")
Map options = (Map)(Map)in.readMap();
this.options = options;
this.params = in.readMap();
// Version 5.1 to 5.3 (exclusive) requires all Script members to be non-null and supports the potential
// for more options than just XContentType. Reorders the read in contents to be in
// same order as the constructor.
} else if (in.getVersion().onOrAfter(Version.V_5_1_1_UNRELEASED)) {
this.type = ScriptType.readFrom(in);
this.lang = in.readString();
this.idOrCode = in.readString();
@SuppressWarnings("unchecked")
Map options = (Map)(Map)in.readMap();
if (this.type != ScriptType.INLINE && options.isEmpty()) {
this.options = null;
} else {
this.options = options;
}
this.params = in.readMap();
// Prior to version 5.1 the script members are read in certain cases as optional and given
// default values when necessary. Also the only option supported is for XContentType.
} else {
this.idOrCode = in.readString();
if (in.readBoolean()) {
this.type = ScriptType.readFrom(in);
} else {
this.type = DEFAULT_SCRIPT_TYPE;
}
String lang = in.readOptionalString();
if (lang == null) {
this.lang = DEFAULT_SCRIPT_LANG;
} else {
this.lang = lang;
}
Map params = in.readMap();
if (params == null) {
this.params = new HashMap<>();
} else {
this.params = params;
}
if (in.readBoolean()) {
this.options = new HashMap<>();
XContentType contentType = XContentType.readFrom(in);
this.options.put(CONTENT_TYPE_OPTION, contentType.mediaType());
} else if (type == ScriptType.INLINE) {
options = new HashMap<>();
} else {
this.options = null;
}
}
}
@Override
public void writeTo(StreamOutput out) throws IOException {
// Version 5.3+ allows lang to be an optional parameter for stored scripts and expects
// options to be null for stored and file scripts.
if (out.getVersion().onOrAfter(Version.V_5_3_0_UNRELEASED)) {
type.writeTo(out);
out.writeOptionalString(lang);
out.writeString(idOrCode);
@SuppressWarnings("unchecked")
Map options = (Map)(Map)this.options;
out.writeMap(options);
out.writeMap(params);
// Version 5.1 to 5.3 (exclusive) requires all Script members to be non-null and supports the potential
// for more options than just XContentType. Reorders the written out contents to be in
// same order as the constructor.
} else if (out.getVersion().onOrAfter(Version.V_5_1_1_UNRELEASED)) {
type.writeTo(out);
if (lang == null) {
out.writeString("");
} else {
out.writeString(lang);
}
out.writeString(idOrCode);
@SuppressWarnings("unchecked")
Map options = (Map)(Map)this.options;
if (options == null) {
out.writeMap(new HashMap<>());
} else {
out.writeMap(options);
}
out.writeMap(params);
// Prior to version 5.1 the Script members were possibly written as optional or null, though there is no case where a null
// value wasn't equivalent to it's default value when actually compiling/executing a script. Meaning, there are no
// backwards compatibility issues, and now there's enforced consistency. Also the only supported compiler
// option was XContentType.
} else {
out.writeString(idOrCode);
out.writeBoolean(true);
type.writeTo(out);
out.writeOptionalString(lang);
if (params.isEmpty()) {
out.writeMap(null);
} else {
out.writeMap(params);
}
if (options != null && options.containsKey(CONTENT_TYPE_OPTION)) {
XContentType contentType = XContentType.fromMediaTypeOrFormat(options.get(CONTENT_TYPE_OPTION));
out.writeBoolean(true);
contentType.writeTo(out);
} else {
out.writeBoolean(false);
}
}
}
/**
* This will build scripts into the following XContent structure:
*
* {@code
* {
* "" : "",
* "lang" : "",
* "options" : {
* "option0" : "",
* "option1" : "",
* ...
* },
* "params" : {
* "param0" : "",
* "param1" : "",
* ...
* }
* }
* }
*
* Example:
* {@code
* {
* "inline" : "return Math.log(doc.popularity) * params.multiplier;",
* "lang" : "painless",
* "params" : {
* "multiplier" : 100.0
* }
* }
* }
*
* Note that lang, options, and params will only be included if there have been any specified.
*
* This also handles templates in a special way. If the {@link Script#CONTENT_TYPE_OPTION} option
* is provided and the {@link ScriptType#INLINE} is specified then the template will be preserved as a raw field.
*
* {@code
* {
* "inline" : { "query" : ... },
* "lang" : "",
* "options" : {
* "option0" : "",
* "option1" : "",
* ...
* },
* "params" : {
* "param0" : "",
* "param1" : "",
* ...
* }
* }
* }
*/
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params builderParams) throws IOException {
builder.startObject();
String contentType = options == null ? null : options.get(CONTENT_TYPE_OPTION);
if (type == ScriptType.INLINE && contentType != null && builder.contentType().mediaType().equals(contentType)) {
builder.rawField(type.getParseField().getPreferredName(), new BytesArray(idOrCode));
} else {
builder.field(type.getParseField().getPreferredName(), idOrCode);
}
if (lang != null) {
builder.field(LANG_PARSE_FIELD.getPreferredName(), lang);
}
if (options != null && !options.isEmpty()) {
builder.field(OPTIONS_PARSE_FIELD.getPreferredName(), options);
}
if (!params.isEmpty()) {
builder.field(PARAMS_PARSE_FIELD.getPreferredName(), params);
}
builder.endObject();
return builder;
}
/**
* @return The {@link ScriptType} for this {@link Script}.
*/
public ScriptType getType() {
return type;
}
/**
* @return The language for this {@link Script} if the {@link ScriptType} is {@link ScriptType#INLINE} or
* {@link ScriptType#FILE}. For {@link ScriptType#STORED} scripts this should be null, but can
* be specified to access scripts stored as part of the stored scripts deprecated API.
*/
public String getLang() {
return lang;
}
/**
* @return The id for this {@link Script} if the {@link ScriptType} is {@link ScriptType#FILE} or {@link ScriptType#STORED}.
* The code for this {@link Script} if the {@link ScriptType} is {@link ScriptType#INLINE}.
*/
public String getIdOrCode() {
return idOrCode;
}
/**
* @return The map of compiler options for this {@link Script} if the {@link ScriptType}
* is {@link ScriptType#INLINE}, {@code null} otherwise.
*/
public Map getOptions() {
return options;
}
/**
* @return The map of user-defined params for this {@link Script}.
*/
public Map getParams() {
return params;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Script script = (Script)o;
if (type != script.type) return false;
if (lang != null ? !lang.equals(script.lang) : script.lang != null) return false;
if (!idOrCode.equals(script.idOrCode)) return false;
if (options != null ? !options.equals(script.options) : script.options != null) return false;
return params.equals(script.params);
}
@Override
public int hashCode() {
int result = type.hashCode();
result = 31 * result + (lang != null ? lang.hashCode() : 0);
result = 31 * result + idOrCode.hashCode();
result = 31 * result + (options != null ? options.hashCode() : 0);
result = 31 * result + params.hashCode();
return result;
}
@Override
public String toString() {
return "Script{" +
"type=" + type +
", lang='" + lang + '\'' +
", idOrCode='" + idOrCode + '\'' +
", options=" + options +
", params=" + params +
'}';
}
}