All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.oneandone.neberus.Neberus Maven / Gradle / Ivy

package net.oneandone.neberus;

import jdk.javadoc.doclet.Doclet;
import jdk.javadoc.doclet.DocletEnvironment;
import jdk.javadoc.doclet.Reporter;
import net.oneandone.neberus.annotation.ApiDocumentation;
import net.oneandone.neberus.annotation.ApiUsecase;
import net.oneandone.neberus.annotation.ApiUsecases;
import net.oneandone.neberus.parse.ClassParser;
import net.oneandone.neberus.parse.JavaxWsRsClassParser;
import net.oneandone.neberus.parse.JavaxWsRsMethodParser;
import net.oneandone.neberus.parse.RestClassData;
import net.oneandone.neberus.parse.RestMethodData;
import net.oneandone.neberus.parse.RestUsecaseData;
import net.oneandone.neberus.parse.SpringMvcClassParser;
import net.oneandone.neberus.parse.SpringMvcMethodParser;
import net.oneandone.neberus.parse.UsecaseParser;
import net.oneandone.neberus.print.DocPrinter;
import net.oneandone.neberus.print.openapiv3.OpenApiV3JsonPrinter;
import net.oneandone.neberus.shortcode.ShortCodeExpander;
import net.oneandone.neberus.util.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.reflections.Reflections;

import javax.lang.model.SourceVersion;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.tools.FileObject;
import javax.tools.StandardLocation;
import javax.ws.rs.Path;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static net.oneandone.neberus.util.JavaDocUtils.getExecutableElements;
import static net.oneandone.neberus.util.JavaDocUtils.getPackageName;
import static net.oneandone.neberus.util.JavaDocUtils.getTypeElements;
import static net.oneandone.neberus.util.JavaDocUtils.hasAnnotation;

public class Neberus implements Doclet {

    private final Options options = new Options();

    @Override
    public boolean run(DocletEnvironment environment) {
        System.out.println("Neberus running");
        options.environment = environment;

        ShortCodeExpander expander = new ShortCodeExpander();
        List modules = loadModules(expander, options);
        DocPrinter docPrinter = new OpenApiV3JsonPrinter(modules, expander, options);

        ClassParser javaxWsRsParser = new JavaxWsRsClassParser(new JavaxWsRsMethodParser(options));
        ClassParser springMvcParser = new SpringMvcClassParser(new SpringMvcMethodParser(options));
        UsecaseParser usecaseParser = new UsecaseParser(options);

        List typeElements = getTypeElements(environment);

        List restClasses = new ArrayList<>();
        List restUsecases = new ArrayList<>();

        String packageDoc = null;

        List filteredClasses = typeElements.stream()
                .filter(typeElement -> options.scanPackages.stream()
                        .anyMatch(pack -> getPackageName(typeElement, environment).startsWith(pack)))
                .collect(Collectors.toList());


        for (TypeElement typeElement : filteredClasses) {

            if (!typeElement.getKind().isInterface() && hasAnnotation(typeElement, ApiDocumentation.class, environment)) {
                System.out.println("Parsing " + typeElement);
                if (StringUtils.isBlank(packageDoc)) {
                    try {
                        PackageElement packageElement = environment.getElementUtils().getPackageOf(typeElement);
                        FileObject fileForInput = environment.getJavaFileManager().getFileForInput(StandardLocation.SOURCE_PATH,
                                packageElement.getQualifiedName().toString(), "package.html");

                        if (fileForInput != null) {
                            packageDoc = environment.getDocTrees().getDocCommentTree(fileForInput).toString();
                        }
                    } catch (IOException e) {
                        System.err.println(e.toString());
                    }
                }

                RestClassData restClassData;

                if (usesJavaxWsRs(typeElement, options)) {
                    restClassData = javaxWsRsParser.parse(typeElement);
                } else {
                    restClassData = springMvcParser.parse(typeElement);
                }

                restClassData.validate(options.ignoreErrors);
                restClasses.add(restClassData);

            }

            modules.forEach(module -> module.parse(typeElement));
        }

        for (TypeElement typeElement : filteredClasses) {
            if (hasAnnotation(typeElement, ApiUsecase.class, environment)
                    || hasAnnotation(typeElement, ApiUsecases.class, environment)) {
                RestUsecaseData restUsecaseData = usecaseParser.parse(typeElement, restClasses);
                restUsecaseData.validate(options.ignoreErrors);
                restUsecases.add(restUsecaseData);
            }
        }

        validateMultipleMethodsForSameHttpMethodAndPath(restClasses, options);

        restClasses.forEach(restClassData -> {
            docPrinter.printRestClassFile(restClassData, restClasses, restUsecases);
        });

        modules.forEach(NeberusModule::print);

        docPrinter.printIndexFile(restClasses, restUsecases, packageDoc);

        URL bootstrapUrl = Neberus.class.getResource("/generated");
        File dest = new File(options.outputDirectory + options.docBasePath);

        System.out.println("Copying static resources");
        FileUtils.copyResourcesRecursively(bootstrapUrl, dest);
        System.out.println("View generated docs: file://" + new File(dest, "index.html").getAbsolutePath().replace("/./", "/"));
        System.out.println("Neberus finished");
        return true;
    }

    private static List loadModules(ShortCodeExpander expander, Options options) {
        Reflections reflections = new Reflections();
        Set> moduleClasses = reflections.getSubTypesOf(NeberusModule.class);

        if (moduleClasses.isEmpty()) {
            return Collections.emptyList();
        }

        String moduleNames = moduleClasses.stream().map(Class::getSimpleName).collect(Collectors.joining(", "));
        System.out.println("Loading modules " + moduleNames);

        return moduleClasses.stream().map(clazz -> {
            try {
                return clazz.getConstructor(ShortCodeExpander.class, Options.class).newInstance(expander, options);
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                System.err.println("Can't load module " + clazz.getName() + ": " + e);
            }
            return null;
        }).filter(Objects::nonNull).collect(Collectors.toList());
    }

    private static boolean usesJavaxWsRs(TypeElement typeElement, Options options) {
        if (hasAnnotation(typeElement, Path.class, options.environment)) {
            return true;
        }

        return getExecutableElements(typeElement).stream()
                .anyMatch(method -> hasAnnotation(method, Path.class, options.environment));
    }

    private static void validateMultipleMethodsForSameHttpMethodAndPath(List restClasses, Options options) {
        Map> methodsByHttpMethodAndPath = new HashMap<>();

        restClasses.stream().flatMap(rc -> rc.methods.stream()).forEach(method -> {
            String methodAndPath = method.methodData.httpMethod + " - " + method.methodData.path;
            methodsByHttpMethodAndPath.computeIfAbsent(methodAndPath, k -> new ArrayList<>()).add(method.methodData);
        });

        // validate
        methodsByHttpMethodAndPath.entrySet().stream()
                .filter(e -> e.getValue().size() > 1)
                .forEach(e -> {
                    System.err.println("Found multiple methods with the same HttpMethod and path <" + e.getKey() + ">. "
                            + "The documentation for all of them must be placed onto one method and all others must be "
                            + "excluded from the Apidoc with @ApiIgnore.");

                    if (!options.ignoreErrors) {
                        throw new IllegalArgumentException();
                    }
                });

    }

    @Override
    public void init(Locale locale, Reporter reporter) {
        // noop
    }

    @Override
    public String getName() {
        return getClass().getSimpleName();
    }

    @Override
    public Set getSupportedOptions() {
        return Set.of(
                // An option that takes no arguments.
                new DocletOption("-ignoreErrors", false, "Ignore generation errors.", null) {
                    @Override
                    public boolean process(String option, List arguments) {
                        options.ignoreErrors = true;
                        return true;
                    }
                },
                new DocletOption("-d", true, "outputDirectory", "") {
                    @Override
                    public boolean process(String option, List arguments) {
                        options.outputDirectory = arguments.get(0) + "/";
                        return true;
                    }
                },
                new DocletOption("--docBasePath", true,
                        "Root path where the generated documentation is placed inside reportOutputDirectory.", "") {
                    @Override
                    public boolean process(String option, List arguments) {
                        options.docBasePath = arguments.get(0);
                        return true;
                    }
                },
                new DocletOption("--apiVersion", true, "Api version.", "") {
                    @Override
                    public boolean process(String option, List arguments) {
                        options.apiVersion = arguments.get(0);
                        return true;
                    }
                },
                new DocletOption("--apiTitle", true, "Api Title.", "") {
                    @Override
                    public boolean process(String option, List<String> arguments) {
                        options.apiTitle = arguments.get(0);
                        return true;
                    }
                },
                new DocletOption("--apiBasePath", true, "Root path of the Api on the server (e.g. '/rest').", "<path>") {
                    @Override
                    public boolean process(String option, List<String> arguments) {
                        options.apiBasePath = arguments.get(0);
                        return true;
                    }
                },
                new DocletOption("--apiHosts", true, "List of hosts where the Api can be accessed, separated by semicolon (;). "
                        + "Description for each host can be provided inside optional trailing brackets. "
                        + "Example: \"https://testserver.com[the default testserver];https://otherserver.com[the other testserver]\"",
                        "<host[description]>(;<host[description]>)*") {
                    @Override
                    public boolean process(String option, List<String> arguments) {
                        options.apiHosts = Arrays.asList(arguments.get(0).split(";"));
                        return true;
                    }
                },
                new DocletOption("--scanPackages", true, "List of packages that include classes relevant for the apidoc",
                        "<package>(;<package>)*") {
                    @Override
                    public boolean process(String option, List<String> arguments) {
                        options.scanPackages = new HashSet<>(Arrays.asList(arguments.get(0).split(";")));
                        return true;
                    }
                },
                new DocletOption("--markup", true, "Global markup option. Valid for all descriptions and used javadoc. "
                        + "Default: HTML.",
                        "[HTML|MARKDOWN|ASCIIDOC]") {
                    @Override
                    public boolean process(String option, List<String> arguments) {
                        options.markup = Options.Markup.valueOf(arguments.get(0));
                        return true;
                    }
                });

    }

    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.RELEASE_11;
    }

}
</code></pre>    <br/>
    <br/>
<div class='clear'></div>
</main>
</div>
<br/><br/>
    <div class="align-center">© 2015 - 2025 <a href="/legal-notice.php">Weber Informatics LLC</a> | <a href="/data-protection.php">Privacy Policy</a></div>
<br/><br/><br/><br/><br/><br/>
</body>
</html>