Răsfoiți Sursa

Scripting: Whitelist API spec gradle task (#66050)

Adds `generateContextApiSpec` gradle task that generates whitelist api
specs under `modules/lang-painless/src/main/generated/whitelist-json`.

The common classes are in `painless-common.json`, the specialized classes
per context are in `painless-$context.json`.

eg. `painless-aggs.json` has the specialization for the aggs contexts

Refs: #49879
Stuart Tettemer 4 ani în urmă
părinte
comite
2aa2224b36

+ 20 - 0
modules/lang-painless/build.gradle

@@ -106,6 +106,26 @@ tasks.register("generateContextDoc", DefaultTestClustersTask) {
     }.assertNormalExitValue()
   }
 }
+/**********************************************
+ *           Context JSON Generation          *
+ **********************************************/
+testClusters {
+  generateContextApiSpecCluster {
+    testDistribution = 'DEFAULT'
+  }
+}
+
+tasks.register("generateContextApiSpec", DefaultTestClustersTask) {
+  dependsOn sourceSets.doc.runtimeClasspath
+  useCluster testClusters.generateContextApiSpecCluster
+  doFirst {
+    project.javaexec {
+      main = 'org.elasticsearch.painless.ContextApiSpecGenerator'
+      classpath = sourceSets.doc.runtimeClasspath
+      systemProperty "cluster.uri", "${-> testClusters.generateContextApiSpecCluster.singleNode().getAllHttpSocketURI().get(0)}"
+    }.assertNormalExitValue()
+  }
+}
 
 /**********************************************
  *            Parser regeneration             *

+ 75 - 0
modules/lang-painless/src/doc/java/org/elasticsearch/painless/ContextApiSpecGenerator.java

@@ -0,0 +1,75 @@
+/*
+ * 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;
+
+import org.elasticsearch.common.SuppressForbidden;
+import org.elasticsearch.common.io.PathUtils;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.common.xcontent.XContentFactory;
+import org.elasticsearch.core.internal.io.IOUtils;
+import org.elasticsearch.painless.action.PainlessContextInfo;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+
+public class ContextApiSpecGenerator {
+    public static void main(String[] args) throws IOException {
+        List<PainlessContextInfo> contexts = ContextGeneratorCommon.getContextInfos();
+        ContextGeneratorCommon.PainlessInfos infos = new ContextGeneratorCommon.PainlessInfos(contexts);
+        Path rootDir = resetRootDir();
+        Path json = rootDir.resolve("painless-common.json");
+        try (PrintStream jsonStream = new PrintStream(
+             Files.newOutputStream(json, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE),
+             false, StandardCharsets.UTF_8.name())) {
+
+            XContentBuilder builder = XContentFactory.jsonBuilder(jsonStream);
+            builder.startObject();
+            builder.field(PainlessContextInfo.CLASSES.getPreferredName(), infos.common);
+            builder.endObject();
+            builder.flush();
+        }
+
+        for (PainlessInfoJson.Context context : infos.contexts) {
+            json = rootDir.resolve("painless-" + context.getName() + ".json");
+            try (PrintStream jsonStream = new PrintStream(
+                Files.newOutputStream(json, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE),
+                false, StandardCharsets.UTF_8.name())) {
+
+                XContentBuilder builder = XContentFactory.jsonBuilder(jsonStream);
+                context.toXContent(builder, null);
+                builder.flush();
+            }
+        }
+    }
+
+    @SuppressForbidden(reason = "resolve context api directory with environment")
+    private static Path resetRootDir() throws IOException {
+        Path rootDir = PathUtils.get("./src/main/generated/whitelist-json");
+        IOUtils.rm(rootDir);
+        Files.createDirectories(rootDir);
+
+        return rootDir;
+    }
+}

+ 20 - 91
modules/lang-painless/src/doc/java/org/elasticsearch/painless/ContextDocGenerator.java

@@ -21,8 +21,6 @@ package org.elasticsearch.painless;
 
 import org.elasticsearch.common.SuppressForbidden;
 import org.elasticsearch.common.io.PathUtils;
-import org.elasticsearch.common.xcontent.XContentParser;
-import org.elasticsearch.common.xcontent.json.JsonXContent;
 import org.elasticsearch.core.internal.io.IOUtils;
 import org.elasticsearch.painless.action.PainlessContextClassBindingInfo;
 import org.elasticsearch.painless.action.PainlessContextClassInfo;
@@ -34,16 +32,12 @@ import org.elasticsearch.painless.action.PainlessContextMethodInfo;
 
 import java.io.IOException;
 import java.io.PrintStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.URLConnection;
 import java.nio.charset.StandardCharsets;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.StandardOpenOption;
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -70,7 +64,7 @@ public final class ContextDocGenerator {
     private static final String SHARED_NAME = "Shared";
 
     public static void main(String[] args) throws IOException {
-        List<PainlessContextInfo> contextInfos = getContextInfos();
+        List<PainlessContextInfo> contextInfos = ContextGeneratorCommon.getContextInfos();
         Set<Object> sharedStaticInfos = createSharedStatics(contextInfos);
         Set<PainlessContextClassInfo> sharedClassInfos = createSharedClasses(contextInfos);
 
@@ -102,33 +96,6 @@ public final class ContextDocGenerator {
         printRootIndexPage(rootDir, contextInfos, isSpecialized);
     }
 
-    @SuppressForbidden(reason = "retrieving data from an internal API not exposed as part of the REST client")
-    private static List<PainlessContextInfo> getContextInfos() throws IOException  {
-        URLConnection getContextNames = new URL(
-                "http://" + System.getProperty("cluster.uri") + "/_scripts/painless/_context").openConnection();
-        XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, getContextNames.getInputStream());
-        parser.nextToken();
-        parser.nextToken();
-        @SuppressWarnings("unchecked")
-        List<String> contextNames = (List<String>)(Object)parser.list();
-        parser.close();
-        ((HttpURLConnection)getContextNames).disconnect();
-
-        List<PainlessContextInfo> contextInfos = new ArrayList<>();
-
-        for (String contextName : contextNames) {
-            URLConnection getContextInfo = new URL(
-                    "http://" + System.getProperty("cluster.uri") + "/_scripts/painless/_context?context=" + contextName).openConnection();
-            parser = JsonXContent.jsonXContent.createParser(null, null, getContextInfo.getInputStream());
-            contextInfos.add(PainlessContextInfo.fromXContent(parser));
-            ((HttpURLConnection)getContextInfo).disconnect();
-        }
-
-        contextInfos.sort(Comparator.comparing(PainlessContextInfo::getName));
-
-        return contextInfos;
-    }
-
     private static Set<Object> createSharedStatics(List<PainlessContextInfo> contextInfos) {
         Map<Object, Integer> staticInfoCounts = new HashMap<>();
 
@@ -291,7 +258,7 @@ public final class ContextDocGenerator {
                     indexStream.println();
                 }
 
-                String className = getType(javaNamesToDisplayNames, classInfo.getName());
+                String className = ContextGeneratorCommon.getType(javaNamesToDisplayNames, classInfo.getName());
                 indexStream.println("* <<" + getClassHeader(contextHeader, className) + ", " + className + ">>");
             }
         }
@@ -354,7 +321,7 @@ public final class ContextDocGenerator {
                         "for a high-level overview of all packages and classes.");
             }
 
-            String className = getType(javaNamesToDisplayNames, classInfo.getName());
+            String className = ContextGeneratorCommon.getType(javaNamesToDisplayNames, classInfo.getName());
             packagesStream.println();
             packagesStream.println("[[" + getClassHeader(contextHeader, className) + "]]");
             packagesStream.println("==== " + className + "");
@@ -443,7 +410,7 @@ public final class ContextDocGenerator {
              parameterIndex < constructorInfo.getParameters().size();
              ++parameterIndex) {
 
-            stream.print(getType(javaNamesToDisplayNames, constructorInfo.getParameters().get(parameterIndex)));
+            stream.print(ContextGeneratorCommon.getType(javaNamesToDisplayNames, constructorInfo.getParameters().get(parameterIndex)));
 
             if (parameterIndex + 1 < constructorInfo.getParameters().size()) {
                 stream.print(", ");
@@ -458,7 +425,7 @@ public final class ContextDocGenerator {
             boolean isStatic, PainlessContextMethodInfo methodInfo) {
 
         stream.print("* " + (isStatic ? "static " : ""));
-        stream.print(getType(javaNamesToDisplayNames, methodInfo.getRtn()) + " ");
+        stream.print(ContextGeneratorCommon.getType(javaNamesToDisplayNames, methodInfo.getRtn()) + " ");
 
         if (methodInfo.getDeclaring().startsWith("java.")) {
             stream.print(getMethodJavaDocLink(methodInfo) + "[" + methodInfo.getName() + "]");
@@ -472,7 +439,7 @@ public final class ContextDocGenerator {
              parameterIndex < methodInfo.getParameters().size();
              ++parameterIndex) {
 
-            stream.print(getType(javaNamesToDisplayNames, methodInfo.getParameters().get(parameterIndex)));
+            stream.print(ContextGeneratorCommon.getType(javaNamesToDisplayNames, methodInfo.getParameters().get(parameterIndex)));
 
             if (parameterIndex + 1 < methodInfo.getParameters().size()) {
                 stream.print(", ");
@@ -485,17 +452,21 @@ public final class ContextDocGenerator {
     private static void printClassBinding(
             PrintStream stream, Map<String, String> javaNamesToDisplayNames, PainlessContextClassBindingInfo classBindingInfo) {
 
-        stream.print("* " + getType(javaNamesToDisplayNames, classBindingInfo.getRtn()) + " " + classBindingInfo.getName() + "(");
+        stream.print("* " +
+            ContextGeneratorCommon.getType(javaNamesToDisplayNames, classBindingInfo.getRtn()) +
+            " " +
+            classBindingInfo.getName() +
+            "(");
 
         for (int parameterIndex = 0; parameterIndex < classBindingInfo.getParameters().size(); ++parameterIndex) {
             // temporary fix to not print org.elasticsearch.script.ScoreScript parameter until
             // class instance bindings are created and the information is appropriately added to the context info classes
             if ("org.elasticsearch.script.ScoreScript".equals(
-                    getType(javaNamesToDisplayNames, classBindingInfo.getParameters().get(parameterIndex)))) {
+                    ContextGeneratorCommon.getType(javaNamesToDisplayNames, classBindingInfo.getParameters().get(parameterIndex)))) {
                 continue;
             }
 
-            stream.print(getType(javaNamesToDisplayNames, classBindingInfo.getParameters().get(parameterIndex)));
+            stream.print(ContextGeneratorCommon.getType(javaNamesToDisplayNames, classBindingInfo.getParameters().get(parameterIndex)));
 
             if (parameterIndex < classBindingInfo.getReadOnly()) {
                 stream.print(" *");
@@ -512,10 +483,14 @@ public final class ContextDocGenerator {
     private static void printInstanceBinding(
             PrintStream stream, Map<String, String> javaNamesToDisplayNames, PainlessContextInstanceBindingInfo instanceBindingInfo) {
 
-        stream.print("* " + getType(javaNamesToDisplayNames, instanceBindingInfo.getRtn()) + " " + instanceBindingInfo.getName() + "(");
+        stream.print("* " +
+            ContextGeneratorCommon.getType(javaNamesToDisplayNames, instanceBindingInfo.getRtn()) +
+            " " +
+            instanceBindingInfo.getName() +
+            "(");
 
         for (int parameterIndex = 0; parameterIndex < instanceBindingInfo.getParameters().size(); ++parameterIndex) {
-            stream.print(getType(javaNamesToDisplayNames, instanceBindingInfo.getParameters().get(parameterIndex)));
+            stream.print(ContextGeneratorCommon.getType(javaNamesToDisplayNames, instanceBindingInfo.getParameters().get(parameterIndex)));
 
             if (parameterIndex + 1 < instanceBindingInfo.getParameters().size()) {
                 stream.print(", ");
@@ -530,7 +505,7 @@ public final class ContextDocGenerator {
             boolean isStatic, PainlessContextFieldInfo fieldInfo) {
 
         stream.print("* " + (isStatic ? "static " : ""));
-        stream.print(getType(javaNamesToDisplayNames, fieldInfo.getType()) + " ");
+        stream.print(ContextGeneratorCommon.getType(javaNamesToDisplayNames, fieldInfo.getType()) + " ");
 
         if (fieldInfo.getDeclaring().startsWith("java.")) {
             stream.println(getFieldJavaDocLink(fieldInfo) + "[" + fieldInfo.getName() + "]");
@@ -539,52 +514,6 @@ public final class ContextDocGenerator {
         }
     }
 
-    private static String getType(Map<String, String> javaNamesToDisplayNames, String javaType) {
-        int arrayDimensions = 0;
-
-        while (javaType.charAt(arrayDimensions) == '[') {
-            ++arrayDimensions;
-        }
-
-        if (arrayDimensions > 0) {
-            if (javaType.charAt(javaType.length() - 1) == ';') {
-                javaType = javaType.substring(arrayDimensions + 1, javaType.length() - 1);
-            } else {
-                javaType = javaType.substring(arrayDimensions);
-            }
-        }
-
-        if ("Z".equals(javaType) || "boolean".equals(javaType)) {
-            javaType = "boolean";
-        } else if ("V".equals(javaType) || "void".equals(javaType)) {
-            javaType = "void";
-        } else if ("B".equals(javaType) || "byte".equals(javaType)) {
-            javaType = "byte";
-        } else if ("S".equals(javaType) || "short".equals(javaType)) {
-            javaType = "short";
-        } else if ("C".equals(javaType) || "char".equals(javaType)) {
-            javaType = "char";
-        } else if ("I".equals(javaType) || "int".equals(javaType)) {
-            javaType = "int";
-        } else if ("J".equals(javaType) || "long".equals(javaType)) {
-            javaType = "long";
-        } else if ("F".equals(javaType) || "float".equals(javaType)) {
-            javaType = "float";
-        } else if ("D".equals(javaType) || "double".equals(javaType)) {
-            javaType = "double";
-        } else if ("org.elasticsearch.painless.lookup.def".equals(javaType)) {
-            javaType = "def";
-        } else {
-            javaType = javaNamesToDisplayNames.get(javaType);
-        }
-
-        while (arrayDimensions-- > 0) {
-            javaType += "[]";
-        }
-
-        return javaType;
-    }
-
     private static String getFieldJavaDocLink(PainlessContextFieldInfo fieldInfo) {
         return "{java11-javadoc}/java.base/" + fieldInfo.getDeclaring().replace('.', '/') + ".html#" + fieldInfo.getName();
     }

+ 236 - 0
modules/lang-painless/src/doc/java/org/elasticsearch/painless/ContextGeneratorCommon.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;
+
+import org.elasticsearch.common.SuppressForbidden;
+import org.elasticsearch.common.xcontent.XContentParser;
+import org.elasticsearch.common.xcontent.json.JsonXContent;
+import org.elasticsearch.painless.action.PainlessContextClassBindingInfo;
+import org.elasticsearch.painless.action.PainlessContextClassInfo;
+import org.elasticsearch.painless.action.PainlessContextInfo;
+import org.elasticsearch.painless.action.PainlessContextInstanceBindingInfo;
+import org.elasticsearch.painless.action.PainlessContextMethodInfo;
+
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class ContextGeneratorCommon {
+    @SuppressForbidden(reason = "retrieving data from an internal API not exposed as part of the REST client")
+    public static List<PainlessContextInfo> getContextInfos() throws IOException {
+        URLConnection getContextNames = new URL(
+            "http://" + System.getProperty("cluster.uri") + "/_scripts/painless/_context").openConnection();
+        XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, getContextNames.getInputStream());
+        parser.nextToken();
+        parser.nextToken();
+        @SuppressWarnings("unchecked")
+        List<String> contextNames = (List<String>)(Object)parser.list();
+        parser.close();
+        ((HttpURLConnection)getContextNames).disconnect();
+
+        List<PainlessContextInfo> contextInfos = new ArrayList<>();
+
+        for (String contextName : contextNames) {
+            URLConnection getContextInfo = new URL(
+                "http://" + System.getProperty("cluster.uri") + "/_scripts/painless/_context?context=" + contextName).openConnection();
+            parser = JsonXContent.jsonXContent.createParser(null, null, getContextInfo.getInputStream());
+            contextInfos.add(PainlessContextInfo.fromXContent(parser));
+            ((HttpURLConnection)getContextInfo).disconnect();
+        }
+
+        contextInfos.sort(Comparator.comparing(PainlessContextInfo::getName));
+
+        return contextInfos;
+    }
+
+    public static String getType(Map<String, String> javaNamesToDisplayNames, String javaType) {
+        int arrayDimensions = 0;
+
+        while (javaType.charAt(arrayDimensions) == '[') {
+            ++arrayDimensions;
+        }
+
+        if (arrayDimensions > 0) {
+            if (javaType.charAt(javaType.length() - 1) == ';') {
+                javaType = javaType.substring(arrayDimensions + 1, javaType.length() - 1);
+            } else {
+                javaType = javaType.substring(arrayDimensions);
+            }
+        }
+
+        if ("Z".equals(javaType) || "boolean".equals(javaType)) {
+            javaType = "boolean";
+        } else if ("V".equals(javaType) || "void".equals(javaType)) {
+            javaType = "void";
+        } else if ("B".equals(javaType) || "byte".equals(javaType)) {
+            javaType = "byte";
+        } else if ("S".equals(javaType) || "short".equals(javaType)) {
+            javaType = "short";
+        } else if ("C".equals(javaType) || "char".equals(javaType)) {
+            javaType = "char";
+        } else if ("I".equals(javaType) || "int".equals(javaType)) {
+            javaType = "int";
+        } else if ("J".equals(javaType) || "long".equals(javaType)) {
+            javaType = "long";
+        } else if ("F".equals(javaType) || "float".equals(javaType)) {
+            javaType = "float";
+        } else if ("D".equals(javaType) || "double".equals(javaType)) {
+            javaType = "double";
+        } else if ("org.elasticsearch.painless.lookup.def".equals(javaType)) {
+            javaType = "def";
+        } else {
+            javaType = javaNamesToDisplayNames.get(javaType);
+        }
+
+        while (arrayDimensions-- > 0) {
+            javaType += "[]";
+        }
+
+        return javaType;
+    }
+
+    private static Map<String, String> getDisplayNames(Collection<PainlessContextInfo> contextInfos) {
+        Map<String, String> javaNamesToDisplayNames = new HashMap<>();
+
+        for (PainlessContextInfo contextInfo : contextInfos) {
+            for (PainlessContextClassInfo classInfo : contextInfo.getClasses()) {
+                String className = classInfo.getName();
+                if (javaNamesToDisplayNames.containsKey(className) == false) {
+                    if (classInfo.isImported()) {
+                        javaNamesToDisplayNames.put(className,
+                            className.substring(className.lastIndexOf('.') + 1).replace('$', '.'));
+                    } else {
+                        javaNamesToDisplayNames.put(className, className.replace('$', '.'));
+                    }
+                }
+            }
+        }
+        return javaNamesToDisplayNames;
+    }
+
+    public static List<PainlessContextClassInfo> sortClassInfos(Collection<PainlessContextClassInfo> unsortedClassInfos) {
+
+        List<PainlessContextClassInfo> classInfos = new ArrayList<>(unsortedClassInfos);
+        classInfos.removeIf(v ->
+            "void".equals(v.getName())  || "boolean".equals(v.getName()) || "byte".equals(v.getName())   ||
+                "short".equals(v.getName()) || "char".equals(v.getName())    || "int".equals(v.getName())    ||
+                "long".equals(v.getName())  || "float".equals(v.getName())   || "double".equals(v.getName()) ||
+                "org.elasticsearch.painless.lookup.def".equals(v.getName())  ||
+                isInternalClass(v.getName())
+        );
+
+        classInfos.sort((c1, c2) -> {
+            String n1 = c1.getName();
+            String n2 = c2.getName();
+            boolean i1 = c1.isImported();
+            boolean i2 = c2.isImported();
+
+            String p1 = n1.substring(0, n1.lastIndexOf('.'));
+            String p2 = n2.substring(0, n2.lastIndexOf('.'));
+
+            int compare = p1.compareTo(p2);
+
+            if (compare == 0) {
+                if (i1 && i2) {
+                    compare = n1.substring(n1.lastIndexOf('.') + 1).compareTo(n2.substring(n2.lastIndexOf('.') + 1));
+                } else if (i1 == false && i2 == false) {
+                    compare = n1.compareTo(n2);
+                } else {
+                    compare = Boolean.compare(i1, i2) * -1;
+                }
+            }
+
+            return compare;
+        });
+
+        return classInfos;
+    }
+
+    private static boolean isInternalClass(String javaName) {
+        return  javaName.equals("org.elasticsearch.script.ScoreScript") ||
+            javaName.equals("org.elasticsearch.xpack.sql.expression.function.scalar.geo.GeoShape") ||
+            javaName.equals("org.elasticsearch.xpack.sql.expression.function.scalar.whitelist.InternalSqlScriptUtils") ||
+            javaName.equals("org.elasticsearch.xpack.sql.expression.literal.IntervalDayTime") ||
+            javaName.equals("org.elasticsearch.xpack.sql.expression.literal.IntervalYearMonth") ||
+            javaName.equals("org.elasticsearch.xpack.eql.expression.function.scalar.whitelist.InternalEqlScriptUtils") ||
+            javaName.equals("org.elasticsearch.xpack.ql.expression.function.scalar.InternalQlScriptUtils") ||
+            javaName.equals("org.elasticsearch.xpack.ql.expression.function.scalar.whitelist.InternalQlScriptUtils") ||
+            javaName.equals("org.elasticsearch.script.ScoreScript$ExplanationHolder");
+    }
+
+    public static List<PainlessContextClassInfo> excludeCommonClassInfos(
+        Set<PainlessContextClassInfo> exclude,
+        List<PainlessContextClassInfo> classInfos
+    ) {
+        List<PainlessContextClassInfo> uniqueClassInfos = new ArrayList<>(classInfos);
+        uniqueClassInfos.removeIf(exclude::contains);
+        return uniqueClassInfos;
+    }
+
+    public static class PainlessInfos {
+        public final Set<PainlessContextMethodInfo> importedMethods;
+        public final Set<PainlessContextClassBindingInfo> classBindings;
+        public final Set<PainlessContextInstanceBindingInfo> instanceBindings;
+
+        public final List<PainlessInfoJson.Class> common;
+        public final List<PainlessInfoJson.Context> contexts;
+
+        public final Map<String, String> javaNamesToDisplayNames;
+
+        public PainlessInfos(List<PainlessContextInfo> contextInfos) {
+            javaNamesToDisplayNames = getDisplayNames(contextInfos);
+
+            Set<PainlessContextClassInfo> commonClassInfos = getCommon(contextInfos, PainlessContextInfo::getClasses);
+            common = PainlessInfoJson.Class.fromInfos(sortClassInfos(commonClassInfos), javaNamesToDisplayNames);
+
+            importedMethods = getCommon(contextInfos, PainlessContextInfo::getImportedMethods);
+
+            classBindings = getCommon(contextInfos, PainlessContextInfo::getClassBindings);
+
+            instanceBindings = getCommon(contextInfos, PainlessContextInfo::getInstanceBindings);
+
+            contexts = contextInfos.stream()
+                .map(ctx -> new PainlessInfoJson.Context(ctx, commonClassInfos, javaNamesToDisplayNames))
+                .collect(Collectors.toList());
+        }
+
+        private <T> Set<T> getCommon(List<PainlessContextInfo> contexts, Function<PainlessContextInfo,List<T>> getter) {
+            Map<T, Integer> infoCounts = new HashMap<>();
+            for (PainlessContextInfo contextInfo : contexts) {
+                for (T info : getter.apply(contextInfo)) {
+                    infoCounts.merge(info, 1, Integer::sum);
+                }
+            }
+            return infoCounts.entrySet().stream().filter(
+                e -> e.getValue() == contexts.size()
+            ).map(Map.Entry::getKey).collect(Collectors.toSet());
+        }
+    }
+}

+ 209 - 0
modules/lang-painless/src/doc/java/org/elasticsearch/painless/PainlessInfoJson.java

@@ -0,0 +1,209 @@
+/*
+ * 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;
+
+import org.elasticsearch.common.xcontent.ToXContentObject;
+import org.elasticsearch.common.xcontent.XContentBuilder;
+import org.elasticsearch.painless.action.PainlessContextClassBindingInfo;
+import org.elasticsearch.painless.action.PainlessContextClassInfo;
+import org.elasticsearch.painless.action.PainlessContextConstructorInfo;
+import org.elasticsearch.painless.action.PainlessContextFieldInfo;
+import org.elasticsearch.painless.action.PainlessContextInfo;
+import org.elasticsearch.painless.action.PainlessContextInstanceBindingInfo;
+import org.elasticsearch.painless.action.PainlessContextMethodInfo;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class PainlessInfoJson {
+    public static class Context implements ToXContentObject {
+        private final String name;
+        private final List<Class> classes;
+        private final List<Method> importedMethods;
+        private final List<PainlessContextClassBindingInfo> classBindings;
+        private final List<PainlessContextInstanceBindingInfo> instanceBindings;
+
+        public Context(
+            PainlessContextInfo info,
+            Set<PainlessContextClassInfo> commonClassInfos,
+            Map<String, String> javaNamesToDisplayNames
+        ) {
+            this.name = info.getName();
+            List<PainlessContextClassInfo> classInfos = ContextGeneratorCommon.excludeCommonClassInfos(commonClassInfos, info.getClasses());
+            classInfos = ContextGeneratorCommon.sortClassInfos(classInfos);
+            this.classes = Class.fromInfos(classInfos, javaNamesToDisplayNames);
+            this.importedMethods = Method.fromInfos(info.getImportedMethods(), javaNamesToDisplayNames);
+            this.classBindings = info.getClassBindings();
+            this.instanceBindings = info.getInstanceBindings();
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.field(PainlessContextInfo.NAME.getPreferredName(), name);
+            builder.field(PainlessContextInfo.CLASSES.getPreferredName(), classes);
+            builder.field(PainlessContextInfo.IMPORTED_METHODS.getPreferredName(), importedMethods);
+            builder.field(PainlessContextInfo.CLASS_BINDINGS.getPreferredName(), classBindings);
+            builder.field(PainlessContextInfo.INSTANCE_BINDINGS.getPreferredName(), instanceBindings);
+            builder.endObject();
+
+            return builder;
+        }
+
+        public String getName() {
+            return name;
+        }
+    }
+
+    public static class Class implements ToXContentObject {
+        private final String name;
+        private final boolean imported;
+        private final List<Constructor> constructors;
+        private final List<Method> staticMethods;
+        private final List<Method> methods;
+        private final List<Field> staticFields;
+        private final List<Field> fields;
+
+        public Class(PainlessContextClassInfo info, Map<String, String> javaNamesToDisplayNames) {
+            this.name = javaNamesToDisplayNames.get(info.getName());
+            this.imported = info.isImported();
+            this.constructors = Constructor.fromInfos(info.getConstructors(), javaNamesToDisplayNames);
+            this.staticMethods = Method.fromInfos(info.getStaticMethods(), javaNamesToDisplayNames);
+            this.methods = Method.fromInfos(info.getMethods(), javaNamesToDisplayNames);
+            this.staticFields = Field.fromInfos(info.getStaticFields(), javaNamesToDisplayNames);
+            this.fields = Field.fromInfos(info.getFields(), javaNamesToDisplayNames);
+        }
+
+        public static List<Class> fromInfos(List<PainlessContextClassInfo> infos, Map<String, String> javaNamesToDisplayNames) {
+            return infos.stream()
+                .map(info -> new Class(info, javaNamesToDisplayNames))
+                .collect(Collectors.toList());
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.field(PainlessContextClassInfo.NAME.getPreferredName(), name);
+            builder.field(PainlessContextClassInfo.IMPORTED.getPreferredName(), imported);
+            builder.field(PainlessContextClassInfo.CONSTRUCTORS.getPreferredName(), constructors);
+            builder.field(PainlessContextClassInfo.STATIC_METHODS.getPreferredName(), staticMethods);
+            builder.field(PainlessContextClassInfo.METHODS.getPreferredName(), methods);
+            builder.field(PainlessContextClassInfo.STATIC_FIELDS.getPreferredName(), staticFields);
+            builder.field(PainlessContextClassInfo.FIELDS.getPreferredName(), fields);
+            builder.endObject();
+
+            return builder;
+        }
+    }
+
+    public static class Method implements ToXContentObject {
+        private final String declaring;
+        private final String name;
+        private final String rtn;
+        private final List<String> parameters;
+
+        public Method(PainlessContextMethodInfo info, Map<String, String> javaNamesToDisplayNames) {
+            this.declaring = javaNamesToDisplayNames.get(info.getDeclaring());
+            this.name = info.getName();
+            this.rtn = ContextGeneratorCommon.getType(javaNamesToDisplayNames, info.getRtn());
+            this.parameters = info.getParameters().stream()
+                .map(p -> ContextGeneratorCommon.getType(javaNamesToDisplayNames, p))
+                .collect(Collectors.toList());
+        }
+
+        public static List<Method> fromInfos(List<PainlessContextMethodInfo> infos, Map<String, String> javaNamesToDisplayNames) {
+            return infos.stream()
+                .map(m -> new Method(m, javaNamesToDisplayNames))
+                .collect(Collectors.toList());
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.field(PainlessContextMethodInfo.DECLARING.getPreferredName(), declaring);
+            builder.field(PainlessContextMethodInfo.NAME.getPreferredName(), name);
+            builder.field(PainlessContextMethodInfo.RTN.getPreferredName(), rtn);
+            builder.field(PainlessContextMethodInfo.PARAMETERS.getPreferredName(), parameters);
+            builder.endObject();
+
+            return builder;
+        }
+    }
+
+    public static class Constructor implements ToXContentObject {
+        private final String declaring;
+        private final List<String> parameters;
+
+        public Constructor(PainlessContextConstructorInfo info, Map<String, String> javaNamesToDisplayNames) {
+            this.declaring = javaNamesToDisplayNames.get(info.getDeclaring());
+            this.parameters = info.getParameters().stream()
+                .map(p -> ContextGeneratorCommon.getType(javaNamesToDisplayNames, p))
+                .collect(Collectors.toList());
+        }
+
+        public static List<Constructor> fromInfos(List<PainlessContextConstructorInfo> infos, Map<String, String> javaNamesToDisplayNames) {
+            return infos.stream()
+                .map(c -> new Constructor(c, javaNamesToDisplayNames))
+                .collect(Collectors.toList());
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.field(PainlessContextConstructorInfo.DECLARING.getPreferredName(), declaring);
+            builder.field(PainlessContextConstructorInfo.PARAMETERS.getPreferredName(), parameters);
+            builder.endObject();
+
+            return builder;
+        }
+    }
+
+    public static class Field implements ToXContentObject {
+        private final String declaring;
+        private final String name;
+        private final String type;
+
+        public Field(PainlessContextFieldInfo info, Map<String, String> javaNamesToDisplayNames) {
+            this.declaring = javaNamesToDisplayNames.get(info.getDeclaring());
+            this.name = info.getName();
+            this.type = ContextGeneratorCommon.getType(javaNamesToDisplayNames, info.getType());
+        }
+
+        public static List<Field> fromInfos(List<PainlessContextFieldInfo> infos, Map<String, String> javaNamesToDisplayNames) {
+            return infos.stream()
+                .map(f -> new Field(f, javaNamesToDisplayNames))
+                .collect(Collectors.toList());
+        }
+
+        @Override
+        public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
+            builder.startObject();
+            builder.field(PainlessContextFieldInfo.DECLARING.getPreferredName(), declaring);
+            builder.field(PainlessContextFieldInfo.NAME.getPreferredName(), name);
+            builder.field(PainlessContextFieldInfo.TYPE.getPreferredName(), type);
+            builder.endObject();
+
+            return builder;
+        }
+    }
+}