RestEndpoint.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.rest;
import ch.colabproject.colab.generator.model.annotations.AdminResource;
import ch.colabproject.colab.generator.model.annotations.AuthenticationRequired;
import ch.colabproject.colab.generator.model.exceptions.HttpException;
import ch.colabproject.colab.generator.model.tools.ClassDoc;
import ch.colabproject.colab.generator.plugin.Logger;
import ch.colabproject.colab.generator.plugin.TypeScriptHelper;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import javax.ws.rs.Consumes;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.glassfish.jersey.media.multipart.FormDataParam;
import org.glassfish.jersey.uri.PathPattern;
import org.glassfish.jersey.uri.UriTemplate;
import org.reflections.Reflections;
/**
* Represent a rest controller.
*
* @author maxence
*/
public class RestEndpoint {
/**
* Full class name.
*/
private String className;
/**
* Simple class name
*/
private String simpleClassName;
/**
* is this class only for admin ?
*/
private boolean adminResource;
/**
* does this class restricted to authenticated users ?
*/
private boolean authenticationRequired;
/**
* Class path params.
*/
private List<Param> pathParameters = new ArrayList<>();
/**
* current indentation level. Used for code generation.
*/
private int indent = 1;
/**
* tabSize used for code generation.
*/
private int tabSize = 4;
/**
* List of rest methods defined in this controller.
*/
private List<RestMethod> restMethods = new ArrayList<>();
/**
* optional errorHandler parameter definition
*/
private static final Param optionalErrorHandler;
static {
optionalErrorHandler = new Param();
optionalErrorHandler.setName("errorHandler");
optionalErrorHandler.setInAnnotationName("errorHandler");
optionalErrorHandler.setJavadoc("optional custom error handler");
optionalErrorHandler.setOptional(true);
optionalErrorHandler.setType(ErrorHandler.class);
}
/**
* Get the value of authenticationRequired
*
* @return the value of authenticationRequired
*/
public boolean isAuthenticationRequired() {
return authenticationRequired;
}
/**
* Set the value of authenticationRequired
*
* @param authenticationRequired new value of authenticationRequired
*/
public void setAuthenticationRequired(boolean authenticationRequired) {
this.authenticationRequired = authenticationRequired;
}
/**
* Get the value of adminResource
*
* @return the value of adminResource
*/
public boolean isAdminResource() {
return adminResource;
}
/**
* Set the value of adminResource
*
* @param adminResource new value of adminResource
*/
public void setAdminResource(boolean adminResource) {
this.adminResource = adminResource;
}
/**
* Get the value of pathParameters
*
* @return the value of pathParameters
*/
public List<Param> getPathParameters() {
return pathParameters;
}
/**
* Set the value of pathParameters
*
* @param pathParameters new value of pathParameters
*/
public void setPathParameters(List<Param> pathParameters) {
this.pathParameters = pathParameters;
}
/**
* Add a new line and indent next line.
*
* @param sb sink
*/
private void newLine(StringBuilder sb) {
sb.append(System.lineSeparator());
for (int i = 0; i < indent * tabSize; i++) {
sb.append(" ");
}
}
/**
* Add a new line and indent next line.
*
* @param sb sink
*/
private void newLineNoIndent(StringBuilder sb) {
sb.append(System.lineSeparator());
}
/**
* add two new line
*
* @param sb sink
*/
private void twoNewLine(StringBuilder sb) {
sb.append(System.lineSeparator());
newLine(sb);
}
/**
* Guess a Class based on its simple name.
*
* @param simpleName simple name of the class
* @param reflections reflections stores
*
* @return class the class if found, null otherwise
*/
private Class<? extends Serializable> getClassFromSimpleName(
String simpleName, Reflections reflections
) {
Optional<Class<? extends Serializable>> any = reflections.getSubTypesOf(Serializable.class)
.stream()
.filter(type -> {
return type.getSimpleName().equals(simpleName);
}).findAny();
if (any.isPresent()) {
return any.get();
} else {
return null;
}
}
/**
* Process java doc and make it exportable
*
* @param javadoc javadoc text
* @param reflections reflections store
* @param parameters list of parameters to keep
*
* @return processed javadoc
*/
private String processJavaDoc(
String javadoc,
boolean keepJava,
Reflections reflections,
List<String> parameters
) {
Pattern atLink = Pattern.compile("\\{@link ([a-zA-Z]+)\\}");
Matcher atLinkMatcher = atLink.matcher(javadoc);
String atLinkProcessed = atLinkMatcher.replaceAll(match -> {
var klass = this.getClassFromSimpleName(match.group(1), reflections);
if (keepJava && klass != null) {
return "{@link " + klass.getName() + " " + match.group(1) + "}";
} else {
return match.group(1);
}
});
Pattern paramPattern = Pattern.compile("@param ([a-zA-Z]+)(.*)");
Matcher paramMatcher = paramPattern.matcher(atLinkProcessed);
String pCleaned = paramMatcher.replaceAll(match -> {
String pName = match.group(1);
if (parameters.contains(pName)) {
return "@param " + match.group(1) + " " + match.group(2);
} else {
return "";
}
});
Pattern throwsPattern = Pattern.compile("@throws ([a-zA-Z]+)");
Matcher throwsMatcher = throwsPattern.matcher(pCleaned);
return throwsMatcher.replaceAll(match -> {
Class<? extends Serializable> klass = this.getClassFromSimpleName(match.group(1),
reflections);
if (klass != null && HttpException.class.isAssignableFrom(klass)) {
return "@throws " + klass.getName();
} else {
return "";
}
});
}
/**
* Write a multi-line block, prefixing each line by the prefix
*
* @param sb sink
* @param prefix to be added
*/
private void appendBlock(StringBuilder sb, String prefix, String block) {
StringBuilder sbPrefix = new StringBuilder();
newLine(sbPrefix);
if (prefix != null) {
sbPrefix.append(prefix);
}
String formattedComment = block.replaceAll("\n", sbPrefix.toString());
sb.append(sbPrefix).append(formattedComment);
}
/**
* Process javadoc block and append it to string builder.
*
* @param sb the string builder
* @param prefix prefix each block line with this prefix
* @param block block to append
* @param keepJava should keep java specific tags (eg @link)
* @param reflections reflections store
* @param parameters parameters to keep
*/
private void appendJavadocBlock(
StringBuilder sb,
String prefix,
String block,
boolean keepJava,
Reflections reflections,
List<String> parameters
) {
this.appendBlock(sb, prefix, processJavaDoc(block, keepJava, reflections, parameters));
}
/**
* Try to imports given class. If the import is possible (ie no simple name collision), register
* the import in {@code imports} and return the simpleName. If importing the class is not
* possible, return the class fullname.
* <p>
* This method works for simple types, not for parametrized
*
* @param name class full name
* @param imports map of simplename to fullname to be imported
*
* @return the name to use
*/
private String resolveSimpleImport(String nameArg, Map<String, String> imports) {
String name = nameArg.strip();
String[] split = name.split("\\.");
String simpleName = split[split.length - 1];
if (name.startsWith("java.lang.")) {
imports.put(simpleName, null);
return simpleName;
}
if (imports.containsKey(simpleName)) {
if (name.equals(imports.get(simpleName))) {
return simpleName;
} else {
// Same simple name, different packages -> use full name
return name;
}
} else {
// first simpleName usage, register it
imports.put(simpleName, name);
return simpleName;
}
}
/**
* Try to imports given class. If the import is possible (ie no simple name collision), register
* the import in {@code imports} and return the simpleName. If importing the class is not
* possible, return the class fullname.
* <p>
* This method works for simple types and for parametrized ones.
*
* @param name class full name
* @param imports map of simplename to fullname to be imported
*
* @return the name to use
*/
private String resolveImport(String name, Map<String, String> imports) {
// case 1: standard type eg java.lang.Long
// case 2: generic type eg. java.lang.List<java.lang.Long>
if (name.contains("<")) {
int templateStart = name.indexOf('<');
int templateEnd = name.lastIndexOf('>');
String type = name.substring(0, templateStart);
String leftPart = resolveSimpleImport(type, imports);
String template = name.substring(templateStart + 1, templateEnd);
if (template.contains(",") && template.contains("<")) {
// very complex template
// eg <java.lang.Long, java.lang.List<java.lang.String>>
// @TODO not yet implemented
Logger.warn("Very Complex generic type " + template);
return leftPart + "<" + template + ">";
} else if (template.contains(",")) {
// multiple simple parameters
return leftPart + "<"
+ Arrays.stream(template.split(","))
.map(item -> resolveImport(item, imports))
.collect(Collectors.joining(","))
+ ">";
} else {
// simple template or generic template -> simple recursive call
return leftPart + "<" + resolveImport(template, imports) + ">";
}
} else {
return resolveSimpleImport(name, imports);
}
}
/**
* Camelcasify simpleClassname (eg ArrayList, UserRestEndpoint, ...)
*
* @param simpleClassName className
*
* @return camel-case version of className eg(arrayList, userRestEndpoint, ...)
*/
private String camelcasify(String simpleClassName) {
String firstChar = simpleClassName.substring(0, 1);
return firstChar.toLowerCase() + simpleClassName.substring(1);
}
private List<String> processParameters(List<Param> params, Map<String, String> imports) {
return params.stream()
.map(param -> resolveImport(param.getType().getTypeName(), imports)
+ " " + param.getName())
.collect(Collectors.toList());
}
/**
* Write java class as string.The generated class is an inner class which has to be included in
* a main class.
*
* @param imports map of imports
* @param clientName name of client class
* @param javadoc javadoc as extracted by the JavaDocEtractor
* @param reflections reflections store
*
* @return generated java inner static class
*/
public String generateJavaClient(
Map<String, String> imports,
String clientName,
Map<String, ClassDoc> javadoc,
Reflections reflections
) {
tabSize = 4;
StringBuilder sb = new StringBuilder();
Logger.debug("Generate client class " + this);
twoNewLine(sb);
sb.append("/**");
ClassDoc classDoc = javadoc.get(this.className);
if (classDoc != null) {
appendJavadocBlock(sb, " *", classDoc.getDoc(), true, reflections, List.of());
newLine(sb);
sb.append(" * <p>");
newLine(sb);
}
sb.append(" * {@link ").append(this.className).append(" } client");
newLine(sb);
sb.append(" */");
newLine(sb);
sb.append("public ").append(this.simpleClassName).append("ClientImpl ")
.append(camelcasify(this.simpleClassName))
.append(" = new ").append(this.simpleClassName).append("ClientImpl();");
twoNewLine(sb);
sb.append("/**");
newLine(sb);
sb.append(" * {@link ").append(this.className).append("} client implementation");
newLine(sb);
sb.append(" */");
newLine(sb);
sb.append("public class ")
.append(this.simpleClassName).append("ClientImpl").append(" {");
indent++;
for (RestMethod method : this.restMethods) {
Logger.debug(" * generate " + method);
////////////////////////////////////////////////////////////////////////////////////////
// JAVADOC
////////////////////////////////////////////////////////////////////////////////////////
// @TODO extract effective java doc from api and reuse it here
twoNewLine(sb);
sb.append("/**");
newLine(sb);
sb.append(" * ").append(method.getHttpMethod()).append(" ")
.append(method.getFullPath()).append(" calls {@link ")
// do not resolve imports in javadoc links
// one would not imports classes unless they are used in the code
.append(this.className)
.append("#").append(method.getName()).append("}");
String methodDoc = classDoc.getMethods().getOrDefault(method.getName(), "");
newLine(sb);
sb.append(" *");
List<String> paramNames = method.getAllParameters().stream()
.map(param -> param.getName())
.collect(Collectors.toList());
appendJavadocBlock(sb, " *", methodDoc, true, reflections, paramNames);
newLine(sb);
sb.append(" */");
newLine(sb);
if (method.isDeprecated()) {
sb.append("@Deprecated");
newLine(sb);
}
////////////////////////////////////////////////////////////////////////////////////////
// SIGNATURE
////////////////////////////////////////////////////////////////////////////////////////
String resolvedReturnType = resolveImport(method.getReturnType().getTypeName(),
imports);
sb.append("public ").append(resolvedReturnType)
.append(" ").append(method.getName()).append("(");
// parameters
List<String> params = new ArrayList<>();
params.addAll(processParameters(this.getPathParameters(), imports));
params.addAll(processParameters(method.getPathParameters(), imports));
params.addAll(processParameters(method.getQueryParameters(), imports));
if (method.getBodyParam() != null) {
params.addAll(processParameters(List.of(method.getBodyParam()), imports));
}
method.getFormParameters().forEach(param -> {
String type = resolveImport(param.getType().getTypeName(), imports);
params.add("FormField<" + type + "> " + param.getName());
});
sb.append(params.stream().collect(Collectors.joining(", ")));
sb.append(") ");
////////////////////////////////////////////////////////////////////////////////////////
// BODY
////////////////////////////////////////////////////////////////////////////////////////
sb.append("{");
indent++;
// generate path
///////////////////
newLine(sb);
// class path + methodPath
sb.append("UriTemplate pathTemplate = new PathPattern(\"")
.append(method.getFullPath());
sb.append("\").getTemplate();");
newLine(sb);
// Aggregate path params
List<Param> pathParams = new ArrayList<>();
pathParams.addAll(this.getPathParameters());
pathParams.addAll(method.getPathParameters());
// compute the path URL
sb.append("String path = pathTemplate.createURI(")
.append(
pathParams.stream().map(param -> param.getName() + ".toString()")
.collect(Collectors.joining(","))
).append(");");
newLine(sb);
// query string parameters
if (!method.getQueryParameters().isEmpty()) {
sb.append("List<String> qs =new ArrayList<>();");
newLine(sb);
if (!method.getQueryParameters().isEmpty()) {
imports.put("ArrayList", "java.util.ArrayList");
imports.put("URLEncoder", "java.net.URLEncoder");
imports.put("StandardCharsets;", "java.nio.charset.StandardCharsets");
}
method.getQueryParameters().stream()
.map(queryParam -> "if (" + queryParam.getName() + " != null){ "
+ " qs.add(\"" + queryParam.getInAnnotationName()
+ "=\"+URLEncoder.encode("
+ queryParam.getName() + ".toString(), StandardCharsets.UTF_8)); }"
)
.forEach(item -> {
sb.append(item);
newLine(sb);
});
sb.append("if (!qs.isEmpty()) { path += \"?\" + String.join(\"&\", qs);}");
newLine(sb);
}
boolean forDataRequest = method.getConsumes().contains(MediaType.MULTIPART_FORM_DATA);
if (forDataRequest) {
imports.put("Map", "java.util.Map");
imports.put("HashMap", "java.util.HashMap");
imports.put("FormField", "ch.colabproject.colab.generator.plugin.rest.FormField");
sb.append("Map<String, FormField> formData = new HashMap<>();");
newLine(sb);
method.getFormParameters().forEach(param -> {
sb.append("formData.put(\"")
.append(param.getInAnnotationName())
.append("\", ")
.append(param.getName())
.append(");");
newLine(sb);
});
}
// make http request
//////////////////////
if (!method.getReturnType().equals(void.class)) {
sb.append("return ");
}
sb.append(clientName).append(".this.")
.append(method.getHttpMethod().toLowerCase())
.append("(path, ");
if (forDataRequest) {
sb.append(" formData").append(", ");
} else if (method.getBodyParam() != null) {
sb.append(method.getBodyParam().getName()).append(", ");
}
if (method.isReturnTypeGeneric()) {
sb.append("new GenericType<").append(resolvedReturnType).append(">(){}");
} else {
sb.append("new GenericType<>(").append(resolvedReturnType).append(".class)");
}
List<String> produces = method.getProduces();
if (produces != null && !produces.isEmpty()) {
String accept = produces.stream()
.map(type -> "\"" + type + "\"")
.collect(Collectors.joining(", "));
sb.append(", ").append(accept);
}
sb.append(");");
indent--;
newLine(sb);
sb.append("}");
}
indent--;
newLine(sb);
sb.append("}");
return sb.toString();
}
/**
* @param sb
* @param functionName
* @param params
* @param returnType
*/
private void generateTypescriptFunction(
StringBuilder sb,
RestMethod method,
String functionName,
List<Param> params,
ClassDoc classDoc,
Map<String, Type> types,
Reflections reflections,
Runnable bodyGenerator
) {
Logger.debug(" * generate " + functionName);
// JSDOC
////////////////////////
newLine(sb);
sb.append("/**");
newLine(sb);
sb.append(" * ").append(method.getHttpMethod()).append(" ")
.append(method.getFullPath());
newLine(sb);
sb.append(" * <p>");
String methodDoc = classDoc != null
? classDoc.getMethods().getOrDefault(method.getName(), "")
: "";
List<String> paramNames = params.stream()
.map(param -> param.getName())
.collect(Collectors.toList());
appendJavadocBlock(sb, " *", methodDoc, false, reflections, paramNames);
newLine(sb);
sb.append(" */");
newLine(sb);
// Signature
/////////////////////////////
sb.append(functionName)
.append(": function(")
.append(params.stream()
.map(param -> param.getName()
+ (param.isOptional() ? "?" : "")
+ ": "
+ TypeScriptHelper.convertType(param.getType(), types))
.collect(Collectors.joining(", ")))
.append(") {");
indent++;
newLine(sb);
bodyGenerator.run();
indent--;
newLine(sb);
sb.append("},");
}
/**
* Write ts client for this controller.This method will populate types map with type which
* requires a dedicated TS interface
*
* @param types map of types which requires
* @param javadoc javadoc as extracted by the JavaDocEtractor
* @param reflections reflections store
*
* @return Typscript REST client
*/
public String generateTypescriptClient(Map<String, Type> types,
Map<String, ClassDoc> javadoc,
Reflections reflections
) {
tabSize = 2;
Logger.debug("Generate typescript class " + this);
indent++;
StringBuilder sb = new StringBuilder();
newLine(sb);
sb.append("/**");
ClassDoc classDoc = javadoc.get(this.className);
if (classDoc != null) {
appendJavadocBlock(sb, " *", classDoc.getDoc(), false, reflections, List.of());
}
newLine(sb);
sb.append(" */");
newLine(sb);
sb.append(this.simpleClassName).append(" : {");
indent++;
newLineNoIndent(sb);
restMethods.forEach(method -> {
// 1 Generate getPath function
List<Param> urlParams = new ArrayList<>(this.getPathParameters());
urlParams.addAll(method.getUrlParameters());
String getPathFunctionName = method.getName() + "Path";
Runnable buildPath = () -> {
sb.append("const queryString : string[] = [];");
newLineNoIndent(sb);
method.getQueryParameters().forEach(queryParam -> {
sb.append("if (").append(queryParam.getName()).append(" != null){");
indent++;
newLine(sb);
sb.append("queryString.push('")
.append(queryParam.getInAnnotationName())
.append("=' + encodeURIComponent(").append(queryParam.getName())
.append("+')'));");
indent--;
newLine(sb);
sb.append("}");
});
newLine(sb);
Map<String, String> pathParams = method.getPathParameters().stream()
.collect(Collectors.toMap(
p -> p.getInAnnotationName(),
p -> "${" + p.getName() + "}")
);
UriTemplate pathTemplate = new PathPattern(method.getFullPath()).getTemplate();
String tsPath = pathTemplate.createURI(pathParams);
sb.append("const path = `${baseUrl}").append(tsPath)
.append("${queryString.length > 0 ? '?' + queryString.join('&') : ''}`;");
};
this.generateTypescriptFunction(sb, method, getPathFunctionName, urlParams,
classDoc, types, reflections,
() -> {
buildPath.run();
newLine(sb);
sb.append("return path;");
}
);
List<Param> allParams = new ArrayList<>(this.getPathParameters());
allParams.addAll(method.getAllParameters());
allParams.add(optionalErrorHandler);
// 2 generate API call function
this.generateTypescriptFunction(sb, method, method.getName(), allParams,
classDoc, types, reflections,
() -> {
buildPath.run();
newLine(sb);
// JSDOC
////////////////////////
if (!method.getFormParameters().isEmpty()) {
sb.append("const formData = new FormData();");
method.getFormParameters().forEach(formParam -> {
// indent++;
newLine(sb);
sb.append("if(")
.append(formParam.getName())
.append(" as unknown instanceof Blob) {");
indent++;
newLine(sb);
sb.append("formData.append('")
.append(formParam.getInAnnotationName())
.append("', ")
.append(formParam.getName())
.append(" as unknown as Blob);");
// indent--;
indent--;
newLine(sb);
sb.append("} else {");
indent++;
newLine(sb);
sb.append("formData.append('")
.append(formParam.getInAnnotationName())
.append("', ")
.append(formParam.getName())
.append(" ? '' + ")
.append(formParam.getName())
.append(" : '')");
indent--;
newLine(sb);
sb.append("}");
newLineNoIndent(sb);
});
newLine(sb);
}
boolean forDataRequest = method.getConsumes()
.contains(MediaType.MULTIPART_FORM_DATA);
String fn = method.getReturnType() == Response.class ? "sendHttpRequest"
: "sendJsonRequest";
String fnTemplate = method.getReturnType() == Response.class ? ""
: "<" +
TypeScriptHelper.convertType(method.getReturnType(), types)
+ ">";
if (forDataRequest) {
sb.append("return ").append(fn).append(fnTemplate)
.append("('").append(method.getHttpMethod())
.append("', path")
.append(", formData")
.append(", errorHandler || defaultErrorHandler")
.append(", '").append(MediaType.MULTIPART_FORM_DATA).append("'")
.append(");");
} else {
sb.append("return ").append(fn).append(fnTemplate)
.append("('").append(method.getHttpMethod())
.append("', path")
.append(", ")
.append(method.getBodyParam() != null
? method.getBodyParam().getName()
: "undefined")
.append(", errorHandler || defaultErrorHandler")
.append(", '").append(MediaType.APPLICATION_JSON).append("'")
.append(");");
}
});
});
indent--;
newLine(sb);
sb.append("},");
return sb.toString();
}
/**
* Register rest method
*
* @param restMethod method to register
*/
private void registerMethod(RestMethod restMethod) {
this.restMethods.add(restMethod);
}
/**
* Register class-level path param
*
* @param name name of the parameter
* @param javadoc some documentation
* @param type type of the parameter
*/
private void addPathParameter(String name, String pathParamName, String javadoc, Type type) {
Param param = new Param();
param.setName(name);
param.setInAnnotationName(pathParamName);
param.setJavadoc(javadoc);
param.setType(type);
this.pathParameters.add(param);
}
/**
* Get all rest methods
*
* @return list of rest methods
*/
public List<RestMethod> getRestMethods() {
return restMethods;
}
@Override
public String toString() {
return this.className;
}
/**
* Get simple class name
*
* @return simple class name
*/
public String getSimpleClassName() {
return simpleClassName;
}
/**
* Analyze path and extract path parameters.
*
* @param path path to analyze
*
* @return path parameters named mapped to null type. Effective type must be resolved while
* parsing various methods
*/
private static Map<String, Class<?>> splitPath(Path path) {
HashMap<String, Class<?>> parameters = new HashMap<>();
if (path != null) {
PathPattern pathPattern = new PathPattern(path.value());
UriTemplate template = pathPattern.getTemplate();
for (String param : template.getTemplateVariables()) {
// no way to detect param type at the moment
parameters.put(param, null);
}
}
return parameters;
}
/**
* Build a RestEndpoint based on a klass
*
* @param klass the class must be annotated with {@link Path}
* @param applicationPath main application path
*
* @return RestEndpoint instance, ready for code generation
*/
public static RestEndpoint build(Class<?> klass, String applicationPath) {
RestEndpoint restEndpoint = new RestEndpoint();
Consumes defaultConsumes = klass.getAnnotation(Consumes.class);
Produces defautProduces = klass.getAnnotation(Produces.class);
restEndpoint.setAdminResource(klass.getAnnotation(AdminResource.class) != null);
restEndpoint.setAuthenticationRequired(
klass.getAnnotation(AuthenticationRequired.class) != null);
restEndpoint.simpleClassName = klass.getSimpleName();
restEndpoint.className = klass.getName();
Path classPath = klass.getAnnotation(Path.class);
// eg @Path("project/{pId: [regex]}/card/{}") or "project"
Map<String, Class<?>> mainPathParam = splitPath(classPath);
Logger.debug("Build RestEndpoint for " + klass);
// Go through each class methods but only cares about ones annotated with
// a HttpMethod-like annotation
for (Method method : klass.getMethods()) {
for (Annotation annotation : method.getAnnotations()) {
// @GET @POST, etc ?
HttpMethod httpMethodAnno = annotation.annotationType()
.getAnnotation(HttpMethod.class);
if (httpMethodAnno != null) {
RestMethod restMethod = new RestMethod();
restMethod.setName(method.getName());
restEndpoint.registerMethod(restMethod);
String httpMethod = httpMethodAnno.value();
restMethod.setHttpMethod(httpMethod);
Path methodPath = method.getAnnotation(Path.class);
Consumes methodConsumes = method.getAnnotation(Consumes.class);
Consumes consumes = methodConsumes != null ? methodConsumes : defaultConsumes;
if (consumes != null) {
restMethod.setConsumes(List.of(consumes.value()));
}
Produces methodProduces = method.getAnnotation(Produces.class);
Produces produces = methodProduces != null ? methodProduces : defautProduces;
if (produces != null) {
restMethod.setProduces(List.of(produces.value()));
}
restMethod
.setAdminResource(method.getAnnotation(AdminResource.class) != null);
restMethod
.setDeprecated(method.getAnnotation(Deprecated.class) != null);
restMethod.setAuthenticationRequired(
method.getAnnotation(AuthenticationRequired.class) != null);
// full path
StringBuilder pathBuilder = new StringBuilder(applicationPath).append('/')
.append(classPath.value());
if (methodPath != null && !methodPath.value().isEmpty() && !"/"
.equals(methodPath.value())) {
if (methodPath.value().charAt(0) != '/') {
pathBuilder.append('/');
}
pathBuilder.append(methodPath.value());
}
String fullPath = pathBuilder.toString();
restMethod.setFullPath(fullPath);
Map<String, Class<?>> methodPathParam = splitPath(methodPath);
// Process parameters
for (Parameter p : method.getParameters()) {
PathParam pathParam = p.getAnnotation(PathParam.class);
QueryParam queryParam = p.getAnnotation(QueryParam.class);
FormDataParam formDataParam = p.getAnnotation(FormDataParam.class);
if (pathParam != null) {
// Path param may be a method specific param or a class one
// at this point, resolving effective pathParam type is possible
if (methodPathParam.containsKey(pathParam.value())) {
methodPathParam.put(pathParam.value(), p.getType());
restMethod.addPathParameter(
p.getName(),
pathParam.value(),
"path param",
p.getType());
} else if (mainPathParam.containsKey(pathParam.value())) {
mainPathParam.put(pathParam.value(), p.getType());
restEndpoint.addPathParameter(
p.getName(),
pathParam.value(),
"path param",
p.getType());
} else {
Logger.error("@PathParam "
+ pathParam.value() + " not found in @Path");
// error !
}
} else if (queryParam != null) {
restMethod.addQueryParameter(
p.getName(),
queryParam.value(),
"query param",
p.getType());
} else if (formDataParam != null) {
restMethod.addFormParameter(
p.getName(),
formDataParam.value(),
"query param",
p.getType());
} else if (p.getAnnotations().length == 0) {
// request body
if (restMethod.getBodyParam() != null) {
// several body param ????
Logger.warn("Several body parameters ???");
}
Param bodyParam = new Param();
bodyParam.setName(p.getName());
bodyParam.setType(p.getParameterizedType());
bodyParam.setJavadoc("body payload");
restMethod.setBodyParam(bodyParam);
}
}
Class<?> returnType = method.getReturnType();
Type genericType = method.getGenericReturnType();
String typeName = returnType.getTypeName();
String genericTypeName = genericType.getTypeName();
boolean isReturnTypeGeneric = !typeName.equals(genericTypeName);
if (isReturnTypeGeneric) {
restMethod.setReturnTypeGeneric(isReturnTypeGeneric);
}
restMethod.setReturnType(isReturnTypeGeneric ? genericType : returnType);
break;
}
}
}
return restEndpoint;
}
}