Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
io.hyperfoil.codegen.OpenapiMojo Maven / Gradle / Ivy
package io.hyperfoil.codegen;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.project.MavenProject;
import org.yaml.snakeyaml.Yaml;
import com.github.javaparser.StaticJavaParser;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Modifier;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.ConstructorDeclaration;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.body.MethodDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.MarkerAnnotationExpr;
import com.github.javaparser.ast.expr.Name;
import com.github.javaparser.ast.expr.NameExpr;
import com.github.javaparser.ast.expr.NormalAnnotationExpr;
import com.github.javaparser.ast.expr.SingleMemberAnnotationExpr;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.stmt.BlockStmt;
@Mojo(name = "codegen", defaultPhase = LifecyclePhase.GENERATE_SOURCES)
public class OpenapiMojo extends AbstractMojo {
private static final String COMPONENTS_SCHEMAS = "#/components/schemas/";
@Parameter(readonly = true, defaultValue = "${project}")
private MavenProject project;
@Parameter(defaultValue = "${project.basedir}/src/main/resources/openapi.yaml")
private String input;
@Parameter(defaultValue = "${project.build.directory}/generated-sources/java")
private String output;
@Parameter(required = true)
private String modelPackage;
@Parameter(required = true)
private String servicePackage;
@Parameter(required = true)
private String routerPackage;
@Parameter
private String defaultDateFormat;
@Parameter(defaultValue = "true")
private boolean addToCompileRoots;
@Override
public void execute() throws MojoExecutionException {
Yaml yaml = new Yaml();
Map openapi;
try (FileInputStream is = new FileInputStream(input)) {
openapi = yaml.load(is);
} catch (IOException e) {
throw new MojoExecutionException("Failed to read " + input, e);
}
ensureDir(new File(output), "Output directory");
ensureDir(Paths.get(output, modelPackage.split("\\.")).toFile(), "Package directory");
ensureDir(Paths.get(output, servicePackage.split("\\.")).toFile(), "Service directory");
ensureDir(Paths.get(output, routerPackage.split("\\.")).toFile(), "Router directory");
generateServiceAndRouter(openapi);
generateModel(openapi);
}
private void generateServiceAndRouter(Map openapi) throws MojoExecutionException {
ArrayList operations = new ArrayList<>();
Map paths = descend(openapi, false, "paths");
for (Map.Entry pathEntry : paths.entrySet()) {
String path = pathEntry.getKey();
@SuppressWarnings("unchecked")
Map> methods = (Map>) pathEntry.getValue();
for (Map.Entry> methodEntry : methods.entrySet()) {
String method = methodEntry.getKey();
Map properties = methodEntry.getValue();
String operationId = requireNonNull(properties, "operationId", path + "." + method);
ArrayList params = new ArrayList<>();
Map requestBodyContent = descend(properties, true, "requestBody", "content");
ArrayList consumes = new ArrayList<>();
if (requestBodyContent != null) {
consumes.addAll(requestBodyContent.keySet());
}
if (consumes.isEmpty()) {
consumes.add("");
}
ArrayList produces = new ArrayList<>();
Map> responses = requireNonNull(properties, "responses", path + "." + method);
for (Map response : responses.values()) {
@SuppressWarnings("unchecked")
Map content = (Map) response.get("content");
if (content != null) {
produces.addAll(content.keySet());
}
}
if (produces.isEmpty()) {
produces.add("");
}
@SuppressWarnings("unchecked")
List> parameters = (List>) properties.get("parameters");
if (parameters != null) {
for (Map param : parameters) {
String name = requireNonNull(param, "name", path, method, "parameters");
String in = requireNonNull(param, "in", path, method, "parameters");
boolean required = "true".equalsIgnoreCase(String.valueOf(param.get("required")));
Map schema = requireNonNull(param, "schema", path, method, name);
Property property = createProperty(path + "." + method + ".", name, schema);
Object defaultValue = schema.get("default");
params.add(new Param(property.originalName, property.fieldName, in, property.type, required,
defaultValue == null ? null : String.valueOf(defaultValue)));
}
}
for (String consume : consumes) {
for (String produce : produces) {
operations.add(
new Operation(path, method, operationId, consume, produce, consumes.size(), produces.size(), params));
}
}
}
}
writeApiService(operations);
writeApiRouter(operations);
}
private void writeApiRouter(ArrayList operations) throws MojoExecutionException {
CompilationUnit unit = new CompilationUnit(routerPackage);
unit.addImport("java.util.Date");
unit.addImport("java.util.List");
unit.addImport("java.util.Map");
unit.addImport("java.util.Collections");
unit.addImport("io.vertx.core.json.Json");
unit.addImport("io.vertx.ext.web.handler.BodyHandler");
unit.addImport("io.vertx.ext.web.Router");
unit.addImport("io.vertx.ext.web.RoutingContext");
unit.addImport("org.apache.logging.log4j.Logger");
unit.addImport("org.apache.logging.log4j.LogManager");
unit.addImport(modelPackage, false, true);
unit.addImport(servicePackage + ".ApiService");
ClassOrInterfaceDeclaration clazz = unit.addClass("ApiRouter", Modifier.Keyword.PUBLIC);
clazz.addField("ApiService", "service", Modifier.Keyword.PRIVATE, Modifier.Keyword.FINAL);
clazz.addField("Logger", "log", Modifier.Keyword.PRIVATE, Modifier.Keyword.STATIC, Modifier.Keyword.FINAL)
.getVariable(0).setInitializer("LogManager.getLogger(ApiRouter.class)");
ConstructorDeclaration ctor = clazz.addConstructor(Modifier.Keyword.PUBLIC);
BlockStmt ctorBody = ctor.addParameter("ApiService", "service").addParameter("Router", "router").getBody();
ctorBody.addStatement("this.service = service;");
ctorBody.addStatement("router.route().handler(BodyHandler.create(System.getProperty(\"java.io.tmpdir\")));");
ctorBody.addStatement("router.errorHandler(500, ctx -> {\n" +
" log.error(\"Error processing {} {}\", ctx.request().method(), ctx.request().uri(), ctx.failure());\n" +
" });");
for (Operation operation : operations) {
StringBuilder routing = new StringBuilder("router.").append(operation.method).append("(\"")
.append(operation.path.replaceAll("\\{", ":").replaceAll("\\}", ""))
.append("\")");
if (!operation.consumes.isEmpty()) {
routing.append(".consumes(\"").append(operation.consumes).append("\")");
}
if (!operation.produces.isEmpty()) {
routing.append(".produces(\"").append(operation.produces).append("\")");
}
routing.append(".handler(this::").append(operation.name()).append(");");
ctorBody.addStatement(routing.toString());
}
for (Operation operation : operations) {
MethodDeclaration method = clazz.addMethod(operation.name(), Modifier.Keyword.PRIVATE);
method.addParameter("RoutingContext", "ctx");
BlockStmt body = new BlockStmt();
method.setBody(body);
StringBuilder invocation = new StringBuilder("service.").append(operation.name()).append("(ctx");
for (Param param : operation.params) {
StringBuilder raw = new StringBuilder();
if (param.in.equals("query") && param.type.replaceAll("<.*>", "").equals("List")) {
raw.append("List _").append(param.varName).append(" = ")
.append(param.in).append("Params(ctx, \"").append(param.originalName).append("\");");
} else {
raw.append("String _").append(param.varName).append(" = ")
.append(param.in).append("Param(ctx, \"").append(param.originalName).append("\", ");
if (param.defaultValue == null) {
raw.append("null");
} else {
raw.append('"').append(param.defaultValue).append('"');
}
raw.append(");");
}
body.addStatement(raw.toString());
if (param.required) {
body.addStatement("if (_" + param.varName + " == null) {" +
"ctx.response().setStatusCode(400).end(\"" + param.in + " parameter '" + param.originalName
+ "' was not set!\");" +
"return; }");
}
body.addStatement(new StringBuilder().append(param.type).append(" ").append(param.varName)
.append(" = convert(_").append(param.varName).append(", ").append(param.type.replaceAll("<.*>", ""))
.append(".class);").toString());
invocation.append(", ").append(param.varName);
}
invocation.append(");");
body.addStatement(invocation.toString());
}
MethodDeclaration pathParam = clazz.addMethod("pathParam", Modifier.Keyword.PRIVATE).setType("String")
.addParameter("RoutingContext", "ctx")
.addParameter("String", "name")
.addParameter("String", "defaultValue");
pathParam.setBody(new BlockStmt().addStatement("return ctx.pathParam(name);"));
MethodDeclaration queryParam = clazz.addMethod("queryParam", Modifier.Keyword.PRIVATE).setType("String")
.addParameter("RoutingContext", "ctx")
.addParameter("String", "name")
.addParameter("String", "defaultValue");
BlockStmt queryParamBody = new BlockStmt()
.addStatement("List list = ctx.queryParam(name);")
.addStatement("if (list == null || list.isEmpty()) return defaultValue;")
.addStatement("return list.iterator().next();");
queryParam.setBody(queryParamBody);
MethodDeclaration queryParams = clazz.addMethod("queryParams", Modifier.Keyword.PRIVATE).setType("List")
.addParameter("RoutingContext", "ctx")
.addParameter("String", "name");
BlockStmt queryParamsBody = new BlockStmt()
.addStatement("List list = ctx.queryParam(name);")
.addStatement("return list;");
queryParams.setBody(queryParamsBody);
MethodDeclaration headerParam = clazz.addMethod("headerParam", Modifier.Keyword.PRIVATE).setType("String")
.addParameter("RoutingContext", "ctx")
.addParameter("String", "name")
.addParameter("String", "defaultValue");
headerParam.setBody(new BlockStmt()
.addStatement("String value = ctx.request().getHeader(name);")
.addStatement("return value == null ? defaultValue : value;"));
MethodDeclaration convert = clazz.addMethod("convert");
convert.addAnnotation(new SingleMemberAnnotationExpr(new Name("SuppressWarnings"), new StringLiteralExpr("unchecked")));
convert.addParameter("String", "value").addParameter("Class", "type").addTypeParameter("T").setType("T");
BlockStmt convertBody = new BlockStmt();
convert.setBody(convertBody);
convertBody.addStatement("if (type == String.class) return (T) value;");
convertBody.addStatement("if (type == boolean.class) return (T) Boolean.valueOf(value);");
convertBody.addStatement("if (type == int.class) return (T) Integer.valueOf(value);");
convertBody.addStatement(
"if (type == List.class) { if (value == null) return (T) Collections.emptyList(); if (value instanceof String) return (T) Collections.singletonList(value); }");
convertBody.addStatement("if (value == null) { if (type == Map.class) return (T) Collections.emptyMap(); return null; }");
convertBody.addStatement("return Json.decodeValue(value, type);");
MethodDeclaration convertList = clazz.addMethod("convert");
convertList
.addAnnotation(new SingleMemberAnnotationExpr(new Name("SuppressWarnings"), new StringLiteralExpr("unchecked")));
convertList.addParameter("List", "value").addParameter("Class", "type").addTypeParameter("T").setType("T");
BlockStmt convertListBody = new BlockStmt();
convertList.setBody(convertListBody);
convertListBody.addStatement("if (value == null) return (T) Collections.emptyList();");
convertListBody.addStatement("return (T) value;");
writeUnit(unit, routerPackage, "ApiRouter.java");
}
private void writeApiService(ArrayList operations) throws MojoExecutionException {
CompilationUnit unit = new CompilationUnit(servicePackage);
unit.addImport(modelPackage, false, true);
unit.addImport("io.vertx.ext.web.RoutingContext");
unit.addImport("java.util", false, true);
ClassOrInterfaceDeclaration clazz = unit.addClass("ApiService");
clazz.setInterface(true);
for (Operation operation : operations) {
MethodDeclaration method = clazz.addMethod(operation.name());
method.setBody(null);
method.addParameter("RoutingContext", "ctx");
for (Param param : operation.params) {
method.addParameter(param.type, param.varName);
}
}
writeUnit(unit, servicePackage, "ApiService.java");
}
private void writeUnit(CompilationUnit unit, String pkg, String className) throws MojoExecutionException {
Path apiServicePath = Paths.get(output, pkg.split("\\.")).resolve(className);
try {
Files.write(apiServicePath, unit.toString().getBytes(StandardCharsets.UTF_8));
} catch (IOException e) {
throw new MojoExecutionException("Cannot write file " + apiServicePath, e);
}
}
private T requireNonNull(Map map, String key, String... where) throws MojoExecutionException {
@SuppressWarnings("unchecked")
T value = (T) map.get(key);
if (value == null) {
throw new MojoExecutionException(String.join(".", where) + " missing '" + key + "'");
}
return value;
}
private void generateModel(Map openapi) throws MojoExecutionException {
Map schemas = descend(openapi, false, "components", "schemas");
for (Map.Entry entry : schemas.entrySet()) {
@SuppressWarnings("unchecked")
Map value = (Map) entry.getValue();
Object type = value.get("type");
if (!"object".equals(type)) {
throw new MojoExecutionException(entry.getKey() + " is not of type 'object': " + type);
}
@SuppressWarnings("unchecked")
Map properties = (Map) value.get("properties");
if (properties == null) {
throw new MojoExecutionException(entry.getKey() + " does not have defined 'properties'");
}
generateType(entry.getKey(), properties);
}
if (addToCompileRoots) {
project.addCompileSourceRoot(output);
}
}
private Map descend(Map root, boolean allowMissing, String... path)
throws MojoExecutionException {
Map target = root;
for (String element : path) {
@SuppressWarnings("unchecked")
Map value = (Map) target.get(element);
if (value == null) {
if (allowMissing) {
return null;
} else {
throw new MojoExecutionException("Cannot descend path " + String.join(".", path));
}
}
target = value;
}
return target;
}
private void ensureDir(File file, String description) throws MojoExecutionException {
if (file.exists()) {
if (!file.isDirectory()) {
throw new MojoExecutionException(description + " " + file + " is not a directory.");
}
} else if (!file.mkdirs()) {
throw new MojoExecutionException("Cannot create directory " + file);
}
}
private void generateType(String name, Map propertyMap) throws MojoExecutionException {
CompilationUnit unit = new CompilationUnit(modelPackage);
unit.addImport("java.util.Date");
unit.addImport("java.util.List");
unit.addImport("com.fasterxml.jackson.annotation.JsonCreator");
unit.addImport("com.fasterxml.jackson.annotation.JsonFormat");
unit.addImport("com.fasterxml.jackson.annotation.JsonInclude");
unit.addImport("com.fasterxml.jackson.annotation.JsonProperty");
ClassOrInterfaceDeclaration clazz = unit.addClass(name, Modifier.Keyword.PUBLIC);
List properties = propertyMap.entrySet().stream()
.map(e -> {
@SuppressWarnings("unchecked")
Map from = (Map) e.getValue();
return createProperty(name, e.getKey(), from);
})
.collect(Collectors.toList());
for (Property property : properties) {
FieldDeclaration fieldDeclaration = clazz.addField(property.type, property.fieldName, Modifier.Keyword.PUBLIC,
Modifier.Keyword.FINAL);
property.fieldAnnotations.forEach(fieldDeclaration::addAnnotation);
}
ConstructorDeclaration ctor = clazz.addConstructor(Modifier.Keyword.PUBLIC);
ctor.addAnnotation(new MarkerAnnotationExpr("JsonCreator"));
BlockStmt ctorBody = new BlockStmt();
for (Property property : properties) {
com.github.javaparser.ast.body.Parameter p = new com.github.javaparser.ast.body.Parameter(
StaticJavaParser.parseType(property.type), property.fieldName);
p.addAnnotation(new SingleMemberAnnotationExpr(new Name("JsonProperty"), new StringLiteralExpr(property.fieldName)));
ctor.addParameter(p);
ctorBody.addStatement("this." + property.fieldName + " = " + property.fieldName + ";");
}
ctor.setBody(ctorBody);
writeUnit(unit, modelPackage, name + ".java");
}
private Property createProperty(String name, String propertyName, Map from) {
String ref = (String) from.get("$ref");
if (from.get("type") == null && ref == null) {
throw fail(name, propertyName, "Either 'type' or '$ref' must be defined.");
}
if (ref != null) {
if (!ref.startsWith(COMPONENTS_SCHEMAS)) {
throw fail(name, propertyName, "Invalid reference to " + ref + " (should start with " + COMPONENTS_SCHEMAS + ")");
}
return new Property(propertyName, sanitizeProperty(propertyName), ref.substring(COMPONENTS_SCHEMAS.length()),
Collections.emptyList());
}
String type = (String) from.get("type");
String propertyType;
String format = (String) from.get("format");
ArrayList fieldAnnotations = new ArrayList<>();
switch (type) {
case "string":
if (format == null) {
propertyType = "String";
} else if (format.equals("date-time")) {
propertyType = "Date";
if (defaultDateFormat != null) {
fieldAnnotations.add(new NormalAnnotationExpr()
.addPair("shape", "JsonFormat.Shape.STRING")
.addPair("pattern", new StringLiteralExpr(defaultDateFormat))
.setName("JsonFormat"));
}
} else {
throw fail(name, propertyName, "Unknown string format " + format);
}
break;
case "array":
@SuppressWarnings("unchecked")
Map items = (Map) from.get("items");
if (items == null) {
throw fail(name, propertyName, "Missing 'items'");
}
Property item = createProperty(name, propertyName + ".items", items);
propertyType = "List<" + item.type + ">";
break;
case "boolean":
propertyType = "boolean";
break;
case "integer":
case "number":
propertyType = format == null ? "int" : format;
break;
case "object":
String externalType = (String) from.get("x-type");
if (externalType == null) {
throw fail(name, propertyName, "Nested objects are not supported; use $ref.");
} else {
propertyType = externalType;
break;
}
default:
throw fail(name, propertyName, "Unknown type " + type);
}
String jsonInclude = (String) from.get("x-json-include");
if (jsonInclude != null) {
fieldAnnotations.add(
new SingleMemberAnnotationExpr(new Name("JsonInclude"), new NameExpr("JsonInclude.Include." + jsonInclude)));
}
return new Property(propertyName, sanitizeProperty(propertyName), propertyType, fieldAnnotations);
}
private String sanitizeProperty(String name) {
StringBuilder sb = new StringBuilder();
boolean upperCase = false;
for (int i = 0; i < name.length(); ++i) {
char c = name.charAt(i);
if (Character.isAlphabetic(c) || Character.isDigit(c)) {
sb.append(upperCase ? Character.toUpperCase(c) : c);
upperCase = false;
} else if (c == '-') {
upperCase = true;
} else {
sb.append('_');
upperCase = false;
}
}
return sb.toString();
}
private RuntimeException fail(String name, String propertyName, String msg) {
return new RuntimeException(new MojoExecutionException(name + "." + propertyName + ": " + msg));
}
private static class Property {
private final String originalName;
private final String fieldName;
private final String type;
private final List fieldAnnotations;
private Property(String originalName, String fieldName, String type, List fieldAnnotations) {
this.originalName = originalName;
this.fieldName = fieldName;
this.type = type;
this.fieldAnnotations = fieldAnnotations;
}
}
private static class Operation {
private final String path;
private final String method;
private final String operationId;
private final String consumes;
private final String produces;
private final int numConsumes;
private final int numProduces;
private final List params;
private final String methodName;
private Operation(String path, String method, String operationId, String consumes, String produces, int numConsumes,
int numProduces, List params) {
this.path = path;
this.method = method;
this.operationId = operationId;
this.consumes = consumes;
this.produces = produces;
this.numConsumes = numConsumes;
this.numProduces = numProduces;
this.params = params;
this.methodName = operationId + (numConsumes > 1 ? "$" + consumes.replaceAll("[-./*]", "_") : "")
+ (numProduces > 1 ? "$" + produces.replaceAll("[-./*]", "_") : "");
}
public String name() {
return methodName;
}
}
private static class Param {
private final String originalName;
private final String varName;
private final String in;
private final String type;
private final boolean required;
private final String defaultValue;
private Param(String originalName, String varName, String in, String type, boolean required, String defaultValue) {
this.originalName = originalName;
this.varName = varName;
this.in = in;
this.type = type;
this.required = required;
this.defaultValue = defaultValue;
}
}
}