瀏覽代碼

Add a Painless Context REST API (#39382)

This PR adds an internal REST API for querying context information about 
Painless whitelists.

Commands include the following:
GET /_scripts/painless/_context -- retrieves a list of contexts
GET /_scripts/painless/_context?context=%name% retrieves all available 
information about the API for this specific context
Jack Conradson 6 年之前
父節點
當前提交
d80367644b
共有 16 個文件被更改,包括 1519 次插入7 次删除
  1. 1 0
      client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java
  2. 20 5
      modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessPlugin.java
  3. 12 2
      modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java
  4. 236 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextAction.java
  5. 147 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextClassBindingInfo.java
  6. 186 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextClassInfo.java
  7. 117 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextConstructorInfo.java
  8. 122 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextFieldInfo.java
  9. 187 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextInfo.java
  10. 138 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextInstanceBindingInfo.java
  11. 137 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextMethodInfo.java
  12. 12 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java
  13. 1 0
      modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java
  14. 164 0
      modules/lang-painless/src/test/java/org/elasticsearch/painless/action/ContextInfoTests.java
  15. 22 0
      modules/lang-painless/src/test/resources/rest-api-spec/test/painless/71_context_api.yml
  16. 17 0
      rest-api-spec/src/main/resources/rest-api-spec/api/scripts_painless_context.json

+ 1 - 0
client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java

@@ -713,6 +713,7 @@ public class RestHighLevelClientTests extends ESTestCase {
             "nodes.hot_threads",
             "nodes.usage",
             "nodes.reload_secure_settings",
+            "scripts_painless_context",
             "search_shards",
         };
         List<String> booleanReturnMethods = Arrays.asList(

+ 20 - 5
modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessPlugin.java

@@ -20,15 +20,18 @@
 package org.elasticsearch.painless;
 
 
+import org.apache.lucene.util.SetOnce;
 import org.elasticsearch.action.ActionRequest;
 import org.elasticsearch.action.ActionResponse;
 import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
 import org.elasticsearch.cluster.node.DiscoveryNodes;
+import org.elasticsearch.common.inject.Module;
 import org.elasticsearch.common.settings.ClusterSettings;
 import org.elasticsearch.common.settings.IndexScopedSettings;
 import org.elasticsearch.common.settings.Setting;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.common.settings.SettingsFilter;
+import org.elasticsearch.painless.action.PainlessContextAction;
 import org.elasticsearch.painless.spi.PainlessExtension;
 import org.elasticsearch.painless.spi.Whitelist;
 import org.elasticsearch.painless.spi.WhitelistLoader;
@@ -81,6 +84,8 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin, Extens
         whitelists = map;
     }
 
+    private final SetOnce<PainlessScriptEngine> painlessScriptEngine = new SetOnce<>();
+
     @Override
     public ScriptEngine getScriptEngine(Settings settings, Collection<ScriptContext<?>> contexts) {
         Map<ScriptContext<?>, List<Whitelist>> contextsWithWhitelists = new HashMap<>();
@@ -92,7 +97,13 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin, Extens
             }
             contextsWithWhitelists.put(context, contextWhitelists);
         }
-        return new PainlessScriptEngine(settings, contextsWithWhitelists);
+        painlessScriptEngine.set(new PainlessScriptEngine(settings, contextsWithWhitelists));
+        return painlessScriptEngine.get();
+    }
+
+    @Override
+    public Collection<Module> createGuiceModules() {
+        return Collections.singleton(b -> b.bind(PainlessScriptEngine.class).toInstance(painlessScriptEngine.get()));
     }
 
     @Override
@@ -118,9 +129,10 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin, Extens
 
     @Override
     public List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {
-        return Collections.singletonList(
-            new ActionHandler<>(PainlessExecuteAction.INSTANCE, PainlessExecuteAction.TransportAction.class)
-        );
+        List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> actions = new ArrayList<>();
+        actions.add(new ActionHandler<>(PainlessExecuteAction.INSTANCE, PainlessExecuteAction.TransportAction.class));
+        actions.add(new ActionHandler<>(PainlessContextAction.INSTANCE, PainlessContextAction.TransportAction.class));
+        return actions;
     }
 
     @Override
@@ -128,6 +140,9 @@ public final class PainlessPlugin extends Plugin implements ScriptPlugin, Extens
                                              IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter,
                                              IndexNameExpressionResolver indexNameExpressionResolver,
                                              Supplier<DiscoveryNodes> nodesInCluster) {
-        return Collections.singletonList(new PainlessExecuteAction.RestAction(settings, restController));
+        List<RestHandler> handlers = new ArrayList<>();
+        handlers.add(new PainlessExecuteAction.RestAction(settings, restController));
+        handlers.add(new PainlessContextAction.RestAction(settings, restController));
+        return handlers;
     }
 }

+ 12 - 2
modules/lang-painless/src/main/java/org/elasticsearch/painless/PainlessScriptEngine.java

@@ -22,6 +22,7 @@ package org.elasticsearch.painless;
 import org.elasticsearch.SpecialPermission;
 import org.elasticsearch.common.settings.Settings;
 import org.elasticsearch.painless.Compiler.Loader;
+import org.elasticsearch.painless.lookup.PainlessLookup;
 import org.elasticsearch.painless.lookup.PainlessLookupBuilder;
 import org.elasticsearch.painless.spi.Whitelist;
 import org.elasticsearch.script.ScriptContext;
@@ -82,6 +83,7 @@ public final class PainlessScriptEngine implements ScriptEngine {
     private final CompilerSettings defaultCompilerSettings = new CompilerSettings();
 
     private final Map<ScriptContext<?>, Compiler> contextsToCompilers;
+    private final Map<ScriptContext<?>, PainlessLookup> contextsToLookups;
 
     /**
      * Constructor.
@@ -91,14 +93,22 @@ public final class PainlessScriptEngine implements ScriptEngine {
         defaultCompilerSettings.setRegexesEnabled(CompilerSettings.REGEX_ENABLED.get(settings));
 
         Map<ScriptContext<?>, Compiler> contextsToCompilers = new HashMap<>();
+        Map<ScriptContext<?>, PainlessLookup> contextsToLookups = new HashMap<>();
 
         for (Map.Entry<ScriptContext<?>, List<Whitelist>> entry : contexts.entrySet()) {
             ScriptContext<?> context = entry.getKey();
-            contextsToCompilers.put(context, new Compiler(context.instanceClazz, context.factoryClazz, context.statefulFactoryClazz,
-                    PainlessLookupBuilder.buildFromWhitelists(entry.getValue())));
+            PainlessLookup lookup = PainlessLookupBuilder.buildFromWhitelists(entry.getValue());
+            contextsToCompilers.put(context,
+                    new Compiler(context.instanceClazz, context.factoryClazz, context.statefulFactoryClazz, lookup));
+            contextsToLookups.put(context, lookup);
         }
 
         this.contextsToCompilers = Collections.unmodifiableMap(contextsToCompilers);
+        this.contextsToLookups = Collections.unmodifiableMap(contextsToLookups);
+    }
+
+    public Map<ScriptContext<?>, PainlessLookup> getContextsToLookups() {
+        return contextsToLookups;
     }
 
     /**

+ 236 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextAction.java

@@ -0,0 +1,236 @@
+/*
+ * 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.painless.action;
+
+import org.elasticsearch.action.Action;
+import org.elasticsearch.action.ActionListener;
+import org.elasticsearch.action.ActionRequest;
+import org.elasticsearch.action.ActionRequestValidationException;
+import org.elasticsearch.action.ActionResponse;
+import org.elasticsearch.action.support.ActionFilters;
+import org.elasticsearch.action.support.HandledTransportAction;
+import org.elasticsearch.client.node.NodeClient;
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.inject.Inject;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.painless.PainlessScriptEngine;
+import org.elasticsearch.painless.lookup.PainlessLookup;
+import org.elasticsearch.rest.BaseRestHandler;
+import org.elasticsearch.rest.RestController;
+import org.elasticsearch.rest.RestRequest;
+import org.elasticsearch.rest.action.RestToXContentListener;
+import org.elasticsearch.script.ScriptContext;
+import org.elasticsearch.tasks.Task;
+import org.elasticsearch.transport.TransportService;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+import static org.elasticsearch.rest.RestRequest.Method.GET;
+
+/**
+ * Internal REST API for querying context information about Painless whitelists.
+ * Commands include the following:
+ * <ul>
+ *     <li> GET /_scripts/painless/_context -- retrieves a list of contexts </li>
+ *     <li> GET /_scripts/painless/_context?context=%name% --
+ *     retrieves all available information about the API for this specific context</li>
+ * </ul>
+ */
+public class PainlessContextAction extends Action<PainlessContextAction.Response> {
+
+    public static final PainlessContextAction INSTANCE = new PainlessContextAction();
+    private static final String NAME = "cluster:admin/scripts/painless/context";
+
+    private static final String SCRIPT_CONTEXT_NAME_PARAM = "context";
+
+    private PainlessContextAction() {
+        super(NAME);
+    }
+
+    @Override
+    public Response newResponse() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Writeable.Reader<Response> getResponseReader() {
+        return Response::new;
+    }
+
+    public static class Request extends ActionRequest {
+
+        private String scriptContextName;
+
+        public Request() {
+            scriptContextName = null;
+        }
+
+        public Request(StreamInput in) throws IOException {
+            super(in);
+            scriptContextName = in.readString();
+        }
+
+        public void setScriptContextName(String scriptContextName) {
+            this.scriptContextName = scriptContextName;
+        }
+
+        public String getScriptContextName() {
+            return scriptContextName;
+        }
+
+        @Override
+        public ActionRequestValidationException validate() {
+            return null;
+        }
+
+        @Override
+        public void readFrom(StreamInput in) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            super.writeTo(out);
+            out.writeString(scriptContextName);
+        }
+    }
+
+    public static class Response extends ActionResponse implements ToXContentObject {
+
+        public static final ParseField CONTEXTS = new ParseField("contexts");
+
+        private final List<String> scriptContextNames;
+        private final PainlessContextInfo painlessContextInfo;
+
+        public Response(List<String> scriptContextNames, PainlessContextInfo painlessContextInfo) {
+            Objects.requireNonNull(scriptContextNames);
+            scriptContextNames = new ArrayList<>(scriptContextNames);
+            scriptContextNames.sort(String::compareTo);
+            this.scriptContextNames = Collections.unmodifiableList(scriptContextNames);
+            this.painlessContextInfo = painlessContextInfo;
+        }
+
+        public Response(StreamInput in) throws IOException {
+            super(in);
+            scriptContextNames = in.readStringList();
+            painlessContextInfo = in.readOptionalWriteable(PainlessContextInfo::new);
+        }
+
+        @Override
+        public void readFrom(StreamInput in) {
+            throw new UnsupportedOperationException();
+        }
+
+        @Override
+        public void writeTo(StreamOutput out) throws IOException {
+            super.writeTo(out);
+            out.writeStringCollection(scriptContextNames);
+            out.writeOptionalWriteable(painlessContextInfo);
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            if (painlessContextInfo == null) {
+                builder.startObject();
+                builder.field(CONTEXTS.getPreferredName(), scriptContextNames);
+                builder.endObject();
+            } else {
+                painlessContextInfo.toXContent(builder, params);
+            }
+
+            return builder;
+        }
+    }
+
+    public static class TransportAction extends HandledTransportAction<Request, Response> {
+
+        private final PainlessScriptEngine painlessScriptEngine;
+
+        @Inject
+        public TransportAction(TransportService transportService, ActionFilters actionFilters, PainlessScriptEngine painlessScriptEngine) {
+            super(NAME, transportService, actionFilters, (Writeable.Reader<Request>)Request::new);
+            this.painlessScriptEngine = painlessScriptEngine;
+        }
+
+        @Override
+        protected void doExecute(Task task, Request request, ActionListener<Response> listener) {
+            List<String> scriptContextNames;
+            PainlessContextInfo painlessContextInfo;
+
+            if (request.scriptContextName == null) {
+                scriptContextNames =
+                        painlessScriptEngine.getContextsToLookups().keySet().stream().map(v -> v.name).collect(Collectors.toList());
+                painlessContextInfo = null;
+            } else {
+                ScriptContext<?> scriptContext = null;
+                PainlessLookup painlessLookup = null;
+
+                for (Map.Entry<ScriptContext<?>, PainlessLookup> contextLookupEntry :
+                        painlessScriptEngine.getContextsToLookups().entrySet()) {
+                    if (contextLookupEntry.getKey().name.equals(request.getScriptContextName())) {
+                        scriptContext = contextLookupEntry.getKey();
+                        painlessLookup = contextLookupEntry.getValue();
+                        break;
+                    }
+                }
+
+                if (scriptContext == null || painlessLookup == null) {
+                    throw new IllegalArgumentException("script context [" + request.getScriptContextName() + "] not found");
+                }
+
+                scriptContextNames = Collections.emptyList();
+                painlessContextInfo = new PainlessContextInfo(scriptContext, painlessLookup);
+            }
+
+            listener.onResponse(new Response(scriptContextNames, painlessContextInfo));
+        }
+    }
+
+    public static class RestAction extends BaseRestHandler {
+
+        public RestAction(Settings settings, RestController controller) {
+            super(settings);
+            controller.registerHandler(GET, "/_scripts/painless/_context", this);
+        }
+
+        @Override
+        public String getName() {
+            return "_scripts_painless_context";
+        }
+
+        @Override
+        protected RestChannelConsumer prepareRequest(RestRequest restRequest, NodeClient client) {
+            Request request = new Request();
+            request.setScriptContextName(restRequest.param(SCRIPT_CONTEXT_NAME_PARAM));
+            return channel -> client.executeLocally(INSTANCE, request, new RestToXContentListener<>(channel));
+        }
+    }
+}

+ 147 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextClassBindingInfo.java

@@ -0,0 +1,147 @@
+/*
+ * 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.painless.action;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.painless.lookup.PainlessClassBinding;
+import org.elasticsearch.painless.lookup.PainlessLookupUtility;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class PainlessContextClassBindingInfo implements Writeable, ToXContentObject {
+
+    public static final ParseField DECLARING = new ParseField("declaring");
+    public static final ParseField NAME = new ParseField("name");
+    public static final ParseField RTN = new ParseField("return");
+    public static final ParseField READ_ONLY = new ParseField("read_only");
+    public static final ParseField PARAMETERS = new ParseField("parameters");
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<PainlessContextClassBindingInfo, Void> PARSER = new ConstructingObjectParser<>(
+            PainlessContextClassBindingInfo.class.getCanonicalName(),
+            (v) ->
+                    new PainlessContextClassBindingInfo(
+                            (String)v[0],
+                            (String)v[1],
+                            (String)v[2],
+                            (int)v[3],
+                            (List<String>)v[4]
+                    )
+    );
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), DECLARING);
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME);
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), RTN);
+        PARSER.declareInt(ConstructingObjectParser.constructorArg(), READ_ONLY);
+        PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), PARAMETERS);
+    }
+
+    private final String declaring;
+    private final String name;
+    private final String rtn;
+    private final int readOnly;
+    private final List<String> parameters;
+
+    public PainlessContextClassBindingInfo(PainlessClassBinding painlessClassBinding) {
+        this(
+                painlessClassBinding.javaMethod.getDeclaringClass().getName(),
+                painlessClassBinding.javaMethod.getName(),
+                painlessClassBinding.returnType.getName(),
+                painlessClassBinding.javaConstructor.getParameterCount(),
+                painlessClassBinding.typeParameters.stream().map(Class::getName).collect(Collectors.toList())
+        );
+    }
+
+    public PainlessContextClassBindingInfo(String declaring, String name, String rtn, int readOnly, List<String> parameters) {
+        this.declaring = Objects.requireNonNull(declaring);
+        this.name = Objects.requireNonNull(name);
+        this.rtn = Objects.requireNonNull(rtn);
+        this.readOnly = readOnly;
+        this.parameters = Collections.unmodifiableList(Objects.requireNonNull(parameters));
+    }
+
+    public PainlessContextClassBindingInfo(StreamInput in) throws IOException {
+        declaring = in.readString();
+        name = in.readString();
+        rtn = in.readString();
+        readOnly = in.readInt();
+        parameters = Collections.unmodifiableList(in.readStringList());
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(declaring);
+        out.writeString(name);
+        out.writeString(rtn);
+        out.writeInt(readOnly);
+        out.writeStringCollection(parameters);
+    }
+
+    public static PainlessContextClassBindingInfo fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
+        builder.startObject();
+        builder.field(DECLARING.getPreferredName(), declaring);
+        builder.field(NAME.getPreferredName(), name);
+        builder.field(RTN.getPreferredName(), rtn);
+        builder.field(READ_ONLY.getPreferredName(), readOnly);
+        builder.field(PARAMETERS.getPreferredName(), parameters);
+        builder.endObject();
+
+        return builder;
+    }
+
+    public String getSortValue() {
+        return PainlessLookupUtility.buildPainlessMethodKey(name, parameters.size());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PainlessContextClassBindingInfo that = (PainlessContextClassBindingInfo) o;
+        return readOnly == that.readOnly &&
+                Objects.equals(declaring, that.declaring) &&
+                Objects.equals(name, that.name) &&
+                Objects.equals(rtn, that.rtn) &&
+                Objects.equals(parameters, that.parameters);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(declaring, name, rtn, readOnly, parameters);
+    }
+}

+ 186 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextClassInfo.java

@@ -0,0 +1,186 @@
+/*
+ * 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.painless.action;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.painless.lookup.PainlessClass;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class PainlessContextClassInfo implements Writeable, ToXContentObject {
+
+    public static final ParseField NAME = new ParseField("name");
+    public static final ParseField IMPORTED = new ParseField("imported");
+    public static final ParseField CONSTRUCTORS = new ParseField("constructors");
+    public static final ParseField STATIC_METHODS = new ParseField("static_methods");
+    public static final ParseField METHODS = new ParseField("methods");
+    public static final ParseField STATIC_FIELDS = new ParseField("static_fields");
+    public static final ParseField FIELDS = new ParseField("fields");
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<PainlessContextClassInfo, Void> PARSER = new ConstructingObjectParser<>(
+            PainlessContextClassInfo.class.getCanonicalName(),
+            (v) ->
+                    new PainlessContextClassInfo(
+                            (String)v[0],
+                            (boolean)v[1],
+                            (List<PainlessContextConstructorInfo>)v[2],
+                            (List<PainlessContextMethodInfo>)v[3],
+                            (List<PainlessContextMethodInfo>)v[4],
+                            (List<PainlessContextFieldInfo>)v[5],
+                            (List<PainlessContextFieldInfo>)v[6]
+                    )
+    );
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME);
+        PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), IMPORTED);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+                (p, c) -> PainlessContextConstructorInfo.fromXContent(p), CONSTRUCTORS);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+                (p, c) -> PainlessContextMethodInfo.fromXContent(p), STATIC_METHODS);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+                (p, c) -> PainlessContextMethodInfo.fromXContent(p), METHODS);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+                (p, c) -> PainlessContextFieldInfo.fromXContent(p), STATIC_FIELDS);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+                (p, c) -> PainlessContextFieldInfo.fromXContent(p), FIELDS);
+    }
+
+    private final String name;
+    private final boolean imported;
+    private final List<PainlessContextConstructorInfo> constructors;
+    private final List<PainlessContextMethodInfo> staticMethods;
+    private final List<PainlessContextMethodInfo> methods;
+    private final List<PainlessContextFieldInfo> staticFields;
+    private final List<PainlessContextFieldInfo> fields;
+
+    public PainlessContextClassInfo(Class<?> javaClass, boolean imported, PainlessClass painlessClass) {
+        this(
+                javaClass.getName(),
+                imported,
+                painlessClass.constructors.values().stream().map(PainlessContextConstructorInfo::new).collect(Collectors.toList()),
+                painlessClass.staticMethods.values().stream().map(PainlessContextMethodInfo::new).collect(Collectors.toList()),
+                painlessClass.methods.values().stream().map(PainlessContextMethodInfo::new).collect(Collectors.toList()),
+                painlessClass.staticFields.values().stream().map(PainlessContextFieldInfo::new).collect(Collectors.toList()),
+                painlessClass.fields.values().stream().map(PainlessContextFieldInfo::new).collect(Collectors.toList())
+        );
+    }
+
+    public PainlessContextClassInfo(String name, boolean imported,
+            List<PainlessContextConstructorInfo> constructors,
+            List<PainlessContextMethodInfo> staticMethods, List<PainlessContextMethodInfo> methods,
+            List<PainlessContextFieldInfo> staticFields, List<PainlessContextFieldInfo> fields) {
+
+        this.name = Objects.requireNonNull(name);
+        this.imported = imported;
+        constructors = new ArrayList<>(Objects.requireNonNull(constructors));
+        constructors.sort(Comparator.comparing(PainlessContextConstructorInfo::getSortValue));
+        this.constructors = Collections.unmodifiableList(constructors);
+        staticMethods = new ArrayList<>(Objects.requireNonNull(staticMethods));
+        staticMethods.sort(Comparator.comparing(PainlessContextMethodInfo::getSortValue));
+        this.staticMethods = Collections.unmodifiableList(staticMethods);
+        methods = new ArrayList<>(Objects.requireNonNull(methods));
+        methods.sort(Comparator.comparing(PainlessContextMethodInfo::getSortValue));
+        this.methods = Collections.unmodifiableList(methods);
+        staticFields = new ArrayList<>(Objects.requireNonNull(staticFields));
+        staticFields.sort(Comparator.comparing(PainlessContextFieldInfo::getSortValue));
+        this.staticFields = Collections.unmodifiableList(staticFields);
+        fields = new ArrayList<>(Objects.requireNonNull(fields));
+        fields.sort(Comparator.comparing(PainlessContextFieldInfo::getSortValue));
+        this.fields = Collections.unmodifiableList(fields);
+    }
+
+    public PainlessContextClassInfo(StreamInput in) throws IOException {
+        name = in.readString();
+        imported = in.readBoolean();
+        constructors = Collections.unmodifiableList(in.readList(PainlessContextConstructorInfo::new));
+        staticMethods = Collections.unmodifiableList(in.readList(PainlessContextMethodInfo::new));
+        methods = Collections.unmodifiableList(in.readList(PainlessContextMethodInfo::new));
+        staticFields = Collections.unmodifiableList(in.readList(PainlessContextFieldInfo::new));
+        fields = Collections.unmodifiableList(in.readList(PainlessContextFieldInfo::new));
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(name);
+        out.writeBoolean(imported);
+        out.writeList(constructors);
+        out.writeList(staticMethods);
+        out.writeList(methods);
+        out.writeList(staticFields);
+        out.writeList(fields);
+    }
+
+    public static PainlessContextClassInfo fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.field(NAME.getPreferredName(), name);
+        builder.field(IMPORTED.getPreferredName(), imported);
+        builder.field(CONSTRUCTORS.getPreferredName(), constructors);
+        builder.field(STATIC_METHODS.getPreferredName(), staticMethods);
+        builder.field(METHODS.getPreferredName(), methods);
+        builder.field(STATIC_FIELDS.getPreferredName(), staticFields);
+        builder.field(FIELDS.getPreferredName(), fields);
+        builder.endObject();
+
+        return builder;
+    }
+
+    public String getSortValue() {
+        return name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PainlessContextClassInfo that = (PainlessContextClassInfo) o;
+        return imported == that.imported &&
+                Objects.equals(name, that.name) &&
+                Objects.equals(constructors, that.constructors) &&
+                Objects.equals(staticMethods, that.staticMethods) &&
+                Objects.equals(methods, that.methods) &&
+                Objects.equals(staticFields, that.staticFields) &&
+                Objects.equals(fields, that.fields);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, imported, constructors, staticMethods, methods, staticFields, fields);
+    }
+}

+ 117 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextConstructorInfo.java

@@ -0,0 +1,117 @@
+/*
+ * 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.painless.action;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.painless.lookup.PainlessConstructor;
+import org.elasticsearch.painless.lookup.PainlessLookupUtility;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class PainlessContextConstructorInfo implements Writeable, ToXContentObject {
+
+    public static final ParseField DECLARING = new ParseField("declaring");
+    public static final ParseField PARAMETERS = new ParseField("parameters");
+
+    private final String declaring;
+    private final List<String> parameters;
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<PainlessContextConstructorInfo, Void> PARSER = new ConstructingObjectParser<>(
+            PainlessContextConstructorInfo.class.getCanonicalName(),
+            (v) ->
+                    new PainlessContextConstructorInfo(
+                            (String)v[0],
+                            (List<String>)v[1]
+                    )
+    );
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), DECLARING);
+        PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), PARAMETERS);
+    }
+
+    public PainlessContextConstructorInfo(PainlessConstructor painlessConstructor) {
+        this (
+                painlessConstructor.javaConstructor.getDeclaringClass().getName(),
+                painlessConstructor.typeParameters.stream().map(Class::getName).collect(Collectors.toList())
+        );
+    }
+
+    public PainlessContextConstructorInfo(String declaring, List<String> parameters) {
+        this.declaring = Objects.requireNonNull(declaring);
+        this.parameters = Collections.unmodifiableList(Objects.requireNonNull(parameters));
+    }
+
+    public PainlessContextConstructorInfo(StreamInput in) throws IOException {
+        declaring = in.readString();
+        parameters = Collections.unmodifiableList(in.readStringList());
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(declaring);
+        out.writeStringCollection(parameters);
+    }
+
+    public static PainlessContextConstructorInfo fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
+        builder.startObject();
+        builder.field(DECLARING.getPreferredName(), declaring);
+        builder.field(PARAMETERS.getPreferredName(), parameters);
+        builder.endObject();
+
+        return builder;
+    }
+
+    public String getSortValue() {
+        return PainlessLookupUtility.buildPainlessConstructorKey(parameters.size());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PainlessContextConstructorInfo that = (PainlessContextConstructorInfo) o;
+        return Objects.equals(declaring, that.declaring) &&
+                Objects.equals(parameters, that.parameters);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(declaring, parameters);
+    }
+}

+ 122 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextFieldInfo.java

@@ -0,0 +1,122 @@
+/*
+ * 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.painless.action;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.painless.lookup.PainlessField;
+import org.elasticsearch.painless.lookup.PainlessLookupUtility;
+
+import java.io.IOException;
+import java.util.Objects;
+
+public class PainlessContextFieldInfo implements Writeable, ToXContentObject {
+
+    public static final ParseField DECLARING = new ParseField("declaring");
+    public static final ParseField NAME = new ParseField("name");
+    public static final ParseField TYPE = new ParseField("type");
+
+    private static final ConstructingObjectParser<PainlessContextFieldInfo, Void> PARSER = new ConstructingObjectParser<>(
+            PainlessContextFieldInfo.class.getCanonicalName(),
+            (v) ->
+                    new PainlessContextFieldInfo(
+                        (String)v[0],
+                        (String)v[1],
+                        (String)v[2]
+                    )
+    );
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), DECLARING);
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME);
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), TYPE);
+    }
+
+    private final String declaring;
+    private final String name;
+    private final String type;
+
+    public PainlessContextFieldInfo(PainlessField painlessField) {
+        this(
+                painlessField.javaField.getDeclaringClass().getName(),
+                painlessField.javaField.getName(),
+                painlessField.typeParameter.getName()
+        );
+    }
+
+    public PainlessContextFieldInfo(String declaring, String name, String type) {
+        this.declaring = Objects.requireNonNull(declaring);
+        this.name = Objects.requireNonNull(name);
+        this.type = Objects.requireNonNull(type);
+    }
+
+    public PainlessContextFieldInfo(StreamInput in) throws IOException {
+        declaring = in.readString();
+        name = in.readString();
+        type = in.readString();
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(declaring);
+        out.writeString(name);
+        out.writeString(type);
+    }
+
+    public static PainlessContextFieldInfo fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.field(DECLARING.getPreferredName(), declaring);
+        builder.field(NAME.getPreferredName(), name);
+        builder.field(TYPE.getPreferredName(), type);
+        builder.endObject();
+
+        return builder;
+    }
+
+    public String getSortValue() {
+        return PainlessLookupUtility.buildPainlessFieldKey(name);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PainlessContextFieldInfo that = (PainlessContextFieldInfo) o;
+        return Objects.equals(declaring, that.declaring) &&
+                Objects.equals(name, that.name) &&
+                Objects.equals(type, that.type);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(declaring, name, type);
+    }
+}

+ 187 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextInfo.java

@@ -0,0 +1,187 @@
+/*
+ * 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.painless.action;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.painless.lookup.PainlessClassBinding;
+import org.elasticsearch.painless.lookup.PainlessInstanceBinding;
+import org.elasticsearch.painless.lookup.PainlessLookup;
+import org.elasticsearch.painless.lookup.PainlessMethod;
+import org.elasticsearch.script.ScriptContext;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class PainlessContextInfo implements Writeable, ToXContentObject {
+
+    public static final ParseField NAME = new ParseField("name");
+    public static final ParseField CLASSES = new ParseField("classes");
+    public static final ParseField IMPORTED_METHODS = new ParseField("imported_methods");
+    public static final ParseField CLASS_BINDINGS = new ParseField("class_bindings");
+    public static final ParseField INSTANCE_BINDINGS = new ParseField("instance_bindings");
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<PainlessContextInfo, Void> PARSER = new ConstructingObjectParser<>(
+            PainlessContextInfo.class.getCanonicalName(),
+            (v) ->
+                    new PainlessContextInfo(
+                            (String)v[0],
+                            (List<PainlessContextClassInfo>)v[1],
+                            (List<PainlessContextMethodInfo>)v[2],
+                            (List<PainlessContextClassBindingInfo>)v[3],
+                            (List<PainlessContextInstanceBindingInfo>)v[4]
+                    )
+    );
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+                (p, c) -> PainlessContextClassInfo.fromXContent(p), CLASSES);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+                (p, c) -> PainlessContextMethodInfo.fromXContent(p), IMPORTED_METHODS);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+                (p, c) -> PainlessContextClassBindingInfo.fromXContent(p), CLASS_BINDINGS);
+        PARSER.declareObjectArray(ConstructingObjectParser.constructorArg(),
+                (p, c) -> PainlessContextInstanceBindingInfo.fromXContent(p), INSTANCE_BINDINGS);
+    }
+
+    private final String name;
+    private final List<PainlessContextClassInfo> classes;
+    private final List<PainlessContextMethodInfo> importedMethods;
+    private final List<PainlessContextClassBindingInfo> classBindings;
+    private final List<PainlessContextInstanceBindingInfo> instanceBindings;
+
+    public PainlessContextInfo(ScriptContext<?> scriptContext, PainlessLookup painlessLookup) {
+        this(
+                scriptContext.name,
+                painlessLookup.getClasses().stream().map(
+                        javaClass -> new PainlessContextClassInfo(
+                                javaClass,
+                                javaClass == painlessLookup.canonicalTypeNameToType(
+                                        javaClass.getName().substring(javaClass.getName().lastIndexOf('.') + 1).replace('$', '.')),
+                                painlessLookup.lookupPainlessClass(javaClass))
+                ).collect(Collectors.toList()),
+                painlessLookup.getImportedPainlessMethodsKeys().stream().map(importedPainlessMethodKey -> {
+                    String[] split = importedPainlessMethodKey.split("/");
+                    String importedPainlessMethodName = split[0];
+                    int importedPainlessMethodArity = Integer.parseInt(split[1]);
+                    PainlessMethod importedPainlessMethod =
+                            painlessLookup.lookupImportedPainlessMethod(importedPainlessMethodName, importedPainlessMethodArity);
+                    return new PainlessContextMethodInfo(importedPainlessMethod);
+                }).collect(Collectors.toList()),
+                painlessLookup.getPainlessClassBindingsKeys().stream().map(painlessClassBindingKey -> {
+                    String[] split = painlessClassBindingKey.split("/");
+                    String painlessClassBindingName = split[0];
+                    int painlessClassBindingArity = Integer.parseInt(split[1]);
+                    PainlessClassBinding painlessClassBinding =
+                            painlessLookup.lookupPainlessClassBinding(painlessClassBindingName, painlessClassBindingArity);
+                    return new PainlessContextClassBindingInfo(painlessClassBinding);
+                }).collect(Collectors.toList()),
+                painlessLookup.getPainlessInstanceBindingsKeys().stream().map(painlessInstanceBindingKey -> {
+                    String[] split = painlessInstanceBindingKey.split("/");
+                    String painlessInstanceBindingName = split[0];
+                    int painlessInstanceBindingArity = Integer.parseInt(split[1]);
+                    PainlessInstanceBinding painlessInstanceBinding =
+                            painlessLookup.lookupPainlessInstanceBinding(painlessInstanceBindingName, painlessInstanceBindingArity);
+                    return new PainlessContextInstanceBindingInfo(painlessInstanceBinding);
+                }).collect(Collectors.toList())
+        );
+    }
+
+    public PainlessContextInfo(String name, List<PainlessContextClassInfo> classes, List<PainlessContextMethodInfo> importedMethods,
+            List<PainlessContextClassBindingInfo> classBindings, List<PainlessContextInstanceBindingInfo> instanceBindings) {
+        this.name = Objects.requireNonNull(name);
+        classes = new ArrayList<>(Objects.requireNonNull(classes));
+        classes.sort(Comparator.comparing(PainlessContextClassInfo::getSortValue));
+        this.classes = Collections.unmodifiableList(classes);
+        importedMethods = new ArrayList<>(Objects.requireNonNull(importedMethods));
+        importedMethods.sort(Comparator.comparing(PainlessContextMethodInfo::getSortValue));
+        this.importedMethods = Collections.unmodifiableList(importedMethods);
+        classBindings = new ArrayList<>(Objects.requireNonNull(classBindings));
+        classBindings.sort(Comparator.comparing(PainlessContextClassBindingInfo::getSortValue));
+        this.classBindings = Collections.unmodifiableList(classBindings);
+        instanceBindings = new ArrayList<>(Objects.requireNonNull(instanceBindings));
+        instanceBindings.sort(Comparator.comparing(PainlessContextInstanceBindingInfo::getSortValue));
+        this.instanceBindings = Collections.unmodifiableList(instanceBindings);
+    }
+
+    public PainlessContextInfo(StreamInput in) throws IOException {
+        name = in.readString();
+        classes = Collections.unmodifiableList(in.readList(PainlessContextClassInfo::new));
+        importedMethods = Collections.unmodifiableList(in.readList(PainlessContextMethodInfo::new));
+        classBindings = Collections.unmodifiableList(in.readList(PainlessContextClassBindingInfo::new));
+        instanceBindings = Collections.unmodifiableList(in.readList(PainlessContextInstanceBindingInfo::new));
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(name);
+        out.writeList(classes);
+        out.writeList(importedMethods);
+        out.writeList(classBindings);
+        out.writeList(instanceBindings);
+    }
+
+    public static PainlessContextInfo fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+        builder.startObject();
+        builder.field(NAME.getPreferredName(), name);
+        builder.field(CLASSES.getPreferredName(), classes);
+        builder.field(IMPORTED_METHODS.getPreferredName(), importedMethods);
+        builder.field(CLASS_BINDINGS.getPreferredName(), classBindings);
+        builder.field(INSTANCE_BINDINGS.getPreferredName(), instanceBindings);
+        builder.endObject();
+
+        return builder;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PainlessContextInfo that = (PainlessContextInfo) o;
+        return Objects.equals(name, that.name) &&
+                Objects.equals(classes, that.classes) &&
+                Objects.equals(importedMethods, that.importedMethods) &&
+                Objects.equals(classBindings, that.classBindings) &&
+                Objects.equals(instanceBindings, that.instanceBindings);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, classes, importedMethods, classBindings, instanceBindings);
+    }
+}

+ 138 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextInstanceBindingInfo.java

@@ -0,0 +1,138 @@
+/*
+ * 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.painless.action;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.painless.lookup.PainlessInstanceBinding;
+import org.elasticsearch.painless.lookup.PainlessLookupUtility;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class PainlessContextInstanceBindingInfo implements Writeable, ToXContentObject {
+
+    public static final ParseField DECLARING = new ParseField("declaring");
+    public static final ParseField NAME = new ParseField("name");
+    public static final ParseField RTN = new ParseField("return");
+    public static final ParseField PARAMETERS = new ParseField("parameters");
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<PainlessContextInstanceBindingInfo, Void> PARSER = new ConstructingObjectParser<>(
+            PainlessContextInstanceBindingInfo.class.getCanonicalName(),
+            (v) ->
+                    new PainlessContextInstanceBindingInfo(
+                            (String)v[0],
+                            (String)v[1],
+                            (String)v[2],
+                            (List<String>)v[3]
+                    )
+    );
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), DECLARING);
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME);
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), RTN);
+        PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), PARAMETERS);
+    }
+
+    private final String declaring;
+    private final String name;
+    private final String rtn;
+    private final List<String> parameters;
+
+    public PainlessContextInstanceBindingInfo(PainlessInstanceBinding painlessInstanceBinding) {
+        this(
+                painlessInstanceBinding.javaMethod.getDeclaringClass().getName(),
+                painlessInstanceBinding.javaMethod.getName(),
+                painlessInstanceBinding.returnType.getName(),
+                painlessInstanceBinding.typeParameters.stream().map(Class::getName).collect(Collectors.toList())
+
+        );
+    }
+
+    public PainlessContextInstanceBindingInfo(String declaring, String name, String rtn, List<String> parameters) {
+        this.declaring = Objects.requireNonNull(declaring);
+        this.name = Objects.requireNonNull(name);
+        this.rtn = Objects.requireNonNull(rtn);
+        this.parameters = Collections.unmodifiableList(Objects.requireNonNull(parameters));
+    }
+
+    public PainlessContextInstanceBindingInfo(StreamInput in) throws IOException {
+        declaring = in.readString();
+        name = in.readString();
+        rtn = in.readString();
+        parameters = Collections.unmodifiableList(in.readStringList());
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(declaring);
+        out.writeString(name);
+        out.writeString(rtn);
+        out.writeStringCollection(parameters);
+    }
+
+    public static PainlessContextInstanceBindingInfo fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
+        builder.startObject();
+        builder.field(DECLARING.getPreferredName(), declaring);
+        builder.field(NAME.getPreferredName(), name);
+        builder.field(RTN.getPreferredName(), rtn);
+        builder.field(PARAMETERS.getPreferredName(), parameters);
+        builder.endObject();
+
+        return builder;
+    }
+
+    public String getSortValue() {
+        return PainlessLookupUtility.buildPainlessMethodKey(name, parameters.size());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PainlessContextInstanceBindingInfo that = (PainlessContextInstanceBindingInfo) o;
+        return Objects.equals(declaring, that.declaring) &&
+                Objects.equals(name, that.name) &&
+                Objects.equals(rtn, that.rtn) &&
+                Objects.equals(parameters, that.parameters);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(declaring, name, rtn, parameters);
+    }
+}

+ 137 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/action/PainlessContextMethodInfo.java

@@ -0,0 +1,137 @@
+/*
+ * 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.painless.action;
+
+import org.elasticsearch.common.ParseField;
+import org.elasticsearch.common.io.stream.StreamInput;
+import org.elasticsearch.common.io.stream.StreamOutput;
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.ConstructingObjectParser;
+import org.elasticsearch.common.xcontent.ToXContent;
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.painless.lookup.PainlessLookupUtility;
+import org.elasticsearch.painless.lookup.PainlessMethod;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class PainlessContextMethodInfo implements Writeable, ToXContentObject {
+
+    public static final ParseField DECLARING = new ParseField("declaring");
+    public static final ParseField NAME = new ParseField("name");
+    public static final ParseField RTN = new ParseField("return");
+    public static final ParseField PARAMETERS = new ParseField("parameters");
+
+    @SuppressWarnings("unchecked")
+    private static final ConstructingObjectParser<PainlessContextMethodInfo, Void> PARSER = new ConstructingObjectParser<>(
+            PainlessContextMethodInfo.class.getCanonicalName(),
+            (v) ->
+                    new PainlessContextMethodInfo(
+                            (String)v[0],
+                            (String)v[1],
+                            (String)v[2],
+                            (List<String>)v[3]
+                    )
+    );
+
+    static {
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), DECLARING);
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), NAME);
+        PARSER.declareString(ConstructingObjectParser.constructorArg(), RTN);
+        PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), PARAMETERS);
+    }
+
+    private final String declaring;
+    private final String name;
+    private final String rtn;
+    private final List<String> parameters;
+
+    public PainlessContextMethodInfo(PainlessMethod painlessMethod) {
+        this(
+                painlessMethod.javaMethod.getDeclaringClass().getName(),
+                painlessMethod.javaMethod.getName(),
+                painlessMethod.returnType.getName(),
+                painlessMethod.typeParameters.stream().map(Class::getName).collect(Collectors.toList())
+        );
+    }
+
+    public PainlessContextMethodInfo(String declaring, String name, String rtn, List<String> parameters) {
+        this.declaring = Objects.requireNonNull(declaring);
+        this.name = Objects.requireNonNull(name);
+        this.rtn = Objects.requireNonNull(rtn);
+        this.parameters = Collections.unmodifiableList(Objects.requireNonNull(parameters));
+    }
+
+    public PainlessContextMethodInfo(StreamInput in) throws IOException {
+        declaring = in.readString();
+        name = in.readString();
+        rtn = in.readString();
+        parameters = Collections.unmodifiableList(in.readStringList());
+    }
+
+    @Override
+    public void writeTo(StreamOutput out) throws IOException {
+        out.writeString(declaring);
+        out.writeString(name);
+        out.writeString(rtn);
+        out.writeStringCollection(parameters);
+    }
+
+    public static PainlessContextMethodInfo fromXContent(XContentParser parser) {
+        return PARSER.apply(parser, null);
+    }
+
+    @Override
+    public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException {
+        builder.startObject();
+        builder.field(DECLARING.getPreferredName(), declaring);
+        builder.field(NAME.getPreferredName(), name);
+        builder.field(RTN.getPreferredName(), rtn);
+        builder.field(PARAMETERS.getPreferredName(), parameters);
+        builder.endObject();
+
+        return builder;
+    }
+
+    public String getSortValue() {
+        return PainlessLookupUtility.buildPainlessMethodKey(name, parameters.size());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        PainlessContextMethodInfo that = (PainlessContextMethodInfo) o;
+        return Objects.equals(declaring, that.declaring) &&
+                Objects.equals(name, that.name) &&
+                Objects.equals(rtn, that.rtn) &&
+                Objects.equals(parameters, that.parameters);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(declaring, name, rtn, parameters);
+    }
+}

+ 12 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookup.java

@@ -87,6 +87,18 @@ public final class PainlessLookup {
         return classesToPainlessClasses.keySet();
     }
 
+    public Set<String> getImportedPainlessMethodsKeys() {
+        return painlessMethodKeysToImportedPainlessMethods.keySet();
+    }
+
+    public Set<String> getPainlessClassBindingsKeys() {
+        return painlessMethodKeysToPainlessClassBindings.keySet();
+    }
+
+    public Set<String> getPainlessInstanceBindingsKeys() {
+        return painlessMethodKeysToPainlessInstanceBindings.keySet();
+    }
+
     public PainlessClass lookupPainlessClass(Class<?> targetClass) {
         return classesToPainlessClasses.get(targetClass);
     }

+ 1 - 0
modules/lang-painless/src/main/java/org/elasticsearch/painless/lookup/PainlessLookupUtility.java

@@ -202,6 +202,7 @@ public final class PainlessLookupUtility {
 
         return typesStringBuilder.toString();
     }
+
     /**
      * Converts a java type to a type based on the terminology specified as part of {@link PainlessLookupUtility} where if a type is an
      * object class or object array, the returned type will be the equivalent def class or def array. Otherwise, this behaves as an

+ 164 - 0
modules/lang-painless/src/test/java/org/elasticsearch/painless/action/ContextInfoTests.java

@@ -0,0 +1,164 @@
+/*
+ * 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.painless.action;
+
+import org.elasticsearch.common.io.stream.Writeable;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.test.AbstractSerializingTestCase;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ContextInfoTests extends AbstractSerializingTestCase<PainlessContextInfo> {
+
+    @Override
+    protected PainlessContextInfo doParseInstance(XContentParser parser) {
+        return PainlessContextInfo.fromXContent(parser);
+    }
+
+    @Override
+    protected PainlessContextInfo createTestInstance() {
+        int classesSize = randomIntBetween(20, 100);
+        List<PainlessContextClassInfo> classes = new ArrayList<>();
+
+        for (int clazz = 0; clazz < classesSize; ++clazz) {
+            int constructorsSize = randomInt(4);
+            List<PainlessContextConstructorInfo> constructors = new ArrayList<>(constructorsSize);
+            for (int constructor = 0; constructor < constructorsSize; ++constructor) {
+                int parameterSize = randomInt(12);
+                List<String> parameters = new ArrayList<>(parameterSize);
+                for (int parameter = 0; parameter < parameterSize; ++parameter) {
+                    parameters.add(randomAlphaOfLengthBetween(1, 20));
+                }
+                constructors.add(new PainlessContextConstructorInfo(
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        parameters));
+            }
+            ;
+
+            int staticMethodsSize = randomInt(4);
+            List<PainlessContextMethodInfo> staticMethods = new ArrayList<>(staticMethodsSize);
+            for (int staticMethod = 0; staticMethod < staticMethodsSize; ++staticMethod) {
+                int parameterSize = randomInt(12);
+                List<String> parameters = new ArrayList<>(parameterSize);
+                for (int parameter = 0; parameter < parameterSize; ++parameter) {
+                    parameters.add(randomAlphaOfLengthBetween(1, 20));
+                }
+                staticMethods.add(new PainlessContextMethodInfo(
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        parameters));
+            }
+
+            int methodsSize = randomInt(10);
+            List<PainlessContextMethodInfo> methods = new ArrayList<>(methodsSize);
+            for (int method = 0; method < methodsSize; ++method) {
+                int parameterSize = randomInt(12);
+                List<String> parameters = new ArrayList<>(parameterSize);
+                for (int parameter = 0; parameter < parameterSize; ++parameter) {
+                    parameters.add(randomAlphaOfLengthBetween(1, 20));
+                }
+                methods.add(new PainlessContextMethodInfo(
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        parameters));
+            }
+
+            int staticFieldsSize = randomInt(10);
+            List<PainlessContextFieldInfo> staticFields = new ArrayList<>();
+            for (int staticField = 0; staticField < staticFieldsSize; ++staticField) {
+                staticFields.add(new PainlessContextFieldInfo(
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        randomAlphaOfLength(randomIntBetween(4, 10))));
+            }
+
+            int fieldsSize = randomInt(4);
+            List<PainlessContextFieldInfo> fields = new ArrayList<>();
+            for (int field = 0; field < fieldsSize; ++field) {
+                fields.add(new PainlessContextFieldInfo(
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        randomAlphaOfLength(randomIntBetween(4, 10)),
+                        randomAlphaOfLength(randomIntBetween(4, 10))));
+            }
+
+            classes.add(new PainlessContextClassInfo(
+                    randomAlphaOfLength(randomIntBetween(3, 200)), randomBoolean(),
+                    constructors, staticMethods, methods, fields, staticFields));
+        }
+
+        int importedMethodsSize = randomInt(4);
+        List<PainlessContextMethodInfo> importedMethods = new ArrayList<>(importedMethodsSize);
+        for (int importedMethod = 0; importedMethod < importedMethodsSize; ++importedMethod) {
+            int parameterSize = randomInt(12);
+            List<String> parameters = new ArrayList<>(parameterSize);
+            for (int parameter = 0; parameter < parameterSize; ++parameter) {
+                parameters.add(randomAlphaOfLengthBetween(1, 20));
+            }
+            importedMethods.add(new PainlessContextMethodInfo(
+                    randomAlphaOfLength(randomIntBetween(4, 10)),
+                    randomAlphaOfLength(randomIntBetween(4, 10)),
+                    randomAlphaOfLength(randomIntBetween(4, 10)),
+                    parameters));
+        }
+        
+        int classBindingsSize = randomInt(3);
+        List<PainlessContextClassBindingInfo> classBindings = new ArrayList<>(classBindingsSize);
+        for (int classBinding = 0; classBinding < classBindingsSize; ++classBinding) {
+            int parameterSize = randomIntBetween(2, 5);
+            int readOnly = randomIntBetween(1, parameterSize - 1);
+            List<String> parameters = new ArrayList<>(parameterSize);
+            for (int parameter = 0; parameter < parameterSize; ++parameter) {
+                parameters.add(randomAlphaOfLengthBetween(1, 20));
+            }
+            classBindings.add(new PainlessContextClassBindingInfo(
+                    randomAlphaOfLength(randomIntBetween(4, 10)),
+                    randomAlphaOfLength(randomIntBetween(4, 10)),
+                    randomAlphaOfLength(randomIntBetween(4, 10)),
+                    readOnly,
+                    parameters));
+        }
+
+        int instanceBindingsSize = randomInt(3);
+        List<PainlessContextInstanceBindingInfo> instanceBindings = new ArrayList<>(classBindingsSize);
+        for (int instanceBinding = 0; instanceBinding < instanceBindingsSize; ++instanceBinding) {
+            int parameterSize = randomInt(12);
+            List<String> parameters = new ArrayList<>(parameterSize);
+            for (int parameter = 0; parameter < parameterSize; ++parameter) {
+                parameters.add(randomAlphaOfLengthBetween(1, 20));
+            }
+            instanceBindings.add(new PainlessContextInstanceBindingInfo(
+                    randomAlphaOfLength(randomIntBetween(4, 10)),
+                    randomAlphaOfLength(randomIntBetween(4, 10)),
+                    randomAlphaOfLength(randomIntBetween(4, 10)),
+                    parameters));
+        }
+        
+        return new PainlessContextInfo(randomAlphaOfLength(20),
+                classes, importedMethods, classBindings, instanceBindings);
+    }
+
+    @Override
+    protected Writeable.Reader<PainlessContextInfo> instanceReader() {
+        return PainlessContextInfo::new;
+    }
+}

+ 22 - 0
modules/lang-painless/src/test/resources/rest-api-spec/test/painless/71_context_api.yml

@@ -0,0 +1,22 @@
+"Action to list contexts":
+    - do:
+        scripts_painless_context: {}
+    - match: { contexts.0: aggregation_selector}
+    - match: { contexts.22: update}
+---
+
+"Action to get all API values for score context":
+    - do:
+        scripts_painless_context:
+            context: score
+    - match: { name: score }
+    - match: { classes.6.name: java.lang.Appendable }
+    - match: { classes.6.imported: true }
+    - match: { classes.6.methods.0.name : append }
+    - match: { classes.6.methods.0.return : java.lang.Appendable }
+    - match: { classes.6.methods.0.parameters.0 : java.lang.CharSequence }
+    - match: { classes.6.methods.0.parameters.1 : int }
+    - match: { classes.6.methods.0.parameters.2 : int }
+    - match: { imported_methods.0.name: dotProduct }
+    - match: { class_bindings.0.name: cosineSimilarity }
+    - match: { instance_bindings: [] }

+ 17 - 0
rest-api-spec/src/main/resources/rest-api-spec/api/scripts_painless_context.json

@@ -0,0 +1,17 @@
+{
+  "scripts_painless_context": {
+    "methods": ["GET"],
+    "url": {
+      "path": "/_scripts/painless/_context",
+      "paths": ["/_scripts/painless/_context"],
+      "parts": {
+      },
+      "params": {
+        "context" : {
+          "type" : "string",
+          "description" : "Select a specific context to retrieve API information about"
+        }
+      }
+    }
+  }
+}