/*
* 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.Version;
import org.elasticsearch.action.admin.cluster.storedscripts.GetStoredScriptResponse;
import org.elasticsearch.cluster.AbstractDiffable;
import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.cluster.Diff;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.ParsingException;
import org.elasticsearch.common.bytes.BytesArray;
import org.elasticsearch.common.bytes.BytesReference;
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.NamedXContentRegistry;
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 StoredScriptSource} represents user-defined parameters for a script
* saved in the {@link ClusterState}.
*/
public class StoredScriptSource extends AbstractDiffable<StoredScriptSource> implements Writeable, ToXContentObject {
/**
* Standard {@link ParseField} for outer level of stored script source.
*/
public static final ParseField SCRIPT_PARSE_FIELD = new ParseField("script");
/**
* Standard {@link ParseField} for outer level of stored script source.
*/
public static final ParseField TEMPLATE_PARSE_FIELD = new ParseField("template");
/**
* Standard {@link ParseField} for lang on the inner level.
*/
public static final ParseField LANG_PARSE_FIELD = new ParseField("lang");
/**
* Standard {@link ParseField} for code on the inner level.
*/
public static final ParseField CODE_PARSE_FIELD = new ParseField("code");
/**
* Standard {@link ParseField} for options on the inner level.
*/
public static final ParseField OPTIONS_PARSE_FIELD = new ParseField("options");
/**
* Helper class used by {@link ObjectParser} to store mutable {@link StoredScriptSource} variables and then
* construct an immutable {@link StoredScriptSource} object based on parsed XContent.
*/
private static final class Builder {
private String lang;
private String code;
private Map<String, String> options;
private Builder() {
// This cannot default to an empty map because options are potentially added at multiple points.
this.options = new HashMap<>();
}
private void setLang(String lang) {
this.lang = lang;
}
/**
* Since stored scripts can accept templates rather than just scripts, 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 setCode(XContentParser parser) {
try {
if (parser.currentToken() == Token.START_OBJECT) {
//this is really for search templates, that need to be converted to json format
XContentBuilder builder = XContentFactory.jsonBuilder();
code = builder.copyCurrentStructure(parser).string();
options.put(Script.CONTENT_TYPE_OPTION, XContentType.JSON.mediaType());
} else {
code = parser.text();
}
} catch (IOException exception) {
throw new UncheckedIOException(exception);
}
}
/**
* Options may have already been added if a template was specified.
* Appends the user-defined compiler options with the internal compiler options.
*/
private void setOptions(Map<String, String> options) {
this.options.putAll(options);
}
/**
* Validates the parameters and creates an {@link StoredScriptSource}.
*/
private StoredScriptSource build() {
if (lang == null) {
throw new IllegalArgumentException("must specify lang for stored script");
} else if (lang.isEmpty()) {
throw new IllegalArgumentException("lang cannot be empty");
}
if (code == null) {
throw new IllegalArgumentException("must specify code for stored script");
} else if (code.isEmpty()) {
throw new IllegalArgumentException("code cannot be empty");
}
if (options.size() > 1 || options.size() == 1 && options.get(Script.CONTENT_TYPE_OPTION) == null) {
throw new IllegalArgumentException("illegal compiler options [" + options + "] specified");
}
return new StoredScriptSource(lang, code, options);
}
}
private static final ObjectParser<Builder, Void> PARSER = new ObjectParser<>("stored script source", Builder::new);
static {
// Defines the fields necessary to parse a Script as XContent using an ObjectParser.
PARSER.declareString(Builder::setLang, LANG_PARSE_FIELD);
PARSER.declareField(Builder::setCode, parser -> parser, CODE_PARSE_FIELD, ValueType.OBJECT_OR_STRING);
PARSER.declareField(Builder::setOptions, XContentParser::mapStrings, OPTIONS_PARSE_FIELD, ValueType.OBJECT);
}
/**
* This will parse XContent into a {@link StoredScriptSource}. The following formats can be parsed:
*
* The simple script format with no compiler options or user-defined params:
*
* Example:
* {@code
* {"script": "return Math.log(doc.popularity) * 100;"}
* }
*
* The above format requires the lang to be specified using the deprecated stored script namespace
* (as a url parameter during a put request). See {@link ScriptMetaData} for more information about
* the stored script namespaces.
*
* The complex script format using the new stored script namespace
* where lang and code are required but options is optional:
*
* {@code
* {
* "script" : {
* "lang" : "<lang>",
* "code" : "<code>",
* "options" : {
* "option0" : "<option0>",
* "option1" : "<option1>",
* ...
* }
* }
* }
* }
*
* Example:
* {@code
* {
* "script": {
* "lang" : "painless",
* "code" : "return Math.log(doc.popularity) * params.multiplier"
* }
* }
* }
*
* The simple template format:
*
* {@code
* {
* "query" : ...
* }
* }
*
* The complex template format:
*
* {@code
* {
* "template": {
* "query" : ...
* }
* }
* }
*
* Note that templates can be handled as both strings and complex JSON objects.
* Also templates may be part of the 'code' parameter in a script. The Parser
* can handle this case as well.
*
* @param lang An optional parameter to allow for use of the deprecated stored
* script namespace. This will be used to specify the language
* coming in as a url parameter from a request or for stored templates.
* @param content The content from the request to be parsed as described above.
* @return The parsed {@link StoredScriptSource}.
*/
public static StoredScriptSource parse(String lang, BytesReference content, XContentType xContentType) {
try (XContentParser parser = xContentType.xContent().createParser(NamedXContentRegistry.EMPTY, content)) {
Token token = parser.nextToken();
if (token != Token.START_OBJECT) {
throw new ParsingException(parser.getTokenLocation(), "unexpected token [" + token + "], expected [{]");
}
token = parser.nextToken();
if (token != Token.FIELD_NAME) {
throw new ParsingException(parser.getTokenLocation(), "unexpected token [" + token + ", expected [" +
SCRIPT_PARSE_FIELD.getPreferredName() + ", " + TEMPLATE_PARSE_FIELD.getPreferredName());
}
String name = parser.currentName();
if (SCRIPT_PARSE_FIELD.getPreferredName().equals(name)) {
token = parser.nextToken();
if (token == Token.VALUE_STRING) {
if (lang == null) {
throw new IllegalArgumentException(
"must specify lang as a url parameter when using the deprecated stored script namespace");
}
return new StoredScriptSource(lang, parser.text(), Collections.emptyMap());
} else if (token == Token.START_OBJECT) {
if (lang == null) {
return PARSER.apply(parser, null).build();
} else {
//this is really for search templates, that need to be converted to json format
try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
builder.copyCurrentStructure(parser);
return new StoredScriptSource(lang, builder.string(), Collections.emptyMap());
}
}
} else {
throw new ParsingException(parser.getTokenLocation(), "unexpected token [" + token + "], expected [{, <code>]");
}
} else {
if (lang == null) {
throw new IllegalArgumentException("unexpected stored script format");
}
if (TEMPLATE_PARSE_FIELD.getPreferredName().equals(name)) {
token = parser.nextToken();
if (token == Token.VALUE_STRING) {
return new StoredScriptSource(lang, parser.text(), Collections.emptyMap());
}
}
try (XContentBuilder builder = XContentFactory.jsonBuilder()) {
if (token != Token.START_OBJECT) {
builder.startObject();
builder.copyCurrentStructure(parser);
builder.endObject();
} else {
builder.copyCurrentStructure(parser);
}
return new StoredScriptSource(lang, builder.string(), Collections.emptyMap());
}
}
} catch (IOException ioe) {
throw new UncheckedIOException(ioe);
}
}
/**
* This will parse XContent into a {@link StoredScriptSource}. The following format is what will be parsed:
*
* {@code
* {
* "script" : {
* "lang" : "<lang>",
* "code" : "<code>",
* "options" : {
* "option0" : "<option0>",
* "option1" : "<option1>",
* ...
* }
* }
* }
* }
*
* Note that the "code" parameter can also handle template parsing including from
* a complex JSON object.
*/
public static StoredScriptSource fromXContent(XContentParser parser) throws IOException {
return PARSER.apply(parser, null).build();
}
/**
* Required for {@link ScriptMetaData.ScriptMetadataDiff}. Uses
* the {@link StoredScriptSource#StoredScriptSource(StreamInput)}
* constructor.
*/
public static Diff<StoredScriptSource> readDiffFrom(StreamInput in) throws IOException {
return readDiffFrom(StoredScriptSource::new, in);
}
private final String lang;
private final String code;
private final Map<String, String> options;
/**
* Constructor for use with {@link GetStoredScriptResponse}
* to support the deprecated stored script namespace.
*/
public StoredScriptSource(String code) {
this.lang = null;
this.code = Objects.requireNonNull(code);
this.options = null;
}
/**
* Standard StoredScriptSource constructor.
* @param lang The language to compile the script with. Must not be {@code null}.
* @param code The source code to compile with. Must not be {@code null}.
* @param options Compiler options to be compiled with. Must not be {@code null},
* use an empty {@link Map} to represent no options.
*/
public StoredScriptSource(String lang, String code, Map<String, String> options) {
this.lang = Objects.requireNonNull(lang);
this.code = Objects.requireNonNull(code);
this.options = Collections.unmodifiableMap(Objects.requireNonNull(options));
}
/**
* Reads a {@link StoredScriptSource} from a stream. Version 5.3+ will read
* all of the lang, code, and options parameters. For versions prior to 5.3,
* only the code parameter will be read in as a bytes reference.
*/
public StoredScriptSource(StreamInput in) throws IOException {
if (in.getVersion().onOrAfter(Version.V_5_3_0_UNRELEASED)) {
this.lang = in.readString();
this.code = in.readString();
@SuppressWarnings("unchecked")
Map<String, String> options = (Map<String, String>)(Map)in.readMap();
this.options = options;
} else {
this.lang = null;
this.code = in.readBytesReference().utf8ToString();
this.options = null;
}
}
/**
* Writes a {@link StoredScriptSource} to a stream. Version 5.3+ will write
* all of the lang, code, and options parameters. For versions prior to 5.3,
* only the code parameter will be read in as a bytes reference.
*/
@Override
public void writeTo(StreamOutput out) throws IOException {
if (out.getVersion().onOrAfter(Version.V_5_3_0_UNRELEASED)) {
out.writeString(lang);
out.writeString(code);
@SuppressWarnings("unchecked")
Map<String, Object> options = (Map<String, Object>)(Map)this.options;
out.writeMap(options);
} else {
out.writeBytesReference(new BytesArray(code));
}
}
/**
* This will write XContent from a {@link StoredScriptSource}. The following format will be written:
*
* {@code
* {
* "script" : {
* "lang" : "<lang>",
* "code" : "<code>",
* "options" : {
* "option0" : "<option0>",
* "option1" : "<option1>",
* ...
* }
* }
* }
* }
*
* Note that the 'code' parameter can also handle templates written as complex JSON.
*/
@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(LANG_PARSE_FIELD.getPreferredName(), lang);
builder.field(CODE_PARSE_FIELD.getPreferredName(), code);
builder.field(OPTIONS_PARSE_FIELD.getPreferredName(), options);
builder.endObject();
return builder;
}
/**
* @return The language used for compiling this script.
*/
public String getLang() {
return lang;
}
/**
* @return The code used for compiling this script.
*/
public String getCode() {
return code;
}
/**
* @return The compiler options used for this script.
*/
public Map<String, String> getOptions() {
return options;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StoredScriptSource that = (StoredScriptSource)o;
if (lang != null ? !lang.equals(that.lang) : that.lang != null) return false;
if (code != null ? !code.equals(that.code) : that.code != null) return false;
return options != null ? options.equals(that.options) : that.options == null;
}
@Override
public int hashCode() {
int result = lang != null ? lang.hashCode() : 0;
result = 31 * result + (code != null ? code.hashCode() : 0);
result = 31 * result + (options != null ? options.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "StoredScriptSource{" +
"lang='" + lang + '\'' +
", code='" + code + '\'' +
", options=" + options +
'}';
}
}