org.openl.rules.openapi.impl.OpenAPIJavaClassGenerator Maven / Gradle / Ivy
The newest version!
package org.openl.rules.openapi.impl;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import javax.ws.rs.Consumes;
import javax.ws.rs.CookieParam;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.HEAD;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.OPTIONS;
import javax.ws.rs.PATCH;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import org.openl.gen.AnnotationDescriptionBuilder;
import org.openl.gen.InterfaceByteCodeBuilder;
import org.openl.gen.InterfaceImplBuilder;
import org.openl.gen.MethodDescriptionBuilder;
import org.openl.gen.MethodParameterBuilder;
import org.openl.gen.TypeDescription;
import org.openl.rules.model.scaffolding.InputParameter;
import org.openl.rules.model.scaffolding.MethodModel;
import org.openl.rules.model.scaffolding.PathInfo;
import org.openl.rules.model.scaffolding.ProjectModel;
import org.openl.rules.model.scaffolding.TypeInfo;
import org.openl.rules.ruleservice.core.annotations.Name;
import org.openl.rules.ruleservice.core.annotations.NoTypeConversion;
import org.openl.rules.ruleservice.core.annotations.ServiceExtraMethod;
import org.openl.rules.ruleservice.core.annotations.ServiceExtraMethodHandler;
import org.openl.rules.ruleservice.core.interceptors.RulesType;
import org.openl.rules.ruleservice.publish.jaxrs.JAXRSOpenLServiceEnhancerHelper;
import org.openl.util.StringUtils;
public class OpenAPIJavaClassGenerator {
private static final String DEFAULT_JSON_TYPE = "application/json";
private static final String DEFAULT_SIMPLE_TYPE = "text/plain";
private static final Class> DEFAULT_DATATYPE_CLASS = Object.class;
public static final String VALUE = "value";
public static final String DEFAULT_OPEN_API_PATH = "org.openl.generated.services";
public static final String DEFAULT_RUNTIME_CTX_PARAM_NAME = "runtimeContext";
private final ProjectModel projectModel;
public OpenAPIJavaClassGenerator(ProjectModel projectModel) {
this.projectModel = projectModel;
}
/**
* Make decision whatever if we need to decorate this method or not
*
* @param method candidate
* @return {@code true} if require decoration
*/
private boolean generateDecision(MethodModel method) {
if (!method.isInclude()) {
return false;
}
for (InputParameter inputParameter : method.getParameters()) {
if (inputParameter.getType().getType() == TypeInfo.Type.SPREADSHEET || inputParameter.getType()
.getType() == TypeInfo.Type.SPREADSHEET_ARRAY) {
return true;
}
}
final PathInfo pathInfo = method.getPathInfo();
StringBuilder sb = new StringBuilder("/" + pathInfo.getFormattedPath());
final List parameters = method.getParameters();
parameters.stream()
.filter(p -> p.getIn() == InputParameter.In.PATH)
.map(InputParameter::getFormattedName)
.forEach(name -> sb.append("/{").append(name).append('}'));
if (!pathInfo.getOriginalPath().contentEquals(sb)) {
// if method name doesn't match expected path
return true;
}
if (StringUtils.isNotBlank(pathInfo.getProduces())) {
final TypeInfo typeInfo = pathInfo.getReturnType();
if (typeInfo.isReference() || typeInfo.getDimension() > 0) {
if (!DEFAULT_JSON_TYPE.equals(pathInfo.getProduces())) {
// if return type is not simple, application/json by default
return true;
}
} else if (!DEFAULT_SIMPLE_TYPE.equals(pathInfo.getProduces())) {
// if return type is simple, text/plain by default
return true;
}
}
final boolean requestBodyIsPresented = parameters.stream().map(InputParameter::getIn).anyMatch(Objects::isNull);
final boolean otherParamsArePresented = parameters.stream()
.map(InputParameter::getIn)
.anyMatch(Objects::nonNull);
if (requestBodyIsPresented && otherParamsArePresented) {
return true;
}
if (parameters.stream()
.map(InputParameter::getIn)
.filter(Objects::nonNull)
.anyMatch(in -> !InputParameter.In.PATH.equals(in))) {
// Only @PathParam annotation is generated by default by Rule Services
return true;
}
if (parameters.stream().anyMatch(p -> !p.getFormattedName().equalsIgnoreCase(p.getOriginalName()))) {
return true;
}
if (StringUtils.isNotBlank(pathInfo.getConsumes())) {
if (projectModel.isRuntimeContextProvided()) {
if (!DEFAULT_JSON_TYPE.equals(pathInfo.getConsumes())) {
// if context, application/json by default
return true;
}
// runtime context param may be null when it's inside the request model
if (!parameters.isEmpty() && pathInfo
.getRuntimeContextParameter() != null && !DEFAULT_RUNTIME_CTX_PARAM_NAME
.equals(pathInfo.getRuntimeContextParameter().getFormattedName())) {
// if runtimeContext param name is not default
return true;
}
} else if (parameters.isEmpty()) {
if (!DEFAULT_SIMPLE_TYPE.equals(pathInfo.getConsumes())) {
// if no prams, text/plan by default
return true;
}
} else {
if (parameters.size() == 1) {
if (parameters.get(0).getType().isReference() || parameters.get(0).getType().getDimension() > 0) {
if (!DEFAULT_JSON_TYPE.equals(pathInfo.getConsumes())) {
// if one not simple param, application/json by default
return true;
}
} else if (!DEFAULT_SIMPLE_TYPE.equals(pathInfo.getConsumes())) {
// if one simple param, text/plain by default
return true;
}
} else if (!DEFAULT_JSON_TYPE.equals(pathInfo.getConsumes())) {
// if more than one param, application/json by default
return true;
}
}
}
switch (pathInfo.getOperation()) {
case GET:
if (projectModel.isRuntimeContextProvided()) {
// if RuntimeContext is provided, POST by default.
return true;
}
if (parameters.size() > JAXRSOpenLServiceEnhancerHelper.MAX_PARAMETERS_COUNT_FOR_GET) {
// if more than 3 parameters, POST by default.
return true;
} else if (!parameters.stream().allMatch(p -> p.getType().getType() == TypeInfo.Type.PRIMITIVE)) {
// if there is at least one non-primitive parameter, POST by default.
return true;
}
break;
case POST:
if (!projectModel.isRuntimeContextProvided()) {
if (parameters.isEmpty()) {
// if no context and empty params, GET by default.
return true;
} else if (parameters
.size() <= JAXRSOpenLServiceEnhancerHelper.MAX_PARAMETERS_COUNT_FOR_GET && parameters.stream()
.allMatch(p -> p.getType().getType() == TypeInfo.Type.PRIMITIVE)) {
// if no context and if there are less than 3 parameters, and they are all primitive, GET by
// default.
return true;
}
}
break;
default:
// if not POST and not GET
return true;
}
return false;
}
public OpenAPIGeneratedClasses generate() {
String interfaceName = DEFAULT_OPEN_API_PATH + ".Service";
InterfaceByteCodeBuilder interfaceBuilder = InterfaceByteCodeBuilder.create(interfaceName);
Stream.concat(projectModel.getSpreadsheetResultModels().stream(), projectModel.getDataModels().stream())
.filter(this::generateDecision)
.map(method -> visitInterfaceMethod(method, false).build())
.forEach(interfaceBuilder::addAbstractMethod);
OpenAPIGeneratedClasses.Builder builder = OpenAPIGeneratedClasses.Builder.initialize();
for (MethodModel extraMethod : projectModel.getNotOpenLModels()) {
InterfaceImplBuilder extraMethodBuilder = new InterfaceImplBuilder(ServiceExtraMethodHandler.class,
DEFAULT_OPEN_API_PATH);
GroovyScriptFile groovyScriptFile = new GroovyScriptFile(extraMethodBuilder.getScriptName(),
extraMethodBuilder.scriptText());
builder.addGroovyCommonScript(groovyScriptFile);
MethodDescriptionBuilder methodDesc = visitInterfaceMethod(extraMethod, true);
methodDesc.addAnnotation(AnnotationDescriptionBuilder.create(ServiceExtraMethod.class)
.withProperty(VALUE, new TypeDescription(groovyScriptFile.getNameWithPackage()))
.build());
interfaceBuilder.addAbstractMethod(methodDesc.build());
}
if (!interfaceBuilder.isEmpty()) {
builder.setGroovyScriptFile(new GroovyScriptFile(interfaceName,
interfaceBuilder.buildGroovy().generatedText()));
}
return builder.build();
}
private MethodDescriptionBuilder visitInterfaceMethod(MethodModel sprModel, boolean extraMethod) {
final PathInfo pathInfo = sprModel.getPathInfo();
final TypeInfo returnTypeInfo = pathInfo.getReturnType();
MethodDescriptionBuilder methodBuilder = MethodDescriptionBuilder.create(pathInfo.getFormattedPath(),
resolveType(returnTypeInfo));
InputParameter runtimeCtxParam = sprModel.getPathInfo().getRuntimeContextParameter();
if (runtimeCtxParam != null) {
MethodParameterBuilder ctxBuilder = MethodParameterBuilder.create(runtimeCtxParam.getType().getJavaName());
final String paramName = runtimeCtxParam.getFormattedName();
if (sprModel.getParameters().size() > 0 && !DEFAULT_RUNTIME_CTX_PARAM_NAME.equals(paramName)) {
ctxBuilder.addAnnotation(
AnnotationDescriptionBuilder.create(Name.class).withProperty(VALUE, paramName).build());
}
methodBuilder.addParameter(ctxBuilder.build());
}
for (InputParameter parameter : sprModel.getParameters()) {
methodBuilder.addParameter(visitMethodParameter(parameter, extraMethod));
}
if (returnTypeInfo.getType() == TypeInfo.Type.DATATYPE) {
methodBuilder.addAnnotation(AnnotationDescriptionBuilder.create(RulesType.class)
.withProperty(VALUE, OpenAPITypeUtils.removeArrayBrackets(returnTypeInfo.getSimpleName()))
.build());
}
writeWebServiceAnnotations(methodBuilder, pathInfo);
return methodBuilder;
}
private TypeDescription visitMethodParameter(InputParameter parameter, boolean extraMethod) {
final TypeInfo paramType = parameter.getType();
MethodParameterBuilder methodParamBuilder = MethodParameterBuilder.create(resolveType(paramType));
if (paramType.getType() == TypeInfo.Type.DATATYPE) {
methodParamBuilder.addAnnotation(AnnotationDescriptionBuilder.create(RulesType.class)
.withProperty(VALUE, OpenAPITypeUtils.removeArrayBrackets(paramType.getSimpleName()))
.build());
} else if (paramType.getType() == TypeInfo.Type.SPREADSHEET || paramType
.getType() == TypeInfo.Type.SPREADSHEET_ARRAY) {
methodParamBuilder.addAnnotation(AnnotationDescriptionBuilder.create(NoTypeConversion.class).build());
}
final String originalParameterName = parameter.getOriginalName();
final String formattedParameterName = parameter.getFormattedName();
final String parameterName = originalParameterName
.equalsIgnoreCase(formattedParameterName) ? formattedParameterName : originalParameterName;
if (extraMethod) {
methodParamBuilder.addAnnotation(
AnnotationDescriptionBuilder.create(Name.class).withProperty(VALUE, parameterName).build());
}
if (parameter.getIn() != null) {
methodParamBuilder
.addAnnotation(AnnotationDescriptionBuilder.create(chooseParamAnnotation(parameter.getIn()))
.withProperty(VALUE, parameterName)
.build());
}
return methodParamBuilder.build();
}
private void writeWebServiceAnnotations(MethodDescriptionBuilder methodBuilder, PathInfo pathInfo) {
methodBuilder.addAnnotation(
AnnotationDescriptionBuilder.create(chooseOperationAnnotation(pathInfo.getOperation())).build());
methodBuilder.addAnnotation(
AnnotationDescriptionBuilder.create(Path.class).withProperty(VALUE, pathInfo.getOriginalPath()).build());
if (StringUtils.isNotBlank(pathInfo.getConsumes())) {
methodBuilder.addAnnotation(AnnotationDescriptionBuilder.create(Consumes.class)
.withProperty(VALUE, pathInfo.getConsumes(), true)
.build());
}
if (StringUtils.isNotBlank(pathInfo.getProduces())) {
methodBuilder.addAnnotation(AnnotationDescriptionBuilder.create(Produces.class)
.withProperty(VALUE, pathInfo.getProduces(), true)
.build());
}
}
static String resolveType(TypeInfo typeInfo) {
if (typeInfo.getType() == TypeInfo.Type.DATATYPE) {
Class> type = DEFAULT_DATATYPE_CLASS;
if (typeInfo.getDimension() > 0) {
int[] dimensions = new int[typeInfo.getDimension()];
type = Array.newInstance(type, dimensions).getClass();
}
return type.getName();
} else {
return typeInfo.getJavaName();
}
}
private Class extends Annotation> chooseOperationAnnotation(PathInfo.Operation operation) {
switch (operation) {
case GET:
return GET.class;
case POST:
return POST.class;
case PUT:
return PUT.class;
case DELETE:
return DELETE.class;
case PATCH:
return PATCH.class;
case HEAD:
return HEAD.class;
case OPTIONS:
return OPTIONS.class;
default:
throw new IllegalStateException("Unable to find operation annotation.");
}
}
private Class extends Annotation> chooseParamAnnotation(InputParameter.In in) {
switch (in) {
case PATH:
return PathParam.class;
case QUERY:
return QueryParam.class;
case COOKIE:
return CookieParam.class;
case HEADER:
return HeaderParam.class;
default:
throw new IllegalStateException("Unable to find param annotation.");
}
}
}