Generator.java
- /*
- * The coLAB project
- * Copyright (C) 2021-2023 AlbaSim, MEI, HEIG-VD, HES-SO
- *
- * Licensed under the MIT License
- */
- package ch.colabproject.colab.generator.plugin;
- import ch.colabproject.colab.generator.model.interfaces.WithId;
- import ch.colabproject.colab.generator.model.interfaces.WithJsonDiscriminator;
- import ch.colabproject.colab.generator.model.tools.ClassDoc;
- import ch.colabproject.colab.generator.model.tools.JavaDocExtractor;
- import ch.colabproject.colab.generator.model.tools.JsonbProvider;
- import ch.colabproject.colab.generator.plugin.rest.RestEndpoint;
- import java.io.BufferedWriter;
- import java.io.IOException;
- import java.lang.reflect.Type;
- import java.nio.file.Files;
- import java.util.AbstractMap.SimpleEntry;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.HashMap;
- import java.util.List;
- import java.util.Map;
- import java.util.Map.Entry;
- import java.util.Set;
- import java.util.stream.Collectors;
- import javax.json.bind.Jsonb;
- import javax.ws.rs.ApplicationPath;
- import javax.ws.rs.Path;
- import org.apache.maven.plugin.MojoFailureException;
- import org.reflections.Reflections;
- /**
- * @author maxence
- */
- public class Generator {
- /**
- * Reflections one-stop-shop object.
- */
- private final Reflections reflections;
- /**
- * All rest controller found.
- */
- private Set<RestEndpoint> restEndpoints;
- /**
- * Client will be generated in this package
- */
- private final String packageName;
- /**
- * Generated client name
- */
- private final String clientName;
- /**
- * Javadoc as extracted by the annotation processor
- */
- private final Map<String, ClassDoc> javadoc;
- /**
- * Main REST application path
- */
- private String applicationPath = null;
- /**
- * Initialize the client generator.
- *
- * @param restPackages packages to analyze
- * @param packageName package in which generate the client
- * @param clientName client class name
- */
- public Generator(String[] restPackages,
- String packageName, String clientName
- ) {
- this.packageName = packageName;
- this.clientName = clientName;
- List<Object> pkgs = new ArrayList<>();
- pkgs.add("ch.colabproject.colab.generator.model");
- pkgs.addAll(Arrays.asList(restPackages));
- this.reflections = new Reflections(pkgs.toArray());
- this.javadoc = JavaDocExtractor.loadJavaDocFromJson();
- }
- /**
- * Initialize the client generator.
- *
- * @param pkgs packages to analyze
- */
- public Generator(String[] pkgs) {
- this(pkgs, null, null);
- }
- /**
- * get JSON-B to use.
- *
- * @return jsbonb mapper
- */
- public Jsonb getJsonBMapper() {
- return JsonbProvider.getJsonb();
- }
- /**
- * Process all classes annotated with {@link Path}. Generate a {@link RestEndpoint} instance for
- * each class and store them in {@link #restEndpoints}
- */
- public void processPackages() {
- Set<Class<?>> appConfig = reflections.getTypesAnnotatedWith(ApplicationPath.class);
- if (!appConfig.isEmpty()) {
- if (appConfig.size() > 1) {
- Logger.warn("Several ApplicationPath found");
- }
- Class<?> applicationConfig = appConfig.iterator().next();
- ApplicationPath annotation = applicationConfig.getAnnotation(ApplicationPath.class);
- if (annotation != null) {
- this.applicationPath = annotation.value();
- }
- }
- Set<Class<?>> restClasses = reflections.getTypesAnnotatedWith(Path.class);
- this.restEndpoints = restClasses.stream()
- .map(klass -> RestEndpoint.build(klass, applicationPath))
- .collect(Collectors.toSet());
- /*
- * .map(p -> p.generateJavaClient()) .innerClasses(Collectors.toList());
- */
- }
- /**
- * One all classes have been processed with {@link #processPackages() }, this method will create
- * subdirectories that match the <code>packageName</code>.Then the client will be generated in
- * the <code>clientName</code>.java file.
- *
- * @param targetDir target directory
- * @param dryRun if true, do not generate files but print output to console
- *
- * @throws org.apache.maven.plugin.MojoFailureException if generation fails
- */
- public void generateJavaClient(String targetDir, boolean dryRun) throws MojoFailureException {
- Map<String, String> imports = new HashMap<>();
- imports.put("RestClient", "ch.colabproject.colab.generator.plugin.rest.RestClient");
- imports.put("Jsonb", "javax.json.bind.Jsonb");
- imports.put("GenericType", "javax.ws.rs.core.GenericType");
- imports.put("PathPattern", "org.glassfish.jersey.uri.PathPattern");
- imports.put("UriTemplate", "org.glassfish.jersey.uri.UriTemplate");
- imports.put("void", null); // null means no import statement
- String innerClasses = this.restEndpoints.stream().map(controller -> {
- String javaCode = controller.generateJavaClient(
- imports,
- clientName,
- javadoc,
- reflections
- );
- return javaCode;
- }).collect(Collectors.joining(System.lineSeparator()));
- StringBuilder sb = new StringBuilder();
- sb.append("package ").append(packageName).append(";\n\n")
- .append(
- imports.values().stream()
- .filter(pkg -> pkg != null)
- .filter(pkg -> !pkg.startsWith("java.lang")) // do not import java.lang
- .sorted()
- .map(pkg -> "import " + pkg + ";")
- .collect(Collectors.joining(System.lineSeparator()))
- )
- .append("\n"
- + "/**\n"
- + " * The ")
- .append(clientName)
- .append(" REST client"
- + " */\n"
- + "@SuppressWarnings(\"PMD.FieldDeclarationsShouldBeAtStartOfClass\")\n"
- + "public class ")
- .append(clientName)
- .append(" extends RestClient {"
- + "\n"
- + "\n"
- + " /**\n"
- + " * Get a REST client\n"
- + " *\n"
- + " * @param baseUri base URI\n"
- + " * @param cookieName session cookie name\n"
- + " * @param jsonb jsonb\n"
- + " * @param clientFeatures addition http client feature\n"
- + " */\n"
- + " public ")
- .append(clientName)
- .append("(String baseUri, String cookieName, Jsonb jsonb, Object... clientFeatures) {\n"
- + " super(baseUri, cookieName, jsonb, clientFeatures);\n"
- + " }")
- .append(innerClasses)
- .append("}");
- if (dryRun) {
- Logger.debug(sb.toString());
- } else {
- String packagePath = targetDir + "/" + packageName.replaceAll("\\.", "/");
- writeFile(sb.toString(), packagePath, clientName + ".java");
- }
- }
- /**
- * Get rest service description
- *
- * @return all rest resource
- */
- public Set<RestEndpoint> getRestEndpoints() {
- return restEndpoints;
- }
- /**
- * Generate typescript client in targetDir
- *
- * @param targetDir generate TS module in this directory
- * @param dryRun if true, do not generate any file but print output to console
- *
- * @throws org.apache.maven.plugin.MojoFailureException if generation fails
- */
- public void generateTypescriptClient(String targetDir, boolean dryRun)
- throws MojoFailureException {
- Map<String, Type> extraTypes = new HashMap<>();
- StringBuilder sb = new StringBuilder();
- sb.append(this.getTsClientTemplate());
- extraTypes.put("WithJsonDiscriminator", WithJsonDiscriminator.class);
- extraTypes.put("WithId", WithId.class);
- String modules = this.restEndpoints.stream().map(
- controller -> controller.generateTypescriptClient(extraTypes, this.javadoc, reflections)
- ).collect(Collectors.joining(System.lineSeparator()));
- // TS interface name => list of @class values
- Map<String, List<String>> inheritance = new HashMap<>();
- List<Entry<String, Type>> queue = new ArrayList<>(extraTypes.entrySet());
- while (!queue.isEmpty()) {
- Map<String, Type> snowballedTypes = new HashMap<>();
- Entry<String, Type> entry = queue.remove(0);
- String tsInterface = TypeScriptHelper.generateInterface(
- entry.getValue(),
- snowballedTypes,
- inheritance,
- reflections,
- javadoc
- );
- sb.append(tsInterface);
- snowballedTypes.forEach((name, type) -> {
- if (!extraTypes.containsKey(name)) {
- extraTypes.put(name, type);
- queue.add(0, new SimpleEntry<>(name, type));
- }
- });
- }
- sb.append("/**\n"
- + " * Some orthopedic tools\n"
- + " */\n\n"
- + "export interface TypeMap {\n ")
- .append(
- inheritance.keySet().stream().map((key) -> key + ": " + key + ";")
- .collect(Collectors.joining("\n "))
- )
- .append("\n}\n\n")
- .append("const inheritance : {[key: string]: string[]} = {\n")
- .append(
- inheritance.entrySet().stream()
- .map((entry) -> entry.getKey() + ": [" + entry.getValue().stream()
- .map(v -> "'" + v + "'")
- .collect(Collectors.joining(", ")) + "]"
- ).collect(Collectors.joining(",\n ")))
- .append("\n}\n\n")
- .append("export const entityIs = <T extends keyof TypeMap>(entity: unknown, klass: T)\n"
- + " : entity is TypeMap[T] => {\n"
- + "\n"
- + " if (typeof entity === 'object' && entity != null) {\n"
- + " if (\"@class\" in entity) {\n"
- + " const dis = entity[\"@class\"];\n"
- + " if (typeof dis === 'string') {\n"
- + " return inheritance[klass].includes(dis);\n"
- + " }\n"
- + " }\n"
- + " }\n"
- + " return false;\n"
- + "}")
- .append("\n\n\n/**\n"
- + "* The ")
- .append(clientName).append(" REST client\n"
- + " */\n"
- + "export const ")
- .append(clientName)
- .append(
- " = function (baseUrl: string, defaultErrorHandler: (error: unknown) => void) {")
- .append("\n return {")
- .append(modules)
- .append(" }\n")
- .append("}");
- if (dryRun) {
- Logger.debug(sb.toString());
- } else {
- String srcPath = targetDir + "/src";
- writeFile(sb.toString(), srcPath, clientName + ".ts");
- writeFile("export * from './dist/" + clientName + "';", targetDir, "index.ts");
- writeFile(generatePackageDotJson(), targetDir, "package.json");
- writeFile(generateTsconfigDotJson(), targetDir, "tsconfig.json");
- }
- }
- /**
- * Write file to disk.
- *
- * @param content file content
- * @param directory directory, will be created if missing
- * @param filename filename
- */
- private void writeFile(String content, String directory, String filename)
- throws MojoFailureException {
- try {
- Files.createDirectories(java.nio.file.Path.of(directory));
- try (BufferedWriter writer = Files.newBufferedWriter(
- java.nio.file.Path.of(directory, filename))) {
- writer.write(content);
- } catch (IOException ex) {
- if (Logger.isInit()) {
- throw new MojoFailureException("Failed to write '"
- + filename + "' in '" + directory + "'", ex);
- }
- }
- } catch (IOException ex) {
- if (Logger.isInit()) {
- throw new MojoFailureException("Failed to create package directory "
- + directory, ex);
- }
- }
- }
- /**
- * Convert Java className-like to dash-separated-lower-case version.
- *
- * @param name eg. MyAwesomeRestClient
- *
- * @return eg. my-awesome-rest-client
- */
- private String generateModuleName(String name) {
- return name
- .trim()
- // prefix all uppercase char preceded by something with an dash
- .replaceAll("(?<!^)[A-Z](?!$)", "-$0")
- .toLowerCase();
- }
- /**
- * get TS client tempalte
- *
- * @return intial content of the TS client
- */
- private String getTsClientTemplate() {
- String tsConfig = FileHelper.readFile("templates/client.ts");
- return tsConfig.replaceAll(
- "\\{\\{MODULE_NAME\\}\\}",
- generateModuleName(clientName)
- );
- }
- /**
- * generate package.json
- *
- * @return content of package.json
- */
- private String generatePackageDotJson() {
- String tsConfig = FileHelper.readFile("templates/package.json");
- return tsConfig.replaceAll(
- "\\{\\{MODULE_NAME\\}\\}",
- generateModuleName(clientName)
- );
- }
- /**
- * generate tsconfig.json
- *
- * @return content of package.json
- */
- private String generateTsconfigDotJson() {
- String tsConfig = FileHelper.readFile("templates/tsconfig.json");
- return tsConfig.replaceAll("\\{\\{CLIENT_NAME\\}\\}", this.clientName);
- }
- }