/* * 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.apache.logging.log4j.Logger; import org.apache.lucene.util.IOUtils; import org.elasticsearch.common.io.PathUtils; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.painless.Definition.Field; import org.elasticsearch.painless.Definition.Method; import org.elasticsearch.painless.Definition.Struct; import org.elasticsearch.painless.Definition.Type; import org.elasticsearch.painless.api.Augmentation; import java.io.IOException; import java.io.PrintStream; import java.lang.reflect.Modifier; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.function.Consumer; import static java.util.Comparator.comparing; import static java.util.stream.Collectors.toList; /** * Generates an API reference from the method and type whitelists in {@link Definition}. */ public class PainlessDocGenerator { private static final Logger logger = ESLoggerFactory.getLogger(PainlessDocGenerator.class); private static final Comparator<Field> FIELD_NAME = comparing(f -> f.name); private static final Comparator<Method> METHOD_NAME = comparing(m -> m.name); private static final Comparator<Method> NUMBER_OF_ARGS = comparing(m -> m.arguments.size()); public static void main(String[] args) throws IOException { Path apiRootPath = PathUtils.get(args[0]); // Blow away the last execution and recreate it from scratch IOUtils.rm(apiRootPath); Files.createDirectories(apiRootPath); Path indexPath = apiRootPath.resolve("index.asciidoc"); logger.info("Starting to write [index.asciidoc]"); try (PrintStream indexStream = new PrintStream( Files.newOutputStream(indexPath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), false, StandardCharsets.UTF_8.name())) { emitGeneratedWarning(indexStream); List<Type> types = Definition.allSimpleTypes().stream().sorted(comparing(t -> t.name)).collect(toList()); for (Type type : types) { if (type.sort.primitive) { // Primitives don't have methods to reference continue; } if ("def".equals(type.name)) { // def is special but doesn't have any methods all of its own. continue; } indexStream.print("include::"); indexStream.print(type.struct.name); indexStream.println(".asciidoc[]"); Path typePath = apiRootPath.resolve(type.struct.name + ".asciidoc"); logger.info("Writing [{}.asciidoc]", type.name); try (PrintStream typeStream = new PrintStream( Files.newOutputStream(typePath, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE), false, StandardCharsets.UTF_8.name())) { emitGeneratedWarning(typeStream); typeStream.print("[["); emitAnchor(typeStream, type.struct); typeStream.print("]]++"); typeStream.print(type.name); typeStream.println("++::"); Consumer<Field> documentField = field -> PainlessDocGenerator.documentField(typeStream, field); Consumer<Method> documentMethod = method -> PainlessDocGenerator.documentMethod(typeStream, method); type.struct.staticMembers.values().stream().sorted(FIELD_NAME).forEach(documentField); type.struct.members.values().stream().sorted(FIELD_NAME).forEach(documentField); type.struct.staticMethods.values().stream().sorted(METHOD_NAME.thenComparing(NUMBER_OF_ARGS)).forEach(documentMethod); type.struct.constructors.values().stream().sorted(NUMBER_OF_ARGS).forEach(documentMethod); Map<String, Struct> inherited = new TreeMap<>(); type.struct.methods.values().stream().sorted(METHOD_NAME.thenComparing(NUMBER_OF_ARGS)).forEach(method -> { if (method.owner == type.struct) { documentMethod(typeStream, method); } else { inherited.put(method.owner.name, method.owner); } }); if (false == inherited.isEmpty()) { typeStream.print("* Inherits methods from "); boolean first = true; for (Struct inheritsFrom : inherited.values()) { if (first) { first = false; } else { typeStream.print(", "); } typeStream.print("++"); emitStruct(typeStream, inheritsFrom); typeStream.print("++"); } typeStream.println(); } } } } logger.info("Done writing [index.asciidoc]"); } private static void documentField(PrintStream stream, Field field) { stream.print("** [["); emitAnchor(stream, field); stream.print("]]"); if (Modifier.isStatic(field.modifiers)) { stream.print("static "); } emitType(stream, field.type); stream.print(' '); String javadocRoot = javadocRoot(field); emitJavadocLink(stream, javadocRoot, field); stream.print('['); stream.print(field.name); stream.print(']'); if (javadocRoot.equals("java8")) { stream.print(" ("); emitJavadocLink(stream, "java9", field); stream.print("[java 9])"); } stream.println(); } /** * Document a method. */ private static void documentMethod(PrintStream stream, Method method) { stream.print("* ++[["); emitAnchor(stream, method); stream.print("]]"); if (false == method.augmentation && Modifier.isStatic(method.modifiers)) { stream.print("static "); } if (false == method.name.equals("<init>")) { emitType(stream, method.rtn); stream.print(' '); } String javadocRoot = javadocRoot(method); emitJavadocLink(stream, javadocRoot, method); stream.print('['); stream.print(methodName(method)); stream.print("]("); boolean first = true; for (Type arg : method.arguments) { if (first) { first = false; } else { stream.print(", "); } emitType(stream, arg); } stream.print(")++"); if (javadocRoot.equals("java8")) { stream.print(" ("); emitJavadocLink(stream, "java9", method); stream.print("[java 9])"); } stream.println(); } /** * Anchor text for a {@link Struct}. */ private static void emitAnchor(PrintStream stream, Struct struct) { stream.print("painless-api-reference-"); stream.print(struct.name.replace('.', '-')); } /** * Anchor text for a {@link Method}. */ private static void emitAnchor(PrintStream stream, Method method) { emitAnchor(stream, method.owner); stream.print('-'); stream.print(methodName(method)); stream.print('-'); stream.print(method.arguments.size()); } /** * Anchor text for a {@link Field}. */ private static void emitAnchor(PrintStream stream, Field field) { emitAnchor(stream, field.owner); stream.print('-'); stream.print(field.name); } private static String methodName(Method method) { return method.name.equals("<init>") ? method.owner.name : method.name; } /** * Emit a {@link Type}. If the type is primitive or an array of primitives this just emits the name of the type. Otherwise this emits an * internal link with the text. */ private static void emitType(PrintStream stream, Type type) { emitStruct(stream, type.struct); for (int i = 0; i < type.dimensions; i++) { stream.print("[]"); } } /** * Emit a {@link Struct}. If the {@linkplain Struct} is primitive or def this just emits the name of the struct. Otherwise this emits an * internal link with the name. */ private static void emitStruct(PrintStream stream, Struct struct) { if (false == struct.clazz.isPrimitive() && false == struct.name.equals("def")) { stream.print("<<"); emitAnchor(stream, struct); stream.print(','); stream.print(struct.name); stream.print(">>"); } else { stream.print(struct.name); } } /** * Emit an external link to Javadoc for a {@link Method}. * * @param root name of the root uri variable */ private static void emitJavadocLink(PrintStream stream, String root, Method method) { stream.print("link:{"); stream.print(root); stream.print("-javadoc}/"); stream.print((method.augmentation ? Augmentation.class : method.owner.clazz).getName().replace('.', '/')); stream.print(".html#"); stream.print(methodName(method)); stream.print("%2D"); boolean first = true; if (method.augmentation) { first = false; stream.print(method.owner.clazz.getName()); } for (Type arg: method.arguments) { if (first) { first = false; } else { stream.print("%2D"); } stream.print(arg.struct.clazz.getName()); if (arg.dimensions > 0) { stream.print(":A"); } } stream.print("%2D"); } /** * Emit an external link to Javadoc for a {@link Field}. * * @param root name of the root uri variable */ private static void emitJavadocLink(PrintStream stream, String root, Field field) { stream.print("link:{"); stream.print(root); stream.print("-javadoc}/"); stream.print(field.owner.clazz.getName().replace('.', '/')); stream.print(".html#"); stream.print(field.javaName); } /** * Pick the javadoc root for a {@link Method}. */ private static String javadocRoot(Method method) { if (method.augmentation) { return "painless"; } return javadocRoot(method.owner); } /** * Pick the javadoc root for a {@link Field}. */ private static String javadocRoot(Field field) { return javadocRoot(field.owner); } /** * Pick the javadoc root for a {@link Struct}. */ private static String javadocRoot(Struct struct) { String classPackage = struct.clazz.getPackage().getName(); if (classPackage.startsWith("java")) { return "java8"; } if (classPackage.startsWith("org.elasticsearch.painless")) { return "painless"; } if (classPackage.startsWith("org.elasticsearch")) { return "elasticsearch"; } if (classPackage.startsWith("org.joda.time")) { return "joda-time"; } if (classPackage.startsWith("org.apache.lucene")) { return "lucene-core"; } throw new IllegalArgumentException("Unrecognized packge: " + classPackage); } private static void emitGeneratedWarning(PrintStream stream) { stream.println("////"); stream.println("Automatically generated by PainlessDocGenerator. Do not edit."); stream.println("Rebuild by running `gradle generatePainlessApi`."); stream.println("////"); stream.println(); } }