StoredScriptSource.java 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. /*
  2. * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
  3. * or more contributor license agreements. Licensed under the Elastic License
  4. * 2.0 and the Server Side Public License, v 1; you may not use this file except
  5. * in compliance with, at your election, the Elastic License 2.0 or the Server
  6. * Side Public License, v 1.
  7. */
  8. package org.elasticsearch.script;
  9. import org.elasticsearch.cluster.AbstractDiffable;
  10. import org.elasticsearch.cluster.ClusterState;
  11. import org.elasticsearch.cluster.Diff;
  12. import org.elasticsearch.common.ParseField;
  13. import org.elasticsearch.common.ParsingException;
  14. import org.elasticsearch.common.Strings;
  15. import org.elasticsearch.common.bytes.BytesReference;
  16. import org.elasticsearch.common.io.stream.StreamInput;
  17. import org.elasticsearch.common.io.stream.StreamOutput;
  18. import org.elasticsearch.common.io.stream.Writeable;
  19. import org.elasticsearch.common.logging.DeprecationCategory;
  20. import org.elasticsearch.common.logging.DeprecationLogger;
  21. import org.elasticsearch.common.xcontent.LoggingDeprecationHandler;
  22. import org.elasticsearch.common.xcontent.NamedXContentRegistry;
  23. import org.elasticsearch.common.xcontent.ObjectParser;
  24. import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
  25. import org.elasticsearch.common.xcontent.ToXContentObject;
  26. import org.elasticsearch.common.xcontent.XContentBuilder;
  27. import org.elasticsearch.common.xcontent.XContentFactory;
  28. import org.elasticsearch.common.xcontent.XContentParser;
  29. import org.elasticsearch.common.xcontent.XContentParser.Token;
  30. import org.elasticsearch.common.xcontent.XContentType;
  31. import java.io.IOException;
  32. import java.io.InputStream;
  33. import java.io.UncheckedIOException;
  34. import java.util.Collections;
  35. import java.util.HashMap;
  36. import java.util.Map;
  37. import java.util.Objects;
  38. /**
  39. * {@link StoredScriptSource} represents user-defined parameters for a script
  40. * saved in the {@link ClusterState}.
  41. */
  42. public class StoredScriptSource extends AbstractDiffable<StoredScriptSource> implements Writeable, ToXContentObject {
  43. /**
  44. * Standard deprecation logger for used to deprecate allowance of empty templates.
  45. */
  46. private static final DeprecationLogger deprecationLogger = DeprecationLogger.getLogger(StoredScriptSource.class);
  47. /**
  48. * Standard {@link ParseField} for outer level of stored script source.
  49. */
  50. public static final ParseField SCRIPT_PARSE_FIELD = new ParseField("script");
  51. /**
  52. * Standard {@link ParseField} for lang on the inner level.
  53. */
  54. public static final ParseField LANG_PARSE_FIELD = new ParseField("lang");
  55. /**
  56. * Standard {@link ParseField} for source on the inner level.
  57. */
  58. public static final ParseField SOURCE_PARSE_FIELD = new ParseField("source", "code");
  59. /**
  60. * Standard {@link ParseField} for options on the inner level.
  61. */
  62. public static final ParseField OPTIONS_PARSE_FIELD = new ParseField("options");
  63. /**
  64. * Helper class used by {@link ObjectParser} to store mutable {@link StoredScriptSource} variables and then
  65. * construct an immutable {@link StoredScriptSource} object based on parsed XContent.
  66. */
  67. private static final class Builder {
  68. private String lang;
  69. private String source;
  70. private Map<String, String> options;
  71. private Builder() {
  72. // This cannot default to an empty map because options are potentially added at multiple points.
  73. this.options = new HashMap<>();
  74. }
  75. private void setLang(String lang) {
  76. this.lang = lang;
  77. }
  78. /**
  79. * Since stored scripts can accept templates rather than just scripts, they must also be able
  80. * to handle template parsing, hence the need for custom parsing source. Templates can
  81. * consist of either an {@link String} or a JSON object. If a JSON object is discovered
  82. * then the content type option must also be saved as a compiler option.
  83. */
  84. private void setSource(XContentParser parser) {
  85. try {
  86. if (parser.currentToken() == Token.START_OBJECT) {
  87. // this is really for search templates, that need to be converted to json format
  88. XContentBuilder builder = XContentFactory.jsonBuilder();
  89. source = Strings.toString(builder.copyCurrentStructure(parser));
  90. options.put(Script.CONTENT_TYPE_OPTION, XContentType.JSON.mediaType());
  91. } else {
  92. source = parser.text();
  93. }
  94. } catch (IOException exception) {
  95. throw new UncheckedIOException(exception);
  96. }
  97. }
  98. /**
  99. * Options may have already been added if a template was specified.
  100. * Appends the user-defined compiler options with the internal compiler options.
  101. */
  102. private void setOptions(Map<String, String> options) {
  103. this.options.putAll(options);
  104. }
  105. /**
  106. * Validates the parameters and creates an {@link StoredScriptSource}.
  107. *
  108. * @param ignoreEmpty Specify as {@code true} to ignoreEmpty the empty source check.
  109. * This allow empty templates to be loaded for backwards compatibility.
  110. * This allow empty templates to be loaded for backwards compatibility.
  111. */
  112. private StoredScriptSource build(boolean ignoreEmpty) {
  113. if (lang == null) {
  114. throw new IllegalArgumentException("must specify lang for stored script");
  115. } else if (lang.isEmpty()) {
  116. throw new IllegalArgumentException("lang cannot be empty");
  117. }
  118. if (source == null) {
  119. if (ignoreEmpty || Script.DEFAULT_TEMPLATE_LANG.equals(lang)) {
  120. if (Script.DEFAULT_TEMPLATE_LANG.equals(lang)) {
  121. deprecationLogger.deprecate(DeprecationCategory.TEMPLATES, "empty_templates",
  122. "empty templates should no longer be used");
  123. } else {
  124. deprecationLogger.deprecate(DeprecationCategory.TEMPLATES, "empty_scripts",
  125. "empty scripts should no longer be used");
  126. }
  127. } else {
  128. throw new IllegalArgumentException("must specify source for stored script");
  129. }
  130. } else if (source.isEmpty()) {
  131. if (ignoreEmpty || Script.DEFAULT_TEMPLATE_LANG.equals(lang)) {
  132. if (Script.DEFAULT_TEMPLATE_LANG.equals(lang)) {
  133. deprecationLogger.deprecate(DeprecationCategory.TEMPLATES, "empty_templates",
  134. "empty templates should no longer be used");
  135. } else {
  136. deprecationLogger.deprecate(DeprecationCategory.TEMPLATES, "empty_scripts",
  137. "empty scripts should no longer be used");
  138. }
  139. } else {
  140. throw new IllegalArgumentException("source cannot be empty");
  141. }
  142. }
  143. if (options.size() > 1 || options.size() == 1 && options.get(Script.CONTENT_TYPE_OPTION) == null) {
  144. throw new IllegalArgumentException("illegal compiler options [" + options + "] specified");
  145. }
  146. return new StoredScriptSource(lang, source, options);
  147. }
  148. }
  149. private static final ObjectParser<Builder, Void> PARSER = new ObjectParser<>("stored script source", true, Builder::new);
  150. static {
  151. // Defines the fields necessary to parse a Script as XContent using an ObjectParser.
  152. PARSER.declareString(Builder::setLang, LANG_PARSE_FIELD);
  153. PARSER.declareField(Builder::setSource, parser -> parser, SOURCE_PARSE_FIELD, ValueType.OBJECT_OR_STRING);
  154. PARSER.declareField(Builder::setOptions, XContentParser::mapStrings, OPTIONS_PARSE_FIELD, ValueType.OBJECT);
  155. }
  156. /**
  157. * This will parse XContent into a {@link StoredScriptSource}. The following formats can be parsed:
  158. *
  159. * The simple script format with no compiler options or user-defined params:
  160. *
  161. * Example:
  162. * {@code
  163. * {"script": "return Math.log(doc.popularity) * 100;"}
  164. * }
  165. *
  166. * The above format requires the lang to be specified using the deprecated stored script namespace
  167. * (as a url parameter during a put request). See {@link ScriptMetadata} for more information about
  168. * the stored script namespaces.
  169. *
  170. * The complex script format using the new stored script namespace
  171. * where lang and source are required but options is optional:
  172. *
  173. * {@code
  174. * {
  175. * "script" : {
  176. * "lang" : "<lang>",
  177. * "source" : "<source>",
  178. * "options" : {
  179. * "option0" : "<option0>",
  180. * "option1" : "<option1>",
  181. * ...
  182. * }
  183. * }
  184. * }
  185. * }
  186. *
  187. * Example:
  188. * {@code
  189. * {
  190. * "script": {
  191. * "lang" : "painless",
  192. * "source" : "return Math.log(doc.popularity) * params.multiplier"
  193. * }
  194. * }
  195. * }
  196. *
  197. * The use of "source" may also be substituted with "code" for backcompat with 5.3 to 5.5 format. For example:
  198. *
  199. * {@code
  200. * {
  201. * "script" : {
  202. * "lang" : "<lang>",
  203. * "code" : "<source>",
  204. * "options" : {
  205. * "option0" : "<option0>",
  206. * "option1" : "<option1>",
  207. * ...
  208. * }
  209. * }
  210. * }
  211. * }
  212. *
  213. * Note that the "source" parameter can also handle template parsing including from
  214. * a complex JSON object.
  215. *
  216. * @param content The content from the request to be parsed as described above.
  217. * @return The parsed {@link StoredScriptSource}.
  218. */
  219. public static StoredScriptSource parse(BytesReference content, XContentType xContentType) {
  220. try (InputStream stream = content.streamInput();
  221. XContentParser parser = xContentType.xContent()
  222. .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, stream)) {
  223. Token token = parser.nextToken();
  224. if (token != Token.START_OBJECT) {
  225. throw new ParsingException(parser.getTokenLocation(), "unexpected token [" + token + "], expected [{]");
  226. }
  227. token = parser.nextToken();
  228. if (token == Token.END_OBJECT) {
  229. deprecationLogger.deprecate(DeprecationCategory.TEMPLATES, "empty_templates", "empty templates should no longer be used");
  230. return new StoredScriptSource(Script.DEFAULT_TEMPLATE_LANG, "", Collections.emptyMap());
  231. }
  232. if (token != Token.FIELD_NAME) {
  233. throw new ParsingException(parser.getTokenLocation(), "unexpected token [" + token + ", expected [" +
  234. SCRIPT_PARSE_FIELD.getPreferredName() + "]");
  235. }
  236. String name = parser.currentName();
  237. if (SCRIPT_PARSE_FIELD.getPreferredName().equals(name)) {
  238. token = parser.nextToken();
  239. if (token == Token.START_OBJECT) {
  240. return PARSER.apply(parser, null).build(false);
  241. } else {
  242. throw new ParsingException(parser.getTokenLocation(), "unexpected token [" + token + "], expected [{, <source>]");
  243. }
  244. } else {
  245. throw new ParsingException(parser.getTokenLocation(), "unexpected field [" + name + "], expected [" +
  246. SCRIPT_PARSE_FIELD.getPreferredName() + "]");
  247. }
  248. } catch (IOException ioe) {
  249. throw new UncheckedIOException(ioe);
  250. }
  251. }
  252. /**
  253. * This will parse XContent into a {@link StoredScriptSource}. The following format is what will be parsed:
  254. *
  255. * {@code
  256. * {
  257. * "script" : {
  258. * "lang" : "<lang>",
  259. * "source" : "<source>",
  260. * "options" : {
  261. * "option0" : "<option0>",
  262. * "option1" : "<option1>",
  263. * ...
  264. * }
  265. * }
  266. * }
  267. * }
  268. *
  269. * Note that the "source" parameter can also handle template parsing including from
  270. * a complex JSON object.
  271. *
  272. * @param ignoreEmpty Specify as {@code true} to ignoreEmpty the empty source check.
  273. * This allows empty templates to be loaded for backwards compatibility.
  274. */
  275. public static StoredScriptSource fromXContent(XContentParser parser, boolean ignoreEmpty) {
  276. return PARSER.apply(parser, null).build(ignoreEmpty);
  277. }
  278. /**
  279. * Required for {@link ScriptMetadata.ScriptMetadataDiff}. Uses
  280. * the {@link StoredScriptSource#StoredScriptSource(StreamInput)}
  281. * constructor.
  282. */
  283. public static Diff<StoredScriptSource> readDiffFrom(StreamInput in) throws IOException {
  284. return readDiffFrom(StoredScriptSource::new, in);
  285. }
  286. private final String lang;
  287. private final String source;
  288. private final Map<String, String> options;
  289. /**
  290. * Standard StoredScriptSource constructor.
  291. * @param lang The language to compile the script with. Must not be {@code null}.
  292. * @param source The source source to compile with. Must not be {@code null}.
  293. * @param options Compiler options to be compiled with. Must not be {@code null},
  294. * use an empty {@link Map} to represent no options.
  295. */
  296. public StoredScriptSource(String lang, String source, Map<String, String> options) {
  297. this.lang = Objects.requireNonNull(lang);
  298. this.source = Objects.requireNonNull(source);
  299. this.options = Collections.unmodifiableMap(Objects.requireNonNull(options));
  300. }
  301. /**
  302. * Reads a {@link StoredScriptSource} from a stream. Version 5.3+ will read
  303. * all of the lang, source, and options parameters. For versions prior to 5.3,
  304. * only the source parameter will be read in as a bytes reference.
  305. */
  306. public StoredScriptSource(StreamInput in) throws IOException {
  307. this.lang = in.readString();
  308. this.source = in.readString();
  309. @SuppressWarnings("unchecked")
  310. Map<String, String> options = (Map<String, String>)(Map)in.readMap();
  311. this.options = options;
  312. }
  313. /**
  314. * Writes a {@link StoredScriptSource} to a stream. Will write
  315. * all of the lang, source, and options parameters.
  316. */
  317. @Override
  318. public void writeTo(StreamOutput out) throws IOException {
  319. out.writeString(lang);
  320. out.writeString(source);
  321. @SuppressWarnings("unchecked")
  322. Map<String, Object> options = (Map<String, Object>)(Map)this.options;
  323. out.writeMap(options);
  324. }
  325. /**
  326. * This will write XContent from a {@link StoredScriptSource}. The following format will be written:
  327. *
  328. * {@code
  329. * {
  330. * "script" : {
  331. * "lang" : "<lang>",
  332. * "source" : "<source>",
  333. * "options" : {
  334. * "option0" : "<option0>",
  335. * "option1" : "<option1>",
  336. * ...
  337. * }
  338. * }
  339. * }
  340. * }
  341. *
  342. * Note that the 'source' parameter can also handle templates written as complex JSON.
  343. */
  344. @Override
  345. public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
  346. builder.startObject();
  347. builder.field(LANG_PARSE_FIELD.getPreferredName(), lang);
  348. builder.field(SOURCE_PARSE_FIELD.getPreferredName(), source);
  349. if (options.isEmpty() == false) {
  350. builder.field(OPTIONS_PARSE_FIELD.getPreferredName(), options);
  351. }
  352. builder.endObject();
  353. return builder;
  354. }
  355. /**
  356. * @return The language used for compiling this script.
  357. */
  358. public String getLang() {
  359. return lang;
  360. }
  361. /**
  362. * @return The source used for compiling this script.
  363. */
  364. public String getSource() {
  365. return source;
  366. }
  367. /**
  368. * @return The compiler options used for this script.
  369. */
  370. public Map<String, String> getOptions() {
  371. return options;
  372. }
  373. @Override
  374. public boolean equals(Object o) {
  375. if (this == o) return true;
  376. if (o == null || getClass() != o.getClass()) return false;
  377. StoredScriptSource that = (StoredScriptSource)o;
  378. return Objects.equals(lang, that.lang)
  379. && Objects.equals(source, that.source)
  380. && Objects.equals(options, that.options);
  381. }
  382. @Override
  383. public int hashCode() {
  384. int result = lang != null ? lang.hashCode() : 0;
  385. result = 31 * result + (source != null ? source.hashCode() : 0);
  386. result = 31 * result + (options != null ? options.hashCode() : 0);
  387. return result;
  388. }
  389. @Override
  390. public String toString() {
  391. return "StoredScriptSource{" +
  392. "lang='" + lang + '\'' +
  393. ", source='" + source + '\'' +
  394. ", options=" + options +
  395. '}';
  396. }
  397. }