Browse Source

Scripting: allow plugins to define custom operations that they use scripts for

Plugins can now define multiple operations/contexts that they use scripts for. Fine-grained settings can then be used to enable/disable scripts based on each single registered context.

Also added a new generic category called `plugin`, which will be used as a default when the context is not specified. This allows us to restore backwards compatibility for plugins on `ScriptService` by restoring the old methods that don't require the script context and making them internally use the `plugin` context, as they can only be called from plugins.

Closes #10347
Closes #10419
javanna 10 years ago
parent
commit
7bd7ea8f13
24 changed files with 685 additions and 401 deletions
  1. 10 2
      docs/reference/modules/scripting.asciidoc
  2. 2 2
      src/main/java/org/elasticsearch/action/update/UpdateHelper.java
  3. 3 3
      src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java
  4. 1 1
      src/main/java/org/elasticsearch/index/query/ScriptFilterParser.java
  5. 2 2
      src/main/java/org/elasticsearch/index/query/TemplateQueryParser.java
  6. 1 1
      src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionParser.java
  7. 87 12
      src/main/java/org/elasticsearch/script/ScriptContext.java
  8. 90 0
      src/main/java/org/elasticsearch/script/ScriptContextRegistry.java
  9. 22 23
      src/main/java/org/elasticsearch/script/ScriptModes.java
  10. 11 0
      src/main/java/org/elasticsearch/script/ScriptModule.java
  11. 45 4
      src/main/java/org/elasticsearch/script/ScriptService.java
  12. 4 3
      src/main/java/org/elasticsearch/search/SearchService.java
  13. 3 6
      src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java
  14. 2 2
      src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java
  15. 4 5
      src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java
  16. 2 5
      src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceParser.java
  17. 2 5
      src/main/java/org/elasticsearch/search/fetch/script/ScriptFieldsParseElement.java
  18. 1 1
      src/main/java/org/elasticsearch/search/sort/ScriptSortParser.java
  19. 2 2
      src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestParser.java
  20. 133 0
      src/test/java/org/elasticsearch/script/CustomScriptContextTests.java
  21. 6 4
      src/test/java/org/elasticsearch/script/NativeScriptTests.java
  22. 83 0
      src/test/java/org/elasticsearch/script/ScriptContextRegistryTests.java
  23. 104 294
      src/test/java/org/elasticsearch/script/ScriptModesTests.java
  24. 65 24
      src/test/java/org/elasticsearch/script/ScriptServiceTests.java

+ 10 - 2
docs/reference/modules/scripting.asciidoc

@@ -300,12 +300,17 @@ supported operations are:
 | `mapping` |Mappings (script transform feature)
 | `search`  |Search api, Percolator api and Suggester api (e.g filters, script_fields)
 | `update`  |Update api
+| `plugin`  |Any plugin that makes use of scripts under the generic `plugin` category
 |=======================================================================
 
+Plugins can also define custom operations that they use scripts for instead
+of using the generic `plugin` category. Those operations can be referred to
+in the following form: `${pluginName}_${operation}`.
+
 The following example disables scripting for `update` and `mapping` operations,
 regardless of the script source, for any engine. Scripts can still be
-executed from sandboxed languages as part of `aggregations` and `search`
-operations though, as the above defaults still get applied.
+executed from sandboxed languages as part of `aggregations`, `search`
+and plugins execution though, as the above defaults still get applied.
 
 [source,yaml]
 -----------------------------------
@@ -325,14 +330,17 @@ script.engine.groovy.file.aggs: on
 script.engine.groovy.file.mapping: on
 script.engine.groovy.file.search: on
 script.engine.groovy.file.update: on
+script.engine.groovy.file.plugin: on
 script.engine.groovy.indexed.aggs: on
 script.engine.groovy.indexed.mapping: off
 script.engine.groovy.indexed.search: on
 script.engine.groovy.indexed.update: off
+script.engine.groovy.indexed.plugin: off
 script.engine.groovy.inline.aggs: on
 script.engine.groovy.inline.mapping: off
 script.engine.groovy.inline.search: off
 script.engine.groovy.inline.update: off
+script.engine.groovy.inline.plugin: off
 
 -----------------------------------
 

+ 2 - 2
src/main/java/org/elasticsearch/action/update/UpdateHelper.java

@@ -94,7 +94,7 @@ public class UpdateHelper extends AbstractComponent {
                 ctx.put("op", "create");
                 ctx.put("_source", upsertDoc);
                 try {
-                    ExecutableScript script = scriptService.executable(request.scriptLang, request.script, request.scriptType, ScriptContext.UPDATE, request.scriptParams);
+                    ExecutableScript script = scriptService.executable(request.scriptLang, request.script, request.scriptType, ScriptContext.Standard.UPDATE, request.scriptParams);
                     script.setNextVar("ctx", ctx);
                     script.run();
                     // we need to unwrap the ctx...
@@ -193,7 +193,7 @@ public class UpdateHelper extends AbstractComponent {
             ctx.put("_source", sourceAndContent.v2());
 
             try {
-                ExecutableScript script = scriptService.executable(request.scriptLang, request.script, request.scriptType, ScriptContext.UPDATE, request.scriptParams);
+                ExecutableScript script = scriptService.executable(request.scriptLang, request.script, request.scriptType, ScriptContext.Standard.UPDATE, request.scriptParams);
                 script.setNextVar("ctx", ctx);
                 script.run();
                 // we need to unwrap the ctx...

+ 3 - 3
src/main/java/org/elasticsearch/index/mapper/DocumentMapper.java

@@ -64,8 +64,9 @@ import org.elasticsearch.index.mapper.internal.VersionFieldMapper;
 import org.elasticsearch.index.mapper.object.ObjectMapper;
 import org.elasticsearch.index.mapper.object.RootObjectMapper;
 import org.elasticsearch.script.ExecutableScript;
-import org.elasticsearch.script.ScriptService;
 import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.script.ScriptService.ScriptType;
 
 import java.io.IOException;
 import java.util.ArrayList;
@@ -79,7 +80,6 @@ import java.util.Set;
 import java.util.concurrent.CopyOnWriteArrayList;
 
 import static com.google.common.collect.Lists.newArrayList;
-import static org.elasticsearch.script.ScriptService.*;
 
 /**
  *
@@ -760,7 +760,7 @@ public class DocumentMapper implements ToXContent {
         public Map<String, Object> transformSourceAsMap(Map<String, Object> sourceAsMap) {
             try {
                 // We use the ctx variable and the _source name to be consistent with the update api.
-                ExecutableScript executable = scriptService.executable(language, script, scriptType, ScriptContext.MAPPING, parameters);
+                ExecutableScript executable = scriptService.executable(language, script, scriptType, ScriptContext.Standard.MAPPING, parameters);
                 Map<String, Object> ctx = new HashMap<>(1);
                 ctx.put("_source", sourceAsMap);
                 executable.setNextVar("ctx", ctx);

+ 1 - 1
src/main/java/org/elasticsearch/index/query/ScriptFilterParser.java

@@ -134,7 +134,7 @@ public class ScriptFilterParser implements FilterParser {
         public ScriptFilter(String scriptLang, String script, ScriptService.ScriptType scriptType, Map<String, Object> params, ScriptService scriptService, SearchLookup searchLookup) {
             this.script = script;
             this.params = params;
-            this.searchScript = scriptService.search(searchLookup, scriptLang, script, scriptType, ScriptContext.SEARCH, newHashMap(params));
+            this.searchScript = scriptService.search(searchLookup, scriptLang, script, scriptType, ScriptContext.Standard.SEARCH, newHashMap(params));
         }
 
         @Override

+ 2 - 2
src/main/java/org/elasticsearch/index/query/TemplateQueryParser.java

@@ -26,8 +26,8 @@ import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentFactory;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.script.ExecutableScript;
-import org.elasticsearch.script.ScriptService;
 import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.script.ScriptService;
 import org.elasticsearch.script.mustache.MustacheScriptEngineService;
 
 import java.io.IOException;
@@ -77,7 +77,7 @@ public class TemplateQueryParser implements QueryParser {
     public Query parse(QueryParseContext parseContext) throws IOException {
         XContentParser parser = parseContext.parser();
         TemplateContext templateContext = parse(parser, PARAMS, parametersToTypes);
-        ExecutableScript executable = this.scriptService.executable(MustacheScriptEngineService.NAME, templateContext.template(), templateContext.scriptType(), ScriptContext.SEARCH, templateContext.params());
+        ExecutableScript executable = this.scriptService.executable(MustacheScriptEngineService.NAME, templateContext.template(), templateContext.scriptType(), ScriptContext.Standard.SEARCH, templateContext.params());
 
         BytesReference querySource = (BytesReference) executable.run();
 

+ 1 - 1
src/main/java/org/elasticsearch/index/query/functionscore/script/ScriptScoreFunctionParser.java

@@ -87,7 +87,7 @@ public class ScriptScoreFunctionParser implements ScoreFunctionParser {
 
         SearchScript searchScript;
         try {
-            searchScript = parseContext.scriptService().search(parseContext.lookup(), scriptParameterParser.lang(), script, scriptType, ScriptContext.SEARCH, vars);
+            searchScript = parseContext.scriptService().search(parseContext.lookup(), scriptParameterParser.lang(), script, scriptType, ScriptContext.Standard.SEARCH, vars);
             return new ScriptScoreFunction(script, vars, searchScript);
         } catch (Exception e) {
             throw new QueryParsingException(parseContext.index(), NAMES[0] + " the script could not be loaded", e);

+ 87 - 12
src/main/java/org/elasticsearch/script/ScriptContext.java

@@ -19,20 +19,95 @@
 
 package org.elasticsearch.script;
 
-import java.util.Locale;
+import org.elasticsearch.ElasticsearchIllegalArgumentException;
+import org.elasticsearch.common.Strings;
 
 /**
- * Operation/api that uses a script as part of its execution.
- * Note that the suggest api is considered part of search for simplicity, as well as the percolate api.
+ * Context of an operation that uses scripts as part of its execution.
  */
-public enum ScriptContext {
-    MAPPING,
-    UPDATE,
-    SEARCH,
-    AGGS;
-
-    @Override
-    public String toString() {
-        return name().toLowerCase(Locale.ROOT);
+public interface ScriptContext {
+
+    /**
+     * @return the name of the operation
+     */
+    String getKey();
+
+    /**
+     * Standard operations that make use of scripts as part of their execution.
+     * Note that the suggest api is considered part of search for simplicity, as well as the percolate api.
+     */
+    enum Standard implements ScriptContext {
+
+        AGGS("aggs"), MAPPING("mapping"), SEARCH("search"), UPDATE("update"),
+        /**
+         * Generic custom operation exposed via plugin
+         *
+         * @deprecated create a new {@link org.elasticsearch.script.ScriptContext.Plugin} instance instead
+         */
+        @Deprecated
+        GENERIC_PLUGIN("plugin");
+
+        private final String key;
+
+        Standard(String key) {
+            this.key = key;
+        }
+
+        @Override
+        public String getKey() {
+            return key;
+        }
+
+        @Override
+        public String toString() {
+            return getKey();
+        }
+    }
+
+    /**
+     * Custom operation exposed via plugin, which makes use of scripts as part of its execution
+     */
+    final class Plugin implements ScriptContext {
+
+        private final String pluginName;
+        private final String operation;
+        private final String key;
+
+        /**
+         * Creates a new custom scripts based operation exposed via plugin.
+         * The name of the plugin combined with the operation name can be used to enable/disable scripts via fine-grained settings.
+         *
+         * @param pluginName the name of the plugin
+         * @param operation the name of the operation
+         */
+        public Plugin(String pluginName, String operation) {
+            if (Strings.hasLength(pluginName) == false) {
+                throw new ElasticsearchIllegalArgumentException("plugin name cannot be empty when registering a custom script context");
+            }
+            if (Strings.hasLength(operation) == false) {
+                throw new ElasticsearchIllegalArgumentException("operation name cannot be empty when registering a custom script context");
+            }
+            this.pluginName = pluginName;
+            this.operation = operation;
+            this.key = pluginName + "_" + operation;
+        }
+
+        public final String getPluginName() {
+            return pluginName;
+        }
+
+        public final String getOperation() {
+            return operation;
+        }
+
+        @Override
+        public final String getKey() {
+            return key;
+        }
+
+        @Override
+        public final String toString() {
+            return getKey();
+        }
     }
 }

+ 90 - 0
src/main/java/org/elasticsearch/script/ScriptContextRegistry.java

@@ -0,0 +1,90 @@
+/*
+ * 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 com.google.common.collect.ImmutableCollection;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import org.elasticsearch.ElasticsearchIllegalArgumentException;
+
+import java.util.Map;
+
+/**
+ * Registry for operations that use scripts as part of their execution. Can be standard operations of custom defined ones (via plugin).
+ * Allows plugins to register custom operations that they use scripts for, via {@link ScriptModule#registerScriptContext(org.elasticsearch.script.ScriptContext.Plugin)}.
+ * Scripts can be enabled/disabled via fine-grained settings for each single registered operation.
+ */
+public final class ScriptContextRegistry {
+    static final ImmutableSet<String> RESERVED_SCRIPT_CONTEXTS = reservedScriptContexts();
+
+    private final ImmutableMap<String, ScriptContext> scriptContexts;
+
+    ScriptContextRegistry(Iterable<ScriptContext.Plugin> customScriptContexts) {
+        Map<String, ScriptContext> scriptContexts = Maps.newHashMap();
+        for (ScriptContext.Standard scriptContext : ScriptContext.Standard.values()) {
+            scriptContexts.put(scriptContext.getKey(), scriptContext);
+        }
+        for (ScriptContext.Plugin customScriptContext : customScriptContexts) {
+            validateScriptContext(customScriptContext);
+            ScriptContext previousContext = scriptContexts.put(customScriptContext.getKey(), customScriptContext);
+            if (previousContext != null) {
+                throw new ElasticsearchIllegalArgumentException("script context [" + customScriptContext.getKey() + "] cannot be registered twice");
+            }
+        }
+        this.scriptContexts = ImmutableMap.copyOf(scriptContexts);
+    }
+
+    /**
+     * @return a list that contains all the supported {@link ScriptContext}s, both standard ones and registered via plugins
+     */
+    ImmutableCollection<ScriptContext> scriptContexts() {
+        return scriptContexts.values();
+    }
+
+    /**
+     * @return <tt>true</tt> if the provided {@link ScriptContext} is supported, <tt>false</tt> otherwise
+     */
+    boolean isSupportedContext(ScriptContext scriptContext) {
+        return scriptContexts.containsKey(scriptContext.getKey());
+    }
+
+    //script contexts can be used in fine-grained settings, we need to be careful with what we allow here
+    private void validateScriptContext(ScriptContext.Plugin scriptContext) {
+        if (RESERVED_SCRIPT_CONTEXTS.contains(scriptContext.getPluginName())) {
+            throw new ElasticsearchIllegalArgumentException("[" + scriptContext.getPluginName() + "] is a reserved name, it cannot be registered as a custom script context");
+        }
+        if (RESERVED_SCRIPT_CONTEXTS.contains(scriptContext.getOperation())) {
+            throw new ElasticsearchIllegalArgumentException("[" + scriptContext.getOperation() + "] is a reserved name, it cannot be registered as a custom script context");
+        }
+    }
+
+    private static ImmutableSet<String> reservedScriptContexts() {
+        ImmutableSet.Builder<String> builder = ImmutableSet.builder();
+        for (ScriptService.ScriptType scriptType : ScriptService.ScriptType.values()) {
+            builder.add(scriptType.toString());
+        }
+        for (ScriptContext.Standard scriptContext : ScriptContext.Standard.values()) {
+            builder.add(scriptContext.getKey());
+        }
+        builder.add("script").add("engine");
+        return builder.build();
+    }
+}

+ 22 - 23
src/main/java/org/elasticsearch/script/ScriptModes.java

@@ -42,45 +42,45 @@ public class ScriptModes {
 
     final ImmutableMap<String, ScriptMode> scriptModes;
 
-    ScriptModes(Map<String, ScriptEngineService> scriptEngines, Settings settings) {
+    ScriptModes(Map<String, ScriptEngineService> scriptEngines, ScriptContextRegistry scriptContextRegistry, Settings settings) {
         //filter out the native engine as we don't want to apply fine grained settings to it.
         //native scripts are always on as they are static by definition.
         Map<String, ScriptEngineService> filteredEngines = Maps.newHashMap(scriptEngines);
         filteredEngines.remove(NativeScriptEngineService.NAME);
-        this.scriptModes = buildScriptModeSettingsMap(settings, filteredEngines);
+        this.scriptModes = buildScriptModeSettingsMap(settings, filteredEngines, scriptContextRegistry);
     }
 
-    private ImmutableMap<String, ScriptMode> buildScriptModeSettingsMap(Settings settings, Map<String, ScriptEngineService> scriptEngines) {
+    private static ImmutableMap<String, ScriptMode> buildScriptModeSettingsMap(Settings settings, Map<String, ScriptEngineService> scriptEngines, ScriptContextRegistry scriptContextRegistry) {
         HashMap<String, ScriptMode> scriptModesMap = Maps.newHashMap();
 
         //file scripts are enabled by default, for any language
-        addGlobalScriptTypeModes(scriptEngines.keySet(), ScriptType.FILE, ScriptMode.ON, scriptModesMap);
+        addGlobalScriptTypeModes(scriptEngines.keySet(), scriptContextRegistry, ScriptType.FILE, ScriptMode.ON, scriptModesMap);
         //indexed scripts are enabled by default only for sandboxed languages
-        addGlobalScriptTypeModes(scriptEngines.keySet(), ScriptType.INDEXED, ScriptMode.SANDBOX, scriptModesMap);
+        addGlobalScriptTypeModes(scriptEngines.keySet(), scriptContextRegistry, ScriptType.INDEXED, ScriptMode.SANDBOX, scriptModesMap);
         //dynamic scripts are enabled by default only for sandboxed languages
-        addGlobalScriptTypeModes(scriptEngines.keySet(), ScriptType.INLINE, ScriptMode.SANDBOX, scriptModesMap);
+        addGlobalScriptTypeModes(scriptEngines.keySet(), scriptContextRegistry, ScriptType.INLINE, ScriptMode.SANDBOX, scriptModesMap);
 
-        processSourceBasedGlobalSettings(settings, scriptEngines, scriptModesMap);
-        processOperationBasedGlobalSettings(settings, scriptEngines, scriptModesMap);
-        processEngineSpecificSettings(settings, scriptEngines, scriptModesMap);
+        processSourceBasedGlobalSettings(settings, scriptEngines, scriptContextRegistry, scriptModesMap);
+        processOperationBasedGlobalSettings(settings, scriptEngines, scriptContextRegistry, scriptModesMap);
+        processEngineSpecificSettings(settings, scriptEngines, scriptContextRegistry, scriptModesMap);
         return ImmutableMap.copyOf(scriptModesMap);
     }
 
-    private static void processSourceBasedGlobalSettings(Settings settings, Map<String, ScriptEngineService> scriptEngines, Map<String, ScriptMode> scriptModes) {
+    private static void processSourceBasedGlobalSettings(Settings settings, Map<String, ScriptEngineService> scriptEngines, ScriptContextRegistry scriptContextRegistry, Map<String, ScriptMode> scriptModes) {
         //read custom source based settings for all operations (e.g. script.indexed: on)
         for (ScriptType scriptType : ScriptType.values()) {
             String scriptTypeSetting = settings.get(SCRIPT_SETTINGS_PREFIX + scriptType);
             if (Strings.hasLength(scriptTypeSetting)) {
                 ScriptMode scriptTypeMode = ScriptMode.parse(scriptTypeSetting);
-                addGlobalScriptTypeModes(scriptEngines.keySet(), scriptType, scriptTypeMode, scriptModes);
+                addGlobalScriptTypeModes(scriptEngines.keySet(), scriptContextRegistry, scriptType, scriptTypeMode, scriptModes);
             }
         }
     }
 
-    private static void processOperationBasedGlobalSettings(Settings settings, Map<String, ScriptEngineService> scriptEngines, Map<String, ScriptMode> scriptModes) {
+    private static void processOperationBasedGlobalSettings(Settings settings, Map<String, ScriptEngineService> scriptEngines, ScriptContextRegistry scriptContextRegistry, Map<String, ScriptMode> scriptModes) {
         //read custom op based settings for all sources (e.g. script.aggs: off)
         //op based settings take precedence over source based settings, hence they get expanded later
-        for (ScriptContext scriptContext : ScriptContext.values()) {
+        for (ScriptContext scriptContext : scriptContextRegistry.scriptContexts()) {
             ScriptMode scriptMode = getScriptContextMode(settings, SCRIPT_SETTINGS_PREFIX, scriptContext);
             if (scriptMode != null) {
                 addGlobalScriptContextModes(scriptEngines.keySet(), scriptContext, scriptMode, scriptModes);
@@ -88,16 +88,15 @@ public class ScriptModes {
         }
     }
 
-    private static void processEngineSpecificSettings(Settings settings, Map<String, ScriptEngineService> scriptEngines, Map<String, ScriptMode> scriptModes) {
+    private static void processEngineSpecificSettings(Settings settings, Map<String, ScriptEngineService> scriptEngines, ScriptContextRegistry scriptContextRegistry, Map<String, ScriptMode> scriptModes) {
         Map<String, Settings> langGroupedSettings = settings.getGroups(ENGINE_SETTINGS_PREFIX, true);
         for (Map.Entry<String, Settings> langSettings : langGroupedSettings.entrySet()) {
             //read engine specific settings that refer to a non existing script lang will be ignored
             ScriptEngineService scriptEngineService = scriptEngines.get(langSettings.getKey());
             if (scriptEngineService != null) {
-                String enginePrefix = ScriptModes.ENGINE_SETTINGS_PREFIX + "." + langSettings.getKey() + ".";
                 for (ScriptType scriptType : ScriptType.values()) {
                     String scriptTypePrefix = scriptType + ".";
-                    for (ScriptContext scriptContext : ScriptContext.values()) {
+                    for (ScriptContext scriptContext : scriptContextRegistry.scriptContexts()) {
                         ScriptMode scriptMode = getScriptContextMode(langSettings.getValue(), scriptTypePrefix, scriptContext);
                         if (scriptMode != null) {
                             addScriptMode(scriptEngineService, scriptType, scriptContext, scriptMode, scriptModes);
@@ -109,16 +108,16 @@ public class ScriptModes {
     }
 
     private static ScriptMode getScriptContextMode(Settings settings, String prefix, ScriptContext scriptContext) {
-        String settingValue = settings.get(prefix + scriptContext);
+        String settingValue = settings.get(prefix + scriptContext.getKey());
         if (Strings.hasLength(settingValue)) {
             return ScriptMode.parse(settingValue);
         }
         return null;
     }
 
-    private static void addGlobalScriptTypeModes(Set<String> langs, ScriptType scriptType, ScriptMode scriptMode, Map<String, ScriptMode> scriptModes) {
+    private static void addGlobalScriptTypeModes(Set<String> langs, ScriptContextRegistry scriptContextRegistry, ScriptType scriptType, ScriptMode scriptMode, Map<String, ScriptMode> scriptModes) {
         for (String lang : langs) {
-            for (ScriptContext scriptContext : ScriptContext.values()) {
+            for (ScriptContext scriptContext : scriptContextRegistry.scriptContexts()) {
                 addScriptMode(lang, scriptType, scriptContext, scriptMode, scriptModes);
             }
         }
@@ -141,7 +140,7 @@ public class ScriptModes {
     }
 
     private static void addScriptMode(String lang, ScriptType scriptType, ScriptContext scriptContext, ScriptMode scriptMode, Map<String, ScriptMode> scriptModes) {
-        scriptModes.put(ENGINE_SETTINGS_PREFIX + "." + lang + "." + scriptType + "." + scriptContext, scriptMode);
+        scriptModes.put(ENGINE_SETTINGS_PREFIX + "." + lang + "." + scriptType + "." + scriptContext.getKey(), scriptMode);
     }
 
     /**
@@ -150,7 +149,7 @@ public class ScriptModes {
      *
      * @param lang the language that the script is written in
      * @param scriptType the type of the script
-     * @param scriptContext the api that requires the execution of the script
+     * @param scriptContext the operation that requires the execution of the script
      * @return whether scripts are on, off, or enabled only for sandboxed languages
      */
     public ScriptMode getScriptMode(String lang, ScriptType scriptType, ScriptContext scriptContext) {
@@ -158,9 +157,9 @@ public class ScriptModes {
         if (NativeScriptEngineService.NAME.equals(lang)) {
             return ScriptMode.ON;
         }
-        ScriptMode scriptMode = scriptModes.get(ENGINE_SETTINGS_PREFIX + "." + lang + "." + scriptType + "." + scriptContext);
+        ScriptMode scriptMode = scriptModes.get(ENGINE_SETTINGS_PREFIX + "." + lang + "." + scriptType + "." + scriptContext.getKey());
         if (scriptMode == null) {
-            throw new ElasticsearchIllegalArgumentException("script mode not found for lang [" + lang + "], script_type [" + scriptType + "], operation [" + scriptContext + "]");
+            throw new ElasticsearchIllegalArgumentException("script mode not found for lang [" + lang + "], script_type [" + scriptType + "], operation [" + scriptContext.getKey() + "]");
         }
         return scriptMode;
     }

+ 11 - 0
src/main/java/org/elasticsearch/script/ScriptModule.java

@@ -46,6 +46,8 @@ public class ScriptModule extends AbstractModule {
 
     private final Map<String, Class<? extends NativeScriptFactory>> scripts = Maps.newHashMap();
 
+    private final List<ScriptContext.Plugin> customScriptContexts = Lists.newArrayList();
+
     public ScriptModule(Settings settings) {
         this.settings = settings;
     }
@@ -58,6 +60,14 @@ public class ScriptModule extends AbstractModule {
         scripts.put(name, script);
     }
 
+    /**
+     * Registers a custom script context that can be used by plugins to categorize the different operations that they use scripts for.
+     * Fine-grained settings allow to enable/disable scripts per context.
+     */
+    public void registerScriptContext(ScriptContext.Plugin scriptContext) {
+        customScriptContexts.add(scriptContext);
+    }
+
     @Override
     protected void configure() {
         MapBinder<String, NativeScriptFactory> scriptsBinder
@@ -105,6 +115,7 @@ public class ScriptModule extends AbstractModule {
             multibinder.addBinding().to(scriptEngine).asEagerSingleton();
         }
 
+        bind(ScriptContextRegistry.class).toInstance(new ScriptContextRegistry(customScriptContexts));
         bind(ScriptService.class).asEagerSingleton();
     }
 }

+ 45 - 4
src/main/java/org/elasticsearch/script/ScriptService.java

@@ -103,6 +103,7 @@ public class ScriptService extends AbstractComponent implements Closeable {
     private final FileWatcher fileWatcher;
 
     private final ScriptModes scriptModes;
+    private final ScriptContextRegistry scriptContextRegistry;
 
     private Client client = null;
 
@@ -113,7 +114,7 @@ public class ScriptService extends AbstractComponent implements Closeable {
 
     @Inject
     public ScriptService(Settings settings, Environment env, Set<ScriptEngineService> scriptEngines,
-                         ResourceWatcherService resourceWatcherService, NodeSettingsService nodeSettingsService) throws IOException {
+                         ResourceWatcherService resourceWatcherService, NodeSettingsService nodeSettingsService, ScriptContextRegistry scriptContextRegistry) throws IOException {
         super(settings);
 
         if (Strings.hasLength(settings.get(DISABLE_DYNAMIC_SCRIPTING_SETTING))) {
@@ -122,6 +123,7 @@ public class ScriptService extends AbstractComponent implements Closeable {
         }
 
         this.scriptEngines = scriptEngines;
+        this.scriptContextRegistry = scriptContextRegistry;
         int cacheMaxSize = settings.getAsInt(SCRIPT_CACHE_SIZE_SETTING, 100);
         TimeValue cacheExpire = settings.getAsTime(SCRIPT_CACHE_EXPIRE_SETTING, null);
         logger.debug("using script cache with max_size [{}], expire [{}]", cacheMaxSize, cacheExpire);
@@ -150,7 +152,7 @@ public class ScriptService extends AbstractComponent implements Closeable {
         this.scriptEnginesByLang = enginesByLangBuilder.build();
         this.scriptEnginesByExt = enginesByExtBuilder.build();
 
-        this.scriptModes = new ScriptModes(this.scriptEnginesByLang, settings);
+        this.scriptModes = new ScriptModes(this.scriptEnginesByLang, scriptContextRegistry, settings);
 
         // add file watcher for static scripts
         scriptsDirectory = env.configFile().resolve("scripts");
@@ -212,6 +214,18 @@ public class ScriptService extends AbstractComponent implements Closeable {
         return scriptEngineService;
     }
 
+    /**
+     * Checks if a script can be executed and compiles it if needed, or returns the previously compiled and cached script.
+     * Doesn't require to specify a script context in order to maintain backwards compatibility, internally uses
+     * the {@link org.elasticsearch.script.ScriptContext.Standard#GENERIC_PLUGIN} default context, assuming that it can only be called from plugins.
+     *
+     * @deprecated use the method variant that accepts the {@link ScriptContext} argument too: {@link #compile(String, String, ScriptType, ScriptContext)}
+     */
+    @Deprecated
+    public CompiledScript compile(String lang,  String script, ScriptType scriptType) {
+        return compile(lang, script, scriptType, ScriptContext.Standard.GENERIC_PLUGIN);
+    }
+
     /**
      * Checks if a script can be executed and compiles it if needed, or returns the previously compiled and cached script.
      */
@@ -226,7 +240,7 @@ public class ScriptService extends AbstractComponent implements Closeable {
 
         ScriptEngineService scriptEngineService = getScriptEngineServiceForLang(lang);
         if (canExecuteScript(lang, scriptEngineService, scriptType, scriptContext) == false) {
-            throw new ScriptException("scripts of type [" + scriptType + "], operation [" + scriptContext + "] and lang [" + lang + "] are disabled");
+            throw new ScriptException("scripts of type [" + scriptType + "], operation [" + scriptContext.getKey() + "] and lang [" + lang + "] are disabled");
         }
         return compileInternal(lang, script, scriptType);
     }
@@ -380,6 +394,18 @@ public class ScriptService extends AbstractComponent implements Closeable {
         }
     }
 
+    /**
+     * Compiles (or retrieves from cache) and executes the provided script.
+     * Doesn't require to specify a script context in order to maintain backwards compatibility, internally uses
+     * the {@link org.elasticsearch.script.ScriptContext.Standard#GENERIC_PLUGIN} default context, assuming that it can only be called from plugins.
+     *
+     * @deprecated use the method variant that accepts the {@link ScriptContext} argument too: {@link #executable(String, String, ScriptType, ScriptContext, Map)}
+     */
+    @Deprecated
+    public ExecutableScript executable(String lang, String script, ScriptType scriptType, Map<String, Object> vars) {
+        return executable(lang, script, scriptType, ScriptContext.Standard.GENERIC_PLUGIN, vars);
+    }
+
     /**
      * Compiles (or retrieves from cache) and executes the provided script
      */
@@ -394,6 +420,18 @@ public class ScriptService extends AbstractComponent implements Closeable {
         return getScriptEngineServiceForLang(compiledScript.lang()).executable(compiledScript.compiled(), vars);
     }
 
+    /**
+     * Compiles (or retrieves from cache) and executes the provided search script
+     * Doesn't require to specify a script context in order to maintain backwards compatibility, internally uses
+     * the {@link org.elasticsearch.script.ScriptContext.Standard#GENERIC_PLUGIN} default context, assuming that it can only be called from plugins.
+     *
+     * @deprecated use the method variant that accepts the {@link ScriptContext} argument too: {@link #search(SearchLookup, String, String, ScriptType, ScriptContext, Map)}
+     */
+    @Deprecated
+    public SearchScript search(SearchLookup lookup, String lang, String script, ScriptType scriptType, @Nullable Map<String, Object> vars) {
+        return search(lookup, lang, script, scriptType, ScriptContext.Standard.GENERIC_PLUGIN, vars);
+    }
+
     /**
      * Compiles (or retrieves from cache) and executes the provided search script
      */
@@ -403,7 +441,7 @@ public class ScriptService extends AbstractComponent implements Closeable {
     }
 
     private boolean isAnyScriptContextEnabled(String lang, ScriptEngineService scriptEngineService, ScriptType scriptType) {
-        for (ScriptContext scriptContext : ScriptContext.values()) {
+        for (ScriptContext scriptContext : scriptContextRegistry.scriptContexts()) {
             if (canExecuteScript(lang, scriptEngineService, scriptType, scriptContext)) {
                 return true;
             }
@@ -413,6 +451,9 @@ public class ScriptService extends AbstractComponent implements Closeable {
 
     private boolean canExecuteScript(String lang, ScriptEngineService scriptEngineService, ScriptType scriptType, ScriptContext scriptContext) {
         assert lang != null;
+        if (scriptContextRegistry.isSupportedContext(scriptContext) == false) {
+            throw new ElasticsearchIllegalArgumentException("script context [" + scriptContext.getKey() + "] not supported");
+        }
         ScriptMode mode = scriptModes.getScriptMode(lang, scriptType, scriptContext);
         switch (mode) {
             case ON:

+ 4 - 3
src/main/java/org/elasticsearch/search/SearchService.java

@@ -71,8 +71,9 @@ import org.elasticsearch.indices.IndicesWarmer.TerminationHandle;
 import org.elasticsearch.indices.IndicesWarmer.WarmerContext;
 import org.elasticsearch.indices.cache.query.IndicesQueryCache;
 import org.elasticsearch.script.ExecutableScript;
-import org.elasticsearch.script.ScriptService;
 import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.script.ScriptContextRegistry;
 import org.elasticsearch.script.mustache.MustacheScriptEngineService;
 import org.elasticsearch.search.dfs.CachedDfSource;
 import org.elasticsearch.search.dfs.DfsPhase;
@@ -652,7 +653,7 @@ public class SearchService extends AbstractLifecycleComponent<SearchService> {
 
         final ExecutableScript executable;
         if (hasLength(request.templateName())) {
-            executable = this.scriptService.executable(MustacheScriptEngineService.NAME, request.templateName(), request.templateType(), ScriptContext.SEARCH, request.templateParams());
+            executable = this.scriptService.executable(MustacheScriptEngineService.NAME, request.templateName(), request.templateType(), ScriptContext.Standard.SEARCH, request.templateParams());
         } else {
             if (!hasLength(request.templateSource())) {
                 return;
@@ -692,7 +693,7 @@ public class SearchService extends AbstractLifecycleComponent<SearchService> {
             if (!hasLength(templateContext.template())) {
                 throw new ElasticsearchParseException("Template must have [template] field configured");
             }
-            executable = this.scriptService.executable(MustacheScriptEngineService.NAME, templateContext.template(), templateContext.scriptType(), ScriptContext.SEARCH, templateContext.params());
+            executable = this.scriptService.executable(MustacheScriptEngineService.NAME, templateContext.template(), templateContext.scriptType(), ScriptContext.Standard.SEARCH, templateContext.params());
         }
 
         BytesReference processedQuery = (BytesReference) executable.run();

+ 3 - 6
src/main/java/org/elasticsearch/search/aggregations/bucket/significant/heuristics/ScriptHeuristic.java

@@ -30,10 +30,7 @@ import org.elasticsearch.common.logging.ESLoggerFactory;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.common.xcontent.XContentParser;
 import org.elasticsearch.index.query.QueryParsingException;
-import org.elasticsearch.script.ExecutableScript;
-import org.elasticsearch.script.ScriptParameterParser;
-import org.elasticsearch.script.ScriptService;
-import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.script.*;
 import org.elasticsearch.search.aggregations.InternalAggregation;
 
 import java.io.IOException;
@@ -86,7 +83,7 @@ public class ScriptHeuristic extends SignificanceHeuristic {
     }
 
     public void initialize(InternalAggregation.ReduceContext context) {
-        script = context.scriptService().executable(scriptLang, scriptString, scriptType, ScriptContext.AGGS, params);
+        script = context.scriptService().executable(scriptLang, scriptString, scriptType, ScriptContext.Standard.AGGS, params);
         script.setNextVar("_subset_freq", subsetDfHolder);
         script.setNextVar("_subset_size", subsetSizeHolder);
         script.setNextVar("_superset_freq", supersetDfHolder);
@@ -171,7 +168,7 @@ public class ScriptHeuristic extends SignificanceHeuristic {
             }
             ExecutableScript searchScript;
             try {
-                searchScript = scriptService.executable(scriptLang, script, scriptType, ScriptContext.AGGS, params);
+                searchScript = scriptService.executable(scriptLang, script, scriptType, ScriptContext.Standard.AGGS, params);
             } catch (Exception e) {
                 throw new ElasticsearchParseException("The script [" + script + "] could not be loaded", e);
             }

+ 2 - 2
src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/InternalScriptedMetric.java

@@ -24,8 +24,8 @@ import org.elasticsearch.common.io.stream.StreamInput;
 import org.elasticsearch.common.io.stream.StreamOutput;
 import org.elasticsearch.common.xcontent.XContentBuilder;
 import org.elasticsearch.script.ExecutableScript;
-import org.elasticsearch.script.ScriptService.ScriptType;
 import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.script.ScriptService.ScriptType;
 import org.elasticsearch.search.aggregations.AggregationStreams;
 import org.elasticsearch.search.aggregations.InternalAggregation;
 import org.elasticsearch.search.aggregations.metrics.InternalMetricsAggregation;
@@ -99,7 +99,7 @@ public class InternalScriptedMetric extends InternalMetricsAggregation implement
             }
             params.put("_aggs", aggregationObjects);
             ExecutableScript script = reduceContext.scriptService().executable(firstAggregation.scriptLang, firstAggregation.reduceScript,
-                    firstAggregation.scriptType, ScriptContext.AGGS, params);
+                    firstAggregation.scriptType, ScriptContext.Standard.AGGS, params);
             aggregation = script.run();
         } else {
             aggregation = aggregationObjects;

+ 4 - 5
src/main/java/org/elasticsearch/search/aggregations/metrics/scripted/ScriptedMetricAggregator.java

@@ -23,9 +23,8 @@ import org.apache.lucene.index.LeafReaderContext;
 import org.elasticsearch.script.ExecutableScript;
 import org.elasticsearch.script.LeafSearchScript;
 import org.elasticsearch.script.ScriptService;
+import org.elasticsearch.script.*;
 import org.elasticsearch.script.ScriptService.ScriptType;
-import org.elasticsearch.script.ScriptContext;
-import org.elasticsearch.script.SearchScript;
 import org.elasticsearch.search.SearchParseException;
 import org.elasticsearch.search.aggregations.Aggregator;
 import org.elasticsearch.search.aggregations.AggregatorFactory;
@@ -75,11 +74,11 @@ public class ScriptedMetricAggregator extends MetricsAggregator {
         }
         ScriptService scriptService = context.searchContext().scriptService();
         if (initScript != null) {
-            scriptService.executable(scriptLang, initScript, initScriptType, ScriptContext.AGGS, this.params).run();
+            scriptService.executable(scriptLang, initScript, initScriptType, ScriptContext.Standard.AGGS, this.params).run();
         }
-        this.mapScript = scriptService.search(context.searchContext().lookup(), scriptLang, mapScript, mapScriptType, ScriptContext.AGGS, this.params);
+        this.mapScript = scriptService.search(context.searchContext().lookup(), scriptLang, mapScript, mapScriptType, ScriptContext.Standard.AGGS, this.params);
         if (combineScript != null) {
-            this.combineScript = scriptService.executable(scriptLang, combineScript, combineScriptType, ScriptContext.AGGS, this.params);
+            this.combineScript = scriptService.executable(scriptLang, combineScript, combineScriptType, ScriptContext.Standard.AGGS, this.params);
         } else {
             this.combineScript = null;
         }

+ 2 - 5
src/main/java/org/elasticsearch/search/aggregations/support/ValuesSourceParser.java

@@ -29,11 +29,8 @@ import org.elasticsearch.index.mapper.core.BooleanFieldMapper;
 import org.elasticsearch.index.mapper.core.DateFieldMapper;
 import org.elasticsearch.index.mapper.core.NumberFieldMapper;
 import org.elasticsearch.index.mapper.ip.IpFieldMapper;
-import org.elasticsearch.script.ScriptParameterParser;
+import org.elasticsearch.script.*;
 import org.elasticsearch.script.ScriptParameterParser.ScriptParameterValue;
-import org.elasticsearch.script.ScriptService;
-import org.elasticsearch.script.ScriptContext;
-import org.elasticsearch.script.SearchScript;
 import org.elasticsearch.search.SearchParseException;
 import org.elasticsearch.search.aggregations.InternalAggregation;
 import org.elasticsearch.search.aggregations.support.format.ValueFormat;
@@ -189,7 +186,7 @@ public class ValuesSourceParser<VS extends ValuesSource> {
     }
 
     private SearchScript createScript() {
-        return input.script == null ? null : context.scriptService().search(context.lookup(), input.lang, input.script, input.scriptType, ScriptContext.AGGS, input.params);
+        return input.script == null ? null : context.scriptService().search(context.lookup(), input.lang, input.script, input.scriptType, ScriptContext.Standard.AGGS, input.params);
     }
 
     private static ValueFormat resolveFormat(@Nullable String format, @Nullable ValueType valueType) {

+ 2 - 5
src/main/java/org/elasticsearch/search/fetch/script/ScriptFieldsParseElement.java

@@ -20,11 +20,8 @@
 package org.elasticsearch.search.fetch.script;
 
 import org.elasticsearch.common.xcontent.XContentParser;
-import org.elasticsearch.script.ScriptParameterParser;
+import org.elasticsearch.script.*;
 import org.elasticsearch.script.ScriptParameterParser.ScriptParameterValue;
-import org.elasticsearch.script.ScriptService;
-import org.elasticsearch.script.ScriptContext;
-import org.elasticsearch.script.SearchScript;
 import org.elasticsearch.search.SearchParseElement;
 import org.elasticsearch.search.internal.SearchContext;
 
@@ -79,7 +76,7 @@ public class ScriptFieldsParseElement implements SearchParseElement {
                     script = scriptValue.script();
                     scriptType = scriptValue.scriptType();
                 }
-                SearchScript searchScript = context.scriptService().search(context.lookup(), scriptParameterParser.lang(), script, scriptType, ScriptContext.SEARCH, params);
+                SearchScript searchScript = context.scriptService().search(context.lookup(), scriptParameterParser.lang(), script, scriptType, ScriptContext.Standard.SEARCH, params);
                 context.scriptFields().add(new ScriptFieldsContext.ScriptField(fieldName, searchScript, ignoreException));
             }
         }

+ 1 - 1
src/main/java/org/elasticsearch/search/sort/ScriptSortParser.java

@@ -118,7 +118,7 @@ public class ScriptSortParser implements SortParser {
         if (type == null) {
             throw new SearchParseException(context, "_script sorting requires setting the type of the script");
         }
-        final SearchScript searchScript = context.scriptService().search(context.lookup(), scriptLang, script, scriptType, ScriptContext.SEARCH, params);
+        final SearchScript searchScript = context.scriptService().search(context.lookup(), scriptLang, script, scriptType, ScriptContext.Standard.SEARCH, params);
 
         if (STRING_SORT_TYPE.equals(type) && (sortMode == MultiValueMode.SUM || sortMode == MultiValueMode.AVG)) {
             throw new SearchParseException(context, "type [string] doesn't support mode [" + sortMode + "]");

+ 2 - 2
src/main/java/org/elasticsearch/search/suggest/phrase/PhraseSuggestParser.java

@@ -30,8 +30,8 @@ import org.elasticsearch.index.analysis.ShingleTokenFilterFactory;
 import org.elasticsearch.index.mapper.FieldMapper;
 import org.elasticsearch.index.mapper.MapperService;
 import org.elasticsearch.script.CompiledScript;
-import org.elasticsearch.script.ScriptService.ScriptType;
 import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.script.ScriptService.ScriptType;
 import org.elasticsearch.script.mustache.MustacheScriptEngineService;
 import org.elasticsearch.search.suggest.SuggestContextParser;
 import org.elasticsearch.search.suggest.SuggestUtils;
@@ -153,7 +153,7 @@ public final class PhraseSuggestParser implements SuggestContextParser {
                             if (suggestion.getCollateQueryScript() != null) {
                                 throw new ElasticsearchIllegalArgumentException("suggester[phrase][collate] query already set, doesn't support additional [" + fieldName + "]");
                             }
-                            CompiledScript compiledScript = suggester.scriptService().compile(MustacheScriptEngineService.NAME, templateNameOrTemplateContent, ScriptType.INLINE, ScriptContext.SEARCH);
+                            CompiledScript compiledScript = suggester.scriptService().compile(MustacheScriptEngineService.NAME, templateNameOrTemplateContent, ScriptType.INLINE, ScriptContext.Standard.SEARCH);
                             if ("query".equals(fieldName)) {
                                 suggestion.setCollateQueryScript(compiledScript);
                             } else {

+ 133 - 0
src/test/java/org/elasticsearch/script/CustomScriptContextTests.java

@@ -0,0 +1,133 @@
+/*
+ * 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 com.google.common.collect.ImmutableSet;
+import org.elasticsearch.ElasticsearchIllegalArgumentException;
+import org.elasticsearch.common.inject.Module;
+import org.elasticsearch.common.settings.ImmutableSettings;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.plugins.AbstractPlugin;
+import org.elasticsearch.script.expression.ExpressionScriptEngineService;
+import org.elasticsearch.script.groovy.GroovyScriptEngineService;
+import org.elasticsearch.script.mustache.MustacheScriptEngineService;
+import org.elasticsearch.test.ElasticsearchIntegrationTest;
+import org.junit.Test;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.notNullValue;
+
+public class CustomScriptContextTests extends ElasticsearchIntegrationTest {
+
+    private static final ImmutableSet<String> LANG_SET = ImmutableSet.of(GroovyScriptEngineService.NAME, MustacheScriptEngineService.NAME, ExpressionScriptEngineService.NAME);
+
+    private static final String PLUGIN_NAME = "testplugin";
+
+    @Override
+    protected Settings nodeSettings(int nodeOrdinal) {
+        return ImmutableSettings.builder().put(super.nodeSettings(nodeOrdinal))
+                .put("plugin.types", CustomScriptContextPlugin.class.getName())
+                .put("script." + PLUGIN_NAME + "_custom_globally_disabled_op", "off")
+                .put("script.engine.expression.inline." + PLUGIN_NAME + "_custom_exp_disabled_op", "off")
+                        .build();
+    }
+
+    @Test
+    public void testCustomScriptContextsSettings() {
+        ScriptService scriptService = internalCluster().getInstance(ScriptService.class);
+        for (String lang : LANG_SET) {
+            for (ScriptService.ScriptType scriptType : ScriptService.ScriptType.values()) {
+                try {
+                    scriptService.compile(lang, "test", scriptType, new ScriptContext.Plugin(PLUGIN_NAME, "custom_globally_disabled_op"));
+                    fail("script compilation should have been rejected");
+                } catch(ScriptException e) {
+                    assertThat(e.getMessage(), containsString("scripts of type [" + scriptType + "], operation [" + PLUGIN_NAME + "_custom_globally_disabled_op] and lang [" + lang + "] are disabled"));
+                }
+            }
+        }
+
+        try {
+            scriptService.compile("expression", "1", ScriptService.ScriptType.INLINE, new ScriptContext.Plugin(PLUGIN_NAME, "custom_exp_disabled_op"));
+            fail("script compilation should have been rejected");
+        } catch(ScriptException e) {
+            assertThat(e.getMessage(), containsString("scripts of type [inline], operation [" + PLUGIN_NAME + "_custom_exp_disabled_op] and lang [expression] are disabled"));
+        }
+
+        CompiledScript compiledScript = scriptService.compile("expression", "1", ScriptService.ScriptType.INLINE, randomFrom(ScriptContext.Standard.values()));
+        assertThat(compiledScript, notNullValue());
+
+        compiledScript = scriptService.compile("mustache", "1", ScriptService.ScriptType.INLINE, new ScriptContext.Plugin(PLUGIN_NAME, "custom_exp_disabled_op"));
+        assertThat(compiledScript, notNullValue());
+
+        for (String lang : LANG_SET) {
+            compiledScript = scriptService.compile(lang, "1", ScriptService.ScriptType.INLINE, new ScriptContext.Plugin(PLUGIN_NAME, "custom_op"));
+            assertThat(compiledScript, notNullValue());
+        }
+    }
+
+    @Test
+    public void testCompileNonRegisteredPluginContext() {
+        ScriptService scriptService = internalCluster().getInstance(ScriptService.class);
+        try {
+            scriptService.compile(randomFrom(LANG_SET.toArray(new String[LANG_SET.size()])), "test", randomFrom(ScriptService.ScriptType.values()), new ScriptContext.Plugin("test", "unknown"));
+            fail("script compilation should have been rejected");
+        } catch(ElasticsearchIllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("script context [test_unknown] not supported"));
+        }
+    }
+
+    @Test
+    public void testCompileNonRegisteredScriptContext() {
+        ScriptService scriptService = internalCluster().getInstance(ScriptService.class);
+        try {
+            scriptService.compile(randomFrom(LANG_SET.toArray(new String[LANG_SET.size()])), "test", randomFrom(ScriptService.ScriptType.values()), new ScriptContext() {
+                @Override
+                public String getKey() {
+                    return "test";
+                }
+            });
+            fail("script compilation should have been rejected");
+        } catch(ElasticsearchIllegalArgumentException e) {
+            assertThat(e.getMessage(), containsString("script context [test] not supported"));
+        }
+    }
+
+    public static class CustomScriptContextPlugin extends AbstractPlugin {
+        @Override
+        public String name() {
+            return "custom_script_context_plugin";
+        }
+
+        @Override
+        public String description() {
+            return "Custom script context plugin";
+        }
+
+        @Override
+        public void processModule(Module module) {
+            if (module instanceof ScriptModule) {
+                ScriptModule scriptModule = (ScriptModule) module;
+                scriptModule.registerScriptContext(new ScriptContext.Plugin(PLUGIN_NAME, "custom_op"));
+                scriptModule.registerScriptContext(new ScriptContext.Plugin(PLUGIN_NAME, "custom_exp_disabled_op"));
+                scriptModule.registerScriptContext(new ScriptContext.Plugin(PLUGIN_NAME, "custom_globally_disabled_op"));
+            }
+        }
+    }
+}

+ 6 - 4
src/test/java/org/elasticsearch/script/NativeScriptTests.java

@@ -20,6 +20,7 @@
 package org.elasticsearch.script;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.inject.Injector;
 import org.elasticsearch.common.inject.ModulesBuilder;
@@ -58,7 +59,7 @@ public class NativeScriptTests extends ElasticsearchTestCase {
 
         ScriptService scriptService = injector.getInstance(ScriptService.class);
 
-        ExecutableScript executable = scriptService.executable(NativeScriptEngineService.NAME, "my", ScriptType.INLINE, ScriptContext.SEARCH, null);
+        ExecutableScript executable = scriptService.executable(NativeScriptEngineService.NAME, "my", ScriptType.INLINE, ScriptContext.Standard.SEARCH, null);
         assertThat(executable.run().toString(), equalTo("test"));
         terminate(injector.getInstance(ThreadPool.class));
     }
@@ -70,7 +71,7 @@ public class NativeScriptTests extends ElasticsearchTestCase {
             ScriptType scriptType = randomFrom(ScriptType.values());
             builder.put(ScriptModes.SCRIPT_SETTINGS_PREFIX + scriptType, randomFrom(ScriptMode.values()));
         } else {
-            ScriptContext scriptContext = randomFrom(ScriptContext.values());
+            String scriptContext = randomFrom(ScriptContext.Standard.values()).getKey();
             builder.put(ScriptModes.SCRIPT_SETTINGS_PREFIX + scriptContext, randomFrom(ScriptMode.values()));
         }
         Settings settings = builder.build();
@@ -79,9 +80,10 @@ public class NativeScriptTests extends ElasticsearchTestCase {
         Map<String, NativeScriptFactory> nativeScriptFactoryMap = new HashMap<>();
         nativeScriptFactoryMap.put("my", new MyNativeScriptFactory());
         Set<ScriptEngineService> scriptEngineServices = ImmutableSet.<ScriptEngineService>of(new NativeScriptEngineService(settings, nativeScriptFactoryMap));
-        ScriptService scriptService = new ScriptService(settings, environment, scriptEngineServices, resourceWatcherService, new NodeSettingsService(settings));
+        ScriptContextRegistry scriptContextRegistry = new ScriptContextRegistry(Lists.<ScriptContext.Plugin>newArrayList());
+        ScriptService scriptService = new ScriptService(settings, environment, scriptEngineServices, resourceWatcherService, new NodeSettingsService(settings), scriptContextRegistry);
 
-        for (ScriptContext scriptContext : ScriptContext.values()) {
+        for (ScriptContext scriptContext : scriptContextRegistry.scriptContexts()) {
             assertThat(scriptService.compile(NativeScriptEngineService.NAME, "my", ScriptType.INLINE, scriptContext), notNullValue());
         }
     }

+ 83 - 0
src/test/java/org/elasticsearch/script/ScriptContextRegistryTests.java

@@ -0,0 +1,83 @@
+/*
+ * 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 com.google.common.collect.Lists;
+import org.elasticsearch.ElasticsearchIllegalArgumentException;
+import org.elasticsearch.test.ElasticsearchTestCase;
+import org.hamcrest.Matchers;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class ScriptContextRegistryTests extends ElasticsearchTestCase {
+
+    @Test
+    public void testValidateCustomScriptContextsOperation() throws IOException {
+        for (final String rejectedContext : ScriptContextRegistry.RESERVED_SCRIPT_CONTEXTS) {
+            try {
+                //try to register a prohibited script context
+                new ScriptContextRegistry(Lists.newArrayList(new ScriptContext.Plugin("test", rejectedContext)));
+                fail("ScriptContextRegistry initialization should have failed");
+            } catch(ElasticsearchIllegalArgumentException e) {
+                assertThat(e.getMessage(), Matchers.containsString("[" + rejectedContext + "] is a reserved name, it cannot be registered as a custom script context"));
+            }
+        }
+    }
+
+    @Test
+    public void testValidateCustomScriptContextsPluginName() throws IOException {
+        for (final String rejectedContext : ScriptContextRegistry.RESERVED_SCRIPT_CONTEXTS) {
+            try {
+                //try to register a prohibited script context
+                new ScriptContextRegistry(Lists.newArrayList(new ScriptContext.Plugin(rejectedContext, "test")));
+                fail("ScriptContextRegistry initialization should have failed");
+            } catch(ElasticsearchIllegalArgumentException e) {
+                assertThat(e.getMessage(), Matchers.containsString("[" + rejectedContext + "] is a reserved name, it cannot be registered as a custom script context"));
+            }
+        }
+    }
+
+    @Test(expected = ElasticsearchIllegalArgumentException.class)
+    public void testValidateCustomScriptContextsEmptyPluginName() throws IOException {
+        new ScriptContext.Plugin(randomBoolean() ? null : "", "test");
+    }
+
+    @Test(expected = ElasticsearchIllegalArgumentException.class)
+    public void testValidateCustomScriptContextsEmptyOperation() throws IOException {
+        new ScriptContext.Plugin("test", randomBoolean() ? null : "");
+    }
+
+    @Test
+    public void testDuplicatedPluginScriptContexts() throws IOException {
+        try {
+            //try to register a prohibited script context
+            new ScriptContextRegistry(Lists.newArrayList(new ScriptContext.Plugin("testplugin", "test"), new ScriptContext.Plugin("testplugin", "test")));
+            fail("ScriptContextRegistry initialization should have failed");
+        } catch(ElasticsearchIllegalArgumentException e) {
+            assertThat(e.getMessage(), Matchers.containsString("script context [testplugin_test] cannot be registered twice"));
+        }
+    }
+
+    @Test
+    public void testNonDuplicatedPluginScriptContexts() throws IOException {
+        new ScriptContextRegistry(Lists.newArrayList(new ScriptContext.Plugin("testplugin1", "test"), new ScriptContext.Plugin("testplugin2", "test")));
+    }
+}

+ 104 - 294
src/test/java/org/elasticsearch/script/ScriptModesTests.java

@@ -19,8 +19,7 @@
 
 package org.elasticsearch.script;
 
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.*;
 import org.elasticsearch.ElasticsearchIllegalArgumentException;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.settings.ImmutableSettings;
@@ -34,10 +33,7 @@ import org.junit.After;
 import org.junit.Before;
 import org.junit.Test;
 
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 
 import static org.hamcrest.CoreMatchers.equalTo;
 import static org.hamcrest.CoreMatchers.notNullValue;
@@ -49,6 +45,8 @@ public class ScriptModesTests extends ElasticsearchTestCase {
     static final String[] ENABLE_VALUES = new String[]{"on", "true", "yes", "1"};
     static final String[] DISABLE_VALUES = new String[]{"off", "false", "no", "0"};
 
+    ScriptContextRegistry scriptContextRegistry;
+    private ScriptContext[] scriptContexts;
     private Map<String, ScriptEngineService> scriptEngines;
     private ScriptModes scriptModes;
     private Set<String> checkedSettings;
@@ -57,6 +55,18 @@ public class ScriptModesTests extends ElasticsearchTestCase {
 
     @Before
     public void setupScriptEngines() {
+        //randomly register custom script contexts
+        int randomInt = randomIntBetween(0, 3);
+        //prevent duplicates using map
+        Map<String, ScriptContext.Plugin> contexts = Maps.newHashMap();
+        for (int i = 0; i < randomInt; i++) {
+            String plugin = randomAsciiOfLength(randomIntBetween(1, 10));
+            String operation = randomAsciiOfLength(randomIntBetween(1, 30));
+            String context = plugin + "-" + operation;
+            contexts.put(context, new ScriptContext.Plugin(plugin, operation));
+        }
+        scriptContextRegistry = new ScriptContextRegistry(contexts.values());
+        scriptContexts = scriptContextRegistry.scriptContexts().toArray(new ScriptContext[scriptContextRegistry.scriptContexts().size()]);
         scriptEngines = buildScriptEnginesByLangMap(ImmutableSet.of(
                 new GroovyScriptEngineService(ImmutableSettings.EMPTY),
                 new MustacheScriptEngineService(ImmutableSettings.EMPTY),
@@ -72,7 +82,7 @@ public class ScriptModesTests extends ElasticsearchTestCase {
     @After
     public void assertNativeScriptsAreAlwaysAllowed() {
         if (assertScriptModesNonNull) {
-            assertThat(scriptModes.getScriptMode(NativeScriptEngineService.NAME, randomFrom(ScriptType.values()), randomFrom(ScriptContext.values())), equalTo(ScriptMode.ON));
+            assertThat(scriptModes.getScriptMode(NativeScriptEngineService.NAME, randomFrom(ScriptType.values()), randomFrom(scriptContexts)), equalTo(ScriptMode.ON));
         }
     }
 
@@ -81,7 +91,7 @@ public class ScriptModesTests extends ElasticsearchTestCase {
         if (assertScriptModesNonNull) {
             assertThat(scriptModes, notNullValue());
             //4 is the number of engines (native excluded), custom is counted twice though as it's associated with two different names
-            int numberOfSettings = 5 * ScriptType.values().length * ScriptContext.values().length;
+            int numberOfSettings = 5 * ScriptType.values().length * scriptContextRegistry.scriptContexts().size();
             assertThat(scriptModes.scriptModes.size(), equalTo(numberOfSettings));
             if (assertAllSettingsWereChecked) {
                 assertThat(checkedSettings.size(), equalTo(numberOfSettings));
@@ -91,7 +101,7 @@ public class ScriptModesTests extends ElasticsearchTestCase {
 
     @Test
     public void testDefaultSettings() {
-        this.scriptModes = new ScriptModes(scriptEngines, ImmutableSettings.EMPTY);
+        this.scriptModes = new ScriptModes(scriptEngines, scriptContextRegistry, ImmutableSettings.EMPTY);
         assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE);
         assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED, ScriptType.INLINE);
     }
@@ -99,239 +109,95 @@ public class ScriptModesTests extends ElasticsearchTestCase {
     @Test(expected = ElasticsearchIllegalArgumentException.class)
     public void testMissingSetting() {
         assertAllSettingsWereChecked = false;
-        this.scriptModes = new ScriptModes(scriptEngines, ImmutableSettings.EMPTY);
-        scriptModes.getScriptMode("non_existing", randomFrom(ScriptType.values()), randomFrom(ScriptContext.values()));
-    }
-
-    @Test
-    public void testEnableInlineGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.inline", randomFrom(ENABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE, ScriptType.INLINE);
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED);
-    }
-
-    @Test
-    public void testDisableInlineGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.inline", randomFrom(DISABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE);
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED);
-        assertScriptModesAllOps(ScriptMode.OFF, ALL_LANGS, ScriptType.INLINE);
-    }
-
-    @Test
-    public void testSandboxInlineGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.inline", randomFrom(ScriptMode.SANDBOX));
-        //nothing changes if setting set is same as default
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE);
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED, ScriptType.INLINE);
-    }
-
-    @Test
-    public void testEnableIndexedGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.indexed", randomFrom(ENABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE, ScriptType.INDEXED);
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INLINE);
-    }
-
-    @Test
-    public void testDisableIndexedGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.indexed", randomFrom(DISABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE);
-        assertScriptModesAllOps(ScriptMode.OFF, ALL_LANGS, ScriptType.INDEXED);
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INLINE);
-    }
-
-    @Test
-    public void testSandboxIndexedGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.indexed", ScriptMode.SANDBOX);
-        //nothing changes if setting set is same as default
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE);
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED, ScriptType.INLINE);
-    }
-
-    @Test
-    public void testEnableFileGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.file", randomFrom(ENABLE_VALUES));
-        //nothing changes if setting set is same as default
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE);
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED, ScriptType.INLINE);
-    }
-
-    @Test
-    public void testDisableFileGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.file", randomFrom(DISABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.OFF, ALL_LANGS, ScriptType.FILE);
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED, ScriptType.INLINE);
-    }
-
-    @Test
-    public void testSandboxFileGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.file", ScriptMode.SANDBOX);
-        //nothing changes if setting set is same as default
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.FILE, ScriptType.INDEXED, ScriptType.INLINE);
-    }
-
-    @Test
-    public void testMultipleScriptTypeGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.file", ScriptMode.SANDBOX).put("script.inline", randomFrom(DISABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.FILE);
-        assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED);
-        assertScriptModesAllOps(ScriptMode.OFF, ALL_LANGS, ScriptType.INLINE);
-    }
-
-    @Test
-    public void testEnableMappingGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.mapping", randomFrom(ENABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.ON, ALL_LANGS, ScriptContext.MAPPING);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.AGGS, ScriptContext.SEARCH, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.AGGS, ScriptContext.SEARCH, ScriptContext.UPDATE);
+        this.scriptModes = new ScriptModes(scriptEngines, scriptContextRegistry, ImmutableSettings.EMPTY);
+        scriptModes.getScriptMode("non_existing", randomFrom(ScriptType.values()), randomFrom(scriptContexts));
     }
 
     @Test
-    public void testDisableMappingGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.mapping", randomFrom(DISABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.OFF, ALL_LANGS, ScriptContext.MAPPING);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.AGGS, ScriptContext.SEARCH, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.AGGS, ScriptContext.SEARCH, ScriptContext.UPDATE);
-    }
-
-    @Test
-    public void testSandboxMappingGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.mapping", ScriptMode.SANDBOX);
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.SANDBOX, ALL_LANGS, ScriptContext.MAPPING);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.AGGS, ScriptContext.SEARCH, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.AGGS, ScriptContext.SEARCH, ScriptContext.UPDATE);
-    }
-
-    @Test
-    public void testEnableSearchGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.search", randomFrom(ENABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.ON, ALL_LANGS, ScriptContext.SEARCH);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.AGGS, ScriptContext.MAPPING, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.AGGS, ScriptContext.MAPPING, ScriptContext.UPDATE);
-    }
-
-    @Test
-    public void testDisableSearchGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.search", randomFrom(DISABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.OFF, ALL_LANGS, ScriptContext.SEARCH);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.AGGS, ScriptContext.MAPPING, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.AGGS, ScriptContext.MAPPING, ScriptContext.UPDATE);
-    }
-
-    @Test
-    public void testSandboxSearchGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.search", ScriptMode.SANDBOX);
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.SANDBOX, ALL_LANGS, ScriptContext.SEARCH);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.AGGS, ScriptContext.MAPPING, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.AGGS, ScriptContext.MAPPING, ScriptContext.UPDATE);
-    }
-
-    @Test
-    public void testEnableAggsGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.aggs", randomFrom(ENABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.ON, ALL_LANGS, ScriptContext.AGGS);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.UPDATE);
-    }
-
-    @Test
-    public void testDisableAggsGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.aggs", randomFrom(DISABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.OFF, ALL_LANGS, ScriptContext.AGGS);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.UPDATE);
-    }
-
-    @Test
-    public void testSandboxAggsGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.aggs", ScriptMode.SANDBOX);
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.SANDBOX, ALL_LANGS, ScriptContext.AGGS);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.UPDATE);
-    }
+    public void testScriptTypeGenericSettings() {
+        int randomInt = randomIntBetween(1, ScriptType.values().length - 1);
+        Set<ScriptType> randomScriptTypesSet = Sets.newHashSet();
+        ScriptMode[] randomScriptModes = new ScriptMode[randomInt];
+        for (int i = 0; i < randomInt; i++) {
+            boolean added = false;
+            while (added == false) {
+                added = randomScriptTypesSet.add(randomFrom(ScriptType.values()));
+            }
+            randomScriptModes[i] = randomFrom(ScriptMode.values());
+        }
+        ScriptType[] randomScriptTypes = randomScriptTypesSet.toArray(new ScriptType[randomScriptTypesSet.size()]);
+        ImmutableSettings.Builder builder = ImmutableSettings.builder();
+        for (int i = 0; i < randomInt; i++) {
+            builder.put(ScriptModes.SCRIPT_SETTINGS_PREFIX + randomScriptTypes[i], randomScriptModes[i]);
+        }
+        this.scriptModes = new ScriptModes(scriptEngines, scriptContextRegistry, builder.build());
 
-    @Test
-    public void testEnableUpdateGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.update", randomFrom(ENABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.ON, ALL_LANGS, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.AGGS);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.AGGS);
+        for (int i = 0; i < randomInt; i++) {
+            assertScriptModesAllOps(randomScriptModes[i], ALL_LANGS, randomScriptTypes[i]);
+        }
+        if (randomScriptTypesSet.contains(ScriptType.FILE) == false) {
+            assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE);
+        }
+        if (randomScriptTypesSet.contains(ScriptType.INDEXED) == false) {
+            assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED);
+        }
+        if (randomScriptTypesSet.contains(ScriptType.INLINE) == false) {
+            assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INLINE);
+        }
     }
 
     @Test
-    public void testDisableUpdateGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.update", randomFrom(DISABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.OFF, ALL_LANGS, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.AGGS);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.AGGS);
-    }
+    public void testScriptContextGenericSettings() {
+        int randomInt = randomIntBetween(1, scriptContexts.length - 1);
+        Set<ScriptContext> randomScriptContextsSet = Sets.newHashSet();
+        ScriptMode[] randomScriptModes = new ScriptMode[randomInt];
+        for (int i = 0; i < randomInt; i++) {
+            boolean added = false;
+            while (added == false) {
+                added = randomScriptContextsSet.add(randomFrom(scriptContexts));
+            }
+            randomScriptModes[i] = randomFrom(ScriptMode.values());
+        }
+        ScriptContext[] randomScriptContexts = randomScriptContextsSet.toArray(new ScriptContext[randomScriptContextsSet.size()]);
+        ImmutableSettings.Builder builder = ImmutableSettings.builder();
+        for (int i = 0; i < randomInt; i++) {
+            builder.put(ScriptModes.SCRIPT_SETTINGS_PREFIX + randomScriptContexts[i].getKey(), randomScriptModes[i]);
+        }
+        this.scriptModes = new ScriptModes(scriptEngines, scriptContextRegistry, builder.build());
 
-    @Test
-    public void testSandboxUpdateGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.update", ScriptMode.SANDBOX);
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.SANDBOX, ALL_LANGS, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.AGGS);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.SEARCH, ScriptContext.MAPPING, ScriptContext.AGGS);
-    }
+        for (int i = 0; i < randomInt; i++) {
+            assertScriptModesAllTypes(randomScriptModes[i], ALL_LANGS, randomScriptContexts[i]);
+        }
 
-    @Test
-    public void testMultipleScriptContextGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.update", ScriptMode.SANDBOX)
-                .put("script.aggs", randomFrom(DISABLE_VALUES))
-                .put("script.search", randomFrom(ENABLE_VALUES));
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.SANDBOX, ALL_LANGS, ScriptContext.UPDATE);
-        assertScriptModesAllTypes(ScriptMode.OFF, ALL_LANGS, ScriptContext.AGGS);
-        assertScriptModesAllTypes(ScriptMode.ON, ALL_LANGS, ScriptContext.SEARCH);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, ScriptContext.MAPPING);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, ScriptContext.MAPPING);
+        ScriptContext[] complementOf = complementOf(randomScriptContexts);
+        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE}, complementOf);
+        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INDEXED, ScriptType.INLINE}, complementOf);
     }
 
     @Test
     public void testConflictingScriptTypeAndOpGenericSettings() {
-        ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.update", randomFrom(DISABLE_VALUES))
+        ScriptContext scriptContext = randomFrom(scriptContexts);
+        ImmutableSettings.Builder builder = ImmutableSettings.builder().put(ScriptModes.SCRIPT_SETTINGS_PREFIX + scriptContext.getKey(), randomFrom(DISABLE_VALUES))
                 .put("script.indexed", randomFrom(ENABLE_VALUES)).put("script.inline", ScriptMode.SANDBOX);
         //operations generic settings have precedence over script type generic settings
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModesAllTypes(ScriptMode.OFF, ALL_LANGS, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE, ScriptType.INDEXED}, ScriptContext.MAPPING, ScriptContext.AGGS, ScriptContext.SEARCH);
-        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INLINE}, ScriptContext.MAPPING, ScriptContext.AGGS, ScriptContext.SEARCH);
+        this.scriptModes = new ScriptModes(scriptEngines, scriptContextRegistry, builder.build());
+        assertScriptModesAllTypes(ScriptMode.OFF, ALL_LANGS, scriptContext);
+        ScriptContext[] complementOf = complementOf(scriptContext);
+        assertScriptModes(ScriptMode.ON, ALL_LANGS, new ScriptType[]{ScriptType.FILE, ScriptType.INDEXED}, complementOf);
+        assertScriptModes(ScriptMode.SANDBOX, ALL_LANGS, new ScriptType[]{ScriptType.INLINE}, complementOf);
     }
 
     @Test
     public void testEngineSpecificSettings() {
         ImmutableSettings.Builder builder = ImmutableSettings.builder()
-                .put(specificEngineOpSettings(GroovyScriptEngineService.NAME, ScriptType.INLINE, ScriptContext.MAPPING), randomFrom(DISABLE_VALUES))
-                .put(specificEngineOpSettings(GroovyScriptEngineService.NAME, ScriptType.INLINE, ScriptContext.UPDATE), randomFrom(DISABLE_VALUES));
+                .put(specificEngineOpSettings(GroovyScriptEngineService.NAME, ScriptType.INLINE, ScriptContext.Standard.MAPPING), randomFrom(DISABLE_VALUES))
+                .put(specificEngineOpSettings(GroovyScriptEngineService.NAME, ScriptType.INLINE, ScriptContext.Standard.UPDATE), randomFrom(DISABLE_VALUES));
         ImmutableSet<String> groovyLangSet = ImmutableSet.of(GroovyScriptEngineService.NAME);
         Set<String> allButGroovyLangSet = new HashSet<>(ALL_LANGS);
         allButGroovyLangSet.remove(GroovyScriptEngineService.NAME);
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModes(ScriptMode.OFF, groovyLangSet, new ScriptType[]{ScriptType.INLINE}, ScriptContext.MAPPING, ScriptContext.UPDATE);
-        assertScriptModes(ScriptMode.SANDBOX, groovyLangSet, new ScriptType[]{ScriptType.INLINE}, ScriptContext.SEARCH, ScriptContext.AGGS);
+        this.scriptModes = new ScriptModes(scriptEngines, scriptContextRegistry, builder.build());
+        assertScriptModes(ScriptMode.OFF, groovyLangSet, new ScriptType[]{ScriptType.INLINE}, ScriptContext.Standard.MAPPING, ScriptContext.Standard.UPDATE);
+        assertScriptModes(ScriptMode.SANDBOX, groovyLangSet, new ScriptType[]{ScriptType.INLINE}, complementOf(ScriptContext.Standard.MAPPING, ScriptContext.Standard.UPDATE));
         assertScriptModesAllOps(ScriptMode.SANDBOX, allButGroovyLangSet, ScriptType.INLINE);
         assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED);
         assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE);
@@ -340,88 +206,21 @@ public class ScriptModesTests extends ElasticsearchTestCase {
     @Test
     public void testInteractionBetweenGenericAndEngineSpecificSettings() {
         ImmutableSettings.Builder builder = ImmutableSettings.builder().put("script.inline", randomFrom(DISABLE_VALUES))
-                .put(specificEngineOpSettings(MustacheScriptEngineService.NAME, ScriptType.INLINE, ScriptContext.AGGS), randomFrom(ENABLE_VALUES))
-                .put(specificEngineOpSettings(MustacheScriptEngineService.NAME, ScriptType.INLINE, ScriptContext.SEARCH), randomFrom(ENABLE_VALUES));
+                .put(specificEngineOpSettings(MustacheScriptEngineService.NAME, ScriptType.INLINE, ScriptContext.Standard.AGGS), randomFrom(ENABLE_VALUES))
+                .put(specificEngineOpSettings(MustacheScriptEngineService.NAME, ScriptType.INLINE, ScriptContext.Standard.SEARCH), randomFrom(ENABLE_VALUES));
         ImmutableSet<String> mustacheLangSet = ImmutableSet.of(MustacheScriptEngineService.NAME);
         Set<String> allButMustacheLangSet = new HashSet<>(ALL_LANGS);
         allButMustacheLangSet.remove(MustacheScriptEngineService.NAME);
-        this.scriptModes = new ScriptModes(scriptEngines, builder.build());
-        assertScriptModes(ScriptMode.ON, mustacheLangSet, new ScriptType[]{ScriptType.INLINE}, ScriptContext.AGGS, ScriptContext.SEARCH);
-        assertScriptModes(ScriptMode.OFF, mustacheLangSet, new ScriptType[]{ScriptType.INLINE}, ScriptContext.MAPPING, ScriptContext.UPDATE);
+        this.scriptModes = new ScriptModes(scriptEngines, scriptContextRegistry, builder.build());
+        assertScriptModes(ScriptMode.ON, mustacheLangSet, new ScriptType[]{ScriptType.INLINE}, ScriptContext.Standard.AGGS, ScriptContext.Standard.SEARCH);
+        assertScriptModes(ScriptMode.OFF, mustacheLangSet, new ScriptType[]{ScriptType.INLINE}, complementOf(ScriptContext.Standard.AGGS, ScriptContext.Standard.SEARCH));
         assertScriptModesAllOps(ScriptMode.OFF, allButMustacheLangSet, ScriptType.INLINE);
         assertScriptModesAllOps(ScriptMode.SANDBOX, ALL_LANGS, ScriptType.INDEXED);
         assertScriptModesAllOps(ScriptMode.ON, ALL_LANGS, ScriptType.FILE);
     }
 
-    @Test
-    public void testDefaultSettingsToString() {
-        assertAllSettingsWereChecked = false;
-        this.scriptModes = new ScriptModes(scriptEngines, ImmutableSettings.EMPTY);
-        assertThat(scriptModes.toString(), equalTo(
-                        "script.engine.custom.file.aggs: on\n" +
-                        "script.engine.custom.file.mapping: on\n" +
-                        "script.engine.custom.file.search: on\n" +
-                        "script.engine.custom.file.update: on\n" +
-                        "script.engine.custom.indexed.aggs: sandbox\n" +
-                        "script.engine.custom.indexed.mapping: sandbox\n" +
-                        "script.engine.custom.indexed.search: sandbox\n" +
-                        "script.engine.custom.indexed.update: sandbox\n" +
-                        "script.engine.custom.inline.aggs: sandbox\n" +
-                        "script.engine.custom.inline.mapping: sandbox\n" +
-                        "script.engine.custom.inline.search: sandbox\n" +
-                        "script.engine.custom.inline.update: sandbox\n" +
-                        "script.engine.expression.file.aggs: on\n" +
-                        "script.engine.expression.file.mapping: on\n" +
-                        "script.engine.expression.file.search: on\n" +
-                        "script.engine.expression.file.update: on\n" +
-                        "script.engine.expression.indexed.aggs: sandbox\n" +
-                        "script.engine.expression.indexed.mapping: sandbox\n" +
-                        "script.engine.expression.indexed.search: sandbox\n" +
-                        "script.engine.expression.indexed.update: sandbox\n" +
-                        "script.engine.expression.inline.aggs: sandbox\n" +
-                        "script.engine.expression.inline.mapping: sandbox\n" +
-                        "script.engine.expression.inline.search: sandbox\n" +
-                        "script.engine.expression.inline.update: sandbox\n" +
-                        "script.engine.groovy.file.aggs: on\n" +
-                        "script.engine.groovy.file.mapping: on\n" +
-                        "script.engine.groovy.file.search: on\n" +
-                        "script.engine.groovy.file.update: on\n" +
-                        "script.engine.groovy.indexed.aggs: sandbox\n" +
-                        "script.engine.groovy.indexed.mapping: sandbox\n" +
-                        "script.engine.groovy.indexed.search: sandbox\n" +
-                        "script.engine.groovy.indexed.update: sandbox\n" +
-                        "script.engine.groovy.inline.aggs: sandbox\n" +
-                        "script.engine.groovy.inline.mapping: sandbox\n" +
-                        "script.engine.groovy.inline.search: sandbox\n" +
-                        "script.engine.groovy.inline.update: sandbox\n" +
-                        "script.engine.mustache.file.aggs: on\n" +
-                        "script.engine.mustache.file.mapping: on\n" +
-                        "script.engine.mustache.file.search: on\n" +
-                        "script.engine.mustache.file.update: on\n" +
-                        "script.engine.mustache.indexed.aggs: sandbox\n" +
-                        "script.engine.mustache.indexed.mapping: sandbox\n" +
-                        "script.engine.mustache.indexed.search: sandbox\n" +
-                        "script.engine.mustache.indexed.update: sandbox\n" +
-                        "script.engine.mustache.inline.aggs: sandbox\n" +
-                        "script.engine.mustache.inline.mapping: sandbox\n" +
-                        "script.engine.mustache.inline.search: sandbox\n" +
-                        "script.engine.mustache.inline.update: sandbox\n" +
-                        "script.engine.test.file.aggs: on\n" +
-                        "script.engine.test.file.mapping: on\n" +
-                        "script.engine.test.file.search: on\n" +
-                        "script.engine.test.file.update: on\n" +
-                        "script.engine.test.indexed.aggs: sandbox\n" +
-                        "script.engine.test.indexed.mapping: sandbox\n" +
-                        "script.engine.test.indexed.search: sandbox\n" +
-                        "script.engine.test.indexed.update: sandbox\n" +
-                        "script.engine.test.inline.aggs: sandbox\n" +
-                        "script.engine.test.inline.mapping: sandbox\n" +
-                        "script.engine.test.inline.search: sandbox\n" +
-                        "script.engine.test.inline.update: sandbox\n"));
-    }
-
     private void assertScriptModesAllOps(ScriptMode expectedScriptMode, Set<String> langs, ScriptType... scriptTypes) {
-        assertScriptModes(expectedScriptMode, langs, scriptTypes, ScriptContext.values());
+        assertScriptModes(expectedScriptMode, langs, scriptTypes, scriptContexts);
     }
 
     private void assertScriptModesAllTypes(ScriptMode expectedScriptMode, Set<String> langs, ScriptContext... scriptContexts) {
@@ -435,15 +234,26 @@ public class ScriptModesTests extends ElasticsearchTestCase {
         for (String lang : langs) {
             for (ScriptType scriptType : scriptTypes) {
                 for (ScriptContext scriptContext : scriptContexts) {
-                    assertThat(lang + "." + scriptType + "." + scriptContext + " doesn't have the expected value", scriptModes.getScriptMode(lang, scriptType, scriptContext), equalTo(expectedScriptMode));
+                    assertThat(lang + "." + scriptType + "." + scriptContext.getKey() + " doesn't have the expected value", scriptModes.getScriptMode(lang, scriptType, scriptContext), equalTo(expectedScriptMode));
                     checkedSettings.add(lang + "." + scriptType + "." + scriptContext);
                 }
             }
         }
     }
 
+    private ScriptContext[] complementOf(ScriptContext... scriptContexts) {
+        Map<String, ScriptContext> copy = Maps.newHashMap();
+        for (ScriptContext scriptContext : scriptContextRegistry.scriptContexts()) {
+            copy.put(scriptContext.getKey(), scriptContext);
+        }
+        for (ScriptContext scriptContext : scriptContexts) {
+            copy.remove(scriptContext.getKey());
+        }
+        return copy.values().toArray(new ScriptContext[copy.size()]);
+    }
+
     private static String specificEngineOpSettings(String lang, ScriptType scriptType, ScriptContext scriptContext) {
-        return ScriptModes.ENGINE_SETTINGS_PREFIX + "." + lang + "." + scriptType + "." + scriptContext;
+        return ScriptModes.ENGINE_SETTINGS_PREFIX + "." + lang + "." + scriptType + "." + scriptContext.getKey();
     }
 
     static ImmutableMap<String, ScriptEngineService> buildScriptEnginesByLangMap(Set<ScriptEngineService> scriptEngines) {

+ 65 - 24
src/test/java/org/elasticsearch/script/ScriptServiceTests.java

@@ -19,6 +19,7 @@
 package org.elasticsearch.script;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
 import org.elasticsearch.ElasticsearchIllegalArgumentException;
 import org.elasticsearch.common.Nullable;
 import org.elasticsearch.common.io.Streams;
@@ -44,16 +45,16 @@ import java.util.Map;
 import java.util.Set;
 
 import static org.elasticsearch.common.settings.ImmutableSettings.settingsBuilder;
+import static org.hamcrest.CoreMatchers.containsString;
 import static org.hamcrest.Matchers.*;
 
-/**
- *
- */
 public class ScriptServiceTests extends ElasticsearchTestCase {
 
     private ResourceWatcherService resourceWatcherService;
     private Set<ScriptEngineService> scriptEngineServices;
     private Map<String, ScriptEngineService> scriptEnginesByLangMap;
+    private ScriptContextRegistry scriptContextRegistry;
+    private ScriptContext[] scriptContexts;
     private ScriptService scriptService;
     private Path scriptsFilePath;
     private Settings baseSettings;
@@ -76,6 +77,24 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
         scriptEngineServices = ImmutableSet.of(new TestEngineService(), new GroovyScriptEngineService(baseSettings),
                 new ExpressionScriptEngineService(baseSettings), new MustacheScriptEngineService(baseSettings));
         scriptEnginesByLangMap = ScriptModesTests.buildScriptEnginesByLangMap(scriptEngineServices);
+        //randomly register custom script contexts
+        int randomInt = randomIntBetween(0, 3);
+        //prevent duplicates using map
+        Map<String, ScriptContext.Plugin> contexts = Maps.newHashMap();
+        for (int i = 0; i < randomInt; i++) {
+            String plugin;
+            do {
+                plugin = randomAsciiOfLength(randomIntBetween(1, 10));
+            } while (ScriptContextRegistry.RESERVED_SCRIPT_CONTEXTS.contains(plugin));
+            String operation;
+            do {
+                operation = randomAsciiOfLength(randomIntBetween(1, 30));
+            } while (ScriptContextRegistry.RESERVED_SCRIPT_CONTEXTS.contains(operation));
+            String context = plugin + "_" + operation;
+            contexts.put(context, new ScriptContext.Plugin(plugin, operation));
+        }
+        scriptContextRegistry = new ScriptContextRegistry(contexts.values());
+        scriptContexts = scriptContextRegistry.scriptContexts().toArray(new ScriptContext[scriptContextRegistry.scriptContexts().size()]);
         logger.info("--> setup script service");
         scriptsFilePath = genericConfigFolder.resolve("scripts");
         Files.createDirectories(scriptsFilePath);
@@ -84,7 +103,7 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
     private void buildScriptService(Settings additionalSettings) throws IOException {
         Settings finalSettings = ImmutableSettings.builder().put(baseSettings).put(additionalSettings).build();
         Environment environment = new Environment(finalSettings);
-        scriptService = new ScriptService(finalSettings, environment, scriptEngineServices, resourceWatcherService, new NodeSettingsService(finalSettings)) {
+        scriptService = new ScriptService(finalSettings, environment, scriptEngineServices, resourceWatcherService, new NodeSettingsService(finalSettings), scriptContextRegistry) {
             @Override
             String getScriptFromIndex(String scriptLang, String id) {
                 //mock the script that gets retrieved from an index
@@ -114,7 +133,7 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
         resourceWatcherService.notifyNow();
 
         logger.info("--> verify that file with extension was correctly processed");
-        CompiledScript compiledScript = scriptService.compile("test", "test_script", ScriptType.FILE, ScriptContext.SEARCH);
+        CompiledScript compiledScript = scriptService.compile("test", "test_script", ScriptType.FILE, ScriptContext.Standard.SEARCH);
         assertThat(compiledScript.compiled(), equalTo((Object) "compiled_test_file"));
 
         logger.info("--> delete both files");
@@ -124,7 +143,7 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
 
         logger.info("--> verify that file with extension was correctly removed");
         try {
-            scriptService.compile("test", "test_script", ScriptType.FILE, ScriptContext.SEARCH);
+            scriptService.compile("test", "test_script", ScriptType.FILE, ScriptContext.Standard.SEARCH);
             fail("the script test_script should no longer exist");
         } catch (ElasticsearchIllegalArgumentException ex) {
             assertThat(ex.getMessage(), containsString("Unable to find on disk script test_script"));
@@ -135,17 +154,17 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
     public void testScriptsSameNameDifferentLanguage() throws IOException {
         buildScriptService(ImmutableSettings.EMPTY);
         createFileScripts("groovy", "expression");
-        CompiledScript groovyScript = scriptService.compile(GroovyScriptEngineService.NAME, "file_script", ScriptType.FILE, randomFrom(ScriptContext.values()));
+        CompiledScript groovyScript = scriptService.compile(GroovyScriptEngineService.NAME, "file_script", ScriptType.FILE, randomFrom(scriptContexts));
         assertThat(groovyScript.lang(), equalTo(GroovyScriptEngineService.NAME));
-        CompiledScript expressionScript = scriptService.compile(ExpressionScriptEngineService.NAME, "file_script", ScriptType.FILE, randomFrom(ScriptContext.values()));
+        CompiledScript expressionScript = scriptService.compile(ExpressionScriptEngineService.NAME, "file_script", ScriptType.FILE, randomFrom(scriptContexts));
         assertThat(expressionScript.lang(), equalTo(ExpressionScriptEngineService.NAME));
     }
 
     @Test
     public void testInlineScriptCompiledOnceMultipleLangAcronyms() throws IOException {
         buildScriptService(ImmutableSettings.EMPTY);
-        CompiledScript compiledScript1 = scriptService.compile("test", "script", ScriptType.INLINE, randomFrom(ScriptContext.values()));
-        CompiledScript compiledScript2 = scriptService.compile("test2", "script", ScriptType.INLINE, randomFrom(ScriptContext.values()));
+        CompiledScript compiledScript1 = scriptService.compile("test", "script", ScriptType.INLINE, randomFrom(scriptContexts));
+        CompiledScript compiledScript2 = scriptService.compile("test2", "script", ScriptType.INLINE, randomFrom(scriptContexts));
         assertThat(compiledScript1, sameInstance(compiledScript2));
     }
 
@@ -153,8 +172,8 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
     public void testFileScriptCompiledOnceMultipleLangAcronyms() throws IOException {
         buildScriptService(ImmutableSettings.EMPTY);
         createFileScripts("test");
-        CompiledScript compiledScript1 = scriptService.compile("test", "file_script", ScriptType.FILE, randomFrom(ScriptContext.values()));
-        CompiledScript compiledScript2 = scriptService.compile("test2", "file_script", ScriptType.FILE, randomFrom(ScriptContext.values()));
+        CompiledScript compiledScript1 = scriptService.compile("test", "file_script", ScriptType.FILE, randomFrom(scriptContexts));
+        CompiledScript compiledScript2 = scriptService.compile("test2", "file_script", ScriptType.FILE, randomFrom(scriptContexts));
         assertThat(compiledScript1, sameInstance(compiledScript2));
     }
 
@@ -174,7 +193,7 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
         buildScriptService(builder.build());
         createFileScripts("groovy", "expression", "mustache", "test");
 
-        for (ScriptContext scriptContext : ScriptContext.values()) {
+        for (ScriptContext scriptContext : scriptContexts) {
             //groovy is not sandboxed, only file scripts are enabled by default
             assertCompileRejected(GroovyScriptEngineService.NAME, "script", ScriptType.INLINE, scriptContext);
             assertCompileRejected(GroovyScriptEngineService.NAME, "script", ScriptType.INDEXED, scriptContext);
@@ -206,12 +225,12 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
             } while (scriptSourceSettings.containsKey(scriptType));
             scriptSourceSettings.put(scriptType, randomFrom(ScriptMode.values()));
         }
-        int numScriptContextSettings = randomIntBetween(0, ScriptContext.values().length);
-        Map<ScriptContext, ScriptMode> scriptContextSettings = new HashMap<>();
+        int numScriptContextSettings = randomIntBetween(0, this.scriptContextRegistry.scriptContexts().size());
+        Map<String, ScriptMode> scriptContextSettings = new HashMap<>();
         for (int i = 0; i < numScriptContextSettings; i++) {
-            ScriptContext scriptContext;
+            String scriptContext;
             do {
-                scriptContext = randomFrom(ScriptContext.values());
+                scriptContext = randomFrom(this.scriptContexts).getKey();
             } while (scriptContextSettings.containsKey(scriptContext));
             scriptContextSettings.put(scriptContext, randomFrom(ScriptMode.values()));
         }
@@ -223,9 +242,9 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
                 ScriptEngineService[] scriptEngineServices = this.scriptEngineServices.toArray(new ScriptEngineService[this.scriptEngineServices.size()]);
                 ScriptEngineService scriptEngineService = randomFrom(scriptEngineServices);
                 ScriptType scriptType = randomFrom(ScriptType.values());
-                ScriptContext scriptContext = randomFrom(ScriptContext.values());
-                settingKey = scriptEngineService.types()[0] + "." + scriptType + "." + scriptContext;
-            } while(engineSettings.containsKey(settingKey));
+                ScriptContext scriptContext = randomFrom(this.scriptContexts);
+                settingKey = scriptEngineService.types()[0] + "." + scriptType + "." + scriptContext.getKey();
+            } while (engineSettings.containsKey(settingKey));
             engineSettings.put(settingKey, randomFrom(ScriptMode.values()));
         }
         //set the selected fine-grained settings
@@ -243,7 +262,7 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
                     break;
             }
         }
-        for (Map.Entry<ScriptContext, ScriptMode> entry : scriptContextSettings.entrySet()) {
+        for (Map.Entry<String, ScriptMode> entry : scriptContextSettings.entrySet()) {
             switch (entry.getValue()) {
                 case ON:
                     builder.put(ScriptModes.SCRIPT_SETTINGS_PREFIX + entry.getKey(), randomFrom(ScriptModesTests.ENABLE_VALUES));
@@ -283,11 +302,11 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
                 //make sure file scripts have a different name than inline ones.
                 //Otherwise they are always considered file ones as they can be found in the static cache.
                 String script = scriptType == ScriptType.FILE ? "file_script" : "script";
-                for (ScriptContext scriptContext : ScriptContext.values()) {
+                for (ScriptContext scriptContext : this.scriptContexts) {
                     //fallback mechanism: 1) engine specific settings 2) op based settings 3) source based settings
-                    ScriptMode scriptMode = engineSettings.get(scriptEngineService.types()[0] + "." + scriptType + "." + scriptContext);
+                    ScriptMode scriptMode = engineSettings.get(scriptEngineService.types()[0] + "." + scriptType + "." + scriptContext.getKey());
                     if (scriptMode == null) {
-                        scriptMode = scriptContextSettings.get(scriptContext);
+                        scriptMode = scriptContextSettings.get(scriptContext.getKey());
                     }
                     if (scriptMode == null) {
                         scriptMode = scriptSourceSettings.get(scriptType);
@@ -318,6 +337,28 @@ public class ScriptServiceTests extends ElasticsearchTestCase {
         }
     }
 
+    @Test
+    public void testCompileNonRegisteredContext() throws IOException {
+        buildScriptService(ImmutableSettings.EMPTY);
+        String pluginName;
+        String unknownContext;
+        do {
+            pluginName = randomAsciiOfLength(randomIntBetween(1, 10));
+            unknownContext = randomAsciiOfLength(randomIntBetween(1, 30));
+        } while(scriptContextRegistry.isSupportedContext(new ScriptContext.Plugin(pluginName, unknownContext)));
+
+        for (ScriptEngineService scriptEngineService : scriptEngineServices) {
+            for (String type : scriptEngineService.types()) {
+                try {
+                    scriptService.compile(type, "test", randomFrom(ScriptType.values()), new ScriptContext.Plugin(pluginName, unknownContext));
+                    fail("script compilation should have been rejected");
+                } catch(ElasticsearchIllegalArgumentException e) {
+                    assertThat(e.getMessage(), containsString("script context [" + pluginName + "_" + unknownContext + "] not supported"));
+                }
+            }
+        }
+    }
+
     private void createFileScripts(String... langs) throws IOException {
         for (String lang : langs) {
             Path scriptPath = scriptsFilePath.resolve("file_script." + lang);