mirror of
https://gitee.com/orangeform/orange-admin.git
synced 2026-01-17 10:36:31 +08:00
重命名 orange-demo-activiti 为 images
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<artifactId>framework</artifactId>
|
||||
<groupId>com.orangeforms</groupId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>apidoc-tools</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>apidoc-tools</name>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.orangeforms</groupId>
|
||||
<artifactId>common-core</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.thoughtworks.qdox</groupId>
|
||||
<artifactId>qdox</artifactId>
|
||||
<version>${qdox.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.orangeforms.apidoc.tools;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.orangeforms.apidoc.tools.codeparser.ApiCodeConfig;
|
||||
import com.orangeforms.apidoc.tools.codeparser.ApiCodeParser;
|
||||
import com.orangeforms.apidoc.tools.export.ApiPostmanExporter;
|
||||
import freemarker.template.TemplateException;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ExportApiApp {
|
||||
|
||||
public static void main(String[] args) throws IOException, TemplateException {
|
||||
// 在第一次导出时,需要打开export-api-config.json配置文件,
|
||||
// 修改其中的工程根目录配置项(projectRootPath),其他配置保持不变即可。
|
||||
InputStream in = ExportApiApp.class.getResourceAsStream("/export-api-config.json");
|
||||
String jsonData = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
|
||||
ApiCodeConfig apiCodeConfig = JSON.parseObject(jsonData, ApiCodeConfig.class);
|
||||
ApiCodeParser apiCodeParser = new ApiCodeParser(apiCodeConfig);
|
||||
ApiCodeParser.ApiProject project = apiCodeParser.doParse();
|
||||
ApiPostmanExporter exporter = new ApiPostmanExporter();
|
||||
// 将下面的目录改为实际输出目录。
|
||||
exporter.doGenerate(project, "/xxx/Desktop/1.json");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.orangeforms.apidoc.tools;
|
||||
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.orangeforms.apidoc.tools.codeparser.ApiCodeConfig;
|
||||
import com.orangeforms.apidoc.tools.codeparser.ApiCodeParser;
|
||||
import com.orangeforms.apidoc.tools.export.ApiDocExporter;
|
||||
import freemarker.template.TemplateException;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
public class ExportDocApp {
|
||||
|
||||
public static void main(String[] args) throws IOException, TemplateException {
|
||||
// 在第一次导出时,需要打开export-api-config.json配置文件,
|
||||
// 修改其中的工程根目录配置项(projectRootPath),其他配置保持不变即可。
|
||||
InputStream in = ExportDocApp.class.getResourceAsStream("/export-api-config.json");
|
||||
String jsonData = StreamUtils.copyToString(in, StandardCharsets.UTF_8);
|
||||
ApiCodeConfig apiCodeConfig = JSON.parseObject(jsonData, ApiCodeConfig.class);
|
||||
ApiCodeParser apiCodeParser = new ApiCodeParser(apiCodeConfig);
|
||||
ApiCodeParser.ApiProject project = apiCodeParser.doParse();
|
||||
ApiDocExporter exporter = new ApiDocExporter();
|
||||
// 将下面的目录改为实际输出目录。
|
||||
exporter.doGenerate(project, "/xxx/Desktop/2.md");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.orangeforms.apidoc.tools.codeparser;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 解析项目中接口信息的配置对象。
|
||||
*
|
||||
* @author Jerry
|
||||
* @date 2021-06-06
|
||||
*/
|
||||
@Data
|
||||
public class ApiCodeConfig {
|
||||
|
||||
/**
|
||||
* 项目名称。
|
||||
*/
|
||||
private String projectName;
|
||||
/**
|
||||
* 项目的基础包名,如(com.demo.multi)。
|
||||
*/
|
||||
private String basePackage;
|
||||
/**
|
||||
* 项目在本地文件系统中的根目录。这里需要注意的是,Windows用户请务必使用反斜杠作为目录分隔符。
|
||||
* 如:"e:/mypath/OrangeSingleDemo","/Users/xxx/OrangeSingleDemo"。
|
||||
*/
|
||||
private String projectRootPath;
|
||||
/**
|
||||
* 是否为微服务项目。
|
||||
*/
|
||||
private Boolean microService;
|
||||
/**
|
||||
* 服务配置列表。对于单体服务,至少也会有一个ServiceConfig对象。
|
||||
*/
|
||||
private List<ServiceConfig> serviceList;
|
||||
|
||||
@Data
|
||||
public static class ServiceConfig {
|
||||
/**
|
||||
* 服务名称。
|
||||
*/
|
||||
private String serviceName;
|
||||
/**
|
||||
* 服务中文显示名称。
|
||||
*/
|
||||
private String showName;
|
||||
/**
|
||||
* 服务所在目录,相对于工程目录的子目录。
|
||||
*/
|
||||
private String servicePath;
|
||||
/**
|
||||
* 仅用于微服务工程。通常为服务路由路径,如:/admin/coursepaper。服务内的接口,都会加上该路径前缀。
|
||||
*/
|
||||
private String serviceRequestPath;
|
||||
/**
|
||||
* 服务的端口号。
|
||||
*/
|
||||
private String port;
|
||||
/**
|
||||
* Api Controller信息列表。
|
||||
*/
|
||||
private List<ControllerInfo> controllerInfoList;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ControllerInfo {
|
||||
/**
|
||||
* Controller.java等接口文件的所在目录。该目录仅为相对于服务代码目录的子目录。
|
||||
* 目录分隔符请务必使用反斜杠。如:"/com/orange/demo/app/controller"。
|
||||
*/
|
||||
private String path;
|
||||
/**
|
||||
* 如果一个服务内,存在多个Controller目录,将再次生成二级子目录,目录名为groupName。(可使用中文)
|
||||
*/
|
||||
private String groupName;
|
||||
/**
|
||||
* 在当前Controller目录下,需要忽略的Controller列表 (只写类名即可)。如:LoginController。
|
||||
*/
|
||||
private Set<String> skipControllers;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,672 @@
|
||||
package com.orangeforms.apidoc.tools.codeparser;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.orangeforms.common.core.object.Tuple2;
|
||||
import com.orangeforms.apidoc.tools.exception.ApiCodeConfigParseException;
|
||||
import com.thoughtworks.qdox.JavaProjectBuilder;
|
||||
import com.thoughtworks.qdox.model.*;
|
||||
import com.thoughtworks.qdox.model.impl.DefaultJavaParameterizedType;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 解析项目中的接口信息,以及关联的Model、Dto和Mapper,主要用于生成接口文档。
|
||||
*
|
||||
* @author Jerry
|
||||
* @date 2021-06-06
|
||||
*/
|
||||
public class ApiCodeParser {
|
||||
|
||||
private static final String PATH_SEPERATOR = "/";
|
||||
private static final String REQUEST_MAPPING = "RequestMapping";
|
||||
private static final String FULL_REQUEST_MAPPING = "org.springframework.web.bind.annotation.RequestMapping";
|
||||
private static final String GET_MAPPING = "GetMapping";
|
||||
private static final String FULL_GET_MAPPING = "org.springframework.web.bind.annotation.GetMapping";
|
||||
private static final String POST_MAPPING = "PostMapping";
|
||||
private static final String FULL_POST_MAPPING = "org.springframework.web.bind.annotation.PostMapping";
|
||||
private static final String VALUE_PROP = "value";
|
||||
private static final String REQUIRED_PROP = "required";
|
||||
private static final String DELETED_COLUMN = "DeletedFlagColumn";
|
||||
|
||||
/**
|
||||
* 忽略微服务间标准调用接口的导出。
|
||||
*/
|
||||
private static final Set<String> IGNORED_API_METHOD_SET = new HashSet<>(8);
|
||||
|
||||
static {
|
||||
IGNORED_API_METHOD_SET.add("listByIds");
|
||||
IGNORED_API_METHOD_SET.add("getById");
|
||||
IGNORED_API_METHOD_SET.add("existIds");
|
||||
IGNORED_API_METHOD_SET.add("existId");
|
||||
IGNORED_API_METHOD_SET.add("deleteById");
|
||||
IGNORED_API_METHOD_SET.add("deleteBy");
|
||||
IGNORED_API_METHOD_SET.add("listBy");
|
||||
IGNORED_API_METHOD_SET.add("listMapBy");
|
||||
IGNORED_API_METHOD_SET.add("listByNotInList");
|
||||
IGNORED_API_METHOD_SET.add("getBy");
|
||||
IGNORED_API_METHOD_SET.add("countBy");
|
||||
IGNORED_API_METHOD_SET.add("aggregateBy");
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础配置。
|
||||
*/
|
||||
private ApiCodeConfig config;
|
||||
/**
|
||||
* 工程对象。
|
||||
*/
|
||||
private ApiProject apiProject;
|
||||
/**
|
||||
* 项目中所有的解析后Java文件,key是Java对象的全名,如:com.orangeforms.xxxx.Student。
|
||||
*/
|
||||
private final Map<String, JavaClass> projectJavaClassMap = new HashMap<>(128);
|
||||
/**
|
||||
* 存储服务数据。key为配置的serviceName。
|
||||
*/
|
||||
private final Map<String, InternalServiceData> serviceDataMap = new HashMap<>(8);
|
||||
|
||||
/**
|
||||
* 构造函数。
|
||||
*
|
||||
* @param config 配置对象。
|
||||
*/
|
||||
public ApiCodeParser(ApiCodeConfig config) {
|
||||
this.config = config;
|
||||
// 验证配置中的数据是否正确,出现错误直接抛出运行时异常。
|
||||
this.verifyConfigData();
|
||||
// 将配置文件中所有目录相关的参数,全部规格化处理,后续的使用中不用再做处理了。
|
||||
this.normalizeConfigPath();
|
||||
for (ApiCodeConfig.ServiceConfig serviceConfig : config.getServiceList()) {
|
||||
InternalServiceData serviceData = new InternalServiceData();
|
||||
// 仅有微服务项目,需要添加服务路由路径。
|
||||
if (StrUtil.isNotBlank(serviceConfig.getServiceRequestPath())) {
|
||||
String serviceRequestPath = "";
|
||||
if (!serviceRequestPath.equals(PATH_SEPERATOR)) {
|
||||
serviceRequestPath = normalizePath(serviceConfig.getServiceRequestPath());
|
||||
}
|
||||
serviceData.setServiceRequestPath(serviceRequestPath);
|
||||
}
|
||||
serviceDataMap.put(serviceConfig.getServiceName(), serviceData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行解析操作。
|
||||
*
|
||||
* @return 解析后的工程对象。
|
||||
*/
|
||||
public ApiProject doParse() throws IOException {
|
||||
// 先把工程完整编译一遍,以便工程内的Java对象的引用信息更加完整。
|
||||
this.parseProject();
|
||||
// 开始逐级推演。
|
||||
apiProject = new ApiProject();
|
||||
apiProject.setProjectName(config.getProjectName());
|
||||
apiProject.setMicroService(config.getMicroService());
|
||||
apiProject.setServiceList(new LinkedList<>());
|
||||
for (ApiCodeConfig.ServiceConfig serviceConfig : config.getServiceList()) {
|
||||
ApiService apiService = this.parseService(serviceConfig);
|
||||
apiProject.getServiceList().add(apiService);
|
||||
}
|
||||
return apiProject;
|
||||
}
|
||||
|
||||
private void parseProject() throws IOException {
|
||||
JavaProjectBuilder javaProjectBuilder = new JavaProjectBuilder();
|
||||
javaProjectBuilder.setEncoding(StandardCharsets.UTF_8.name());
|
||||
javaProjectBuilder.addSourceTree(new File(config.getProjectRootPath()));
|
||||
// 全部导入,便于后续解析中使用和检索。
|
||||
for (JavaClass javaClass : javaProjectBuilder.getClasses()) {
|
||||
projectJavaClassMap.put(javaClass.getFullyQualifiedName(), javaClass);
|
||||
}
|
||||
}
|
||||
|
||||
private ApiService parseService(ApiCodeConfig.ServiceConfig serviceConfig) {
|
||||
InternalServiceData serviceData = serviceDataMap.get(serviceConfig.getServiceName());
|
||||
ApiService apiService = new ApiService();
|
||||
apiService.setServiceName(serviceConfig.getServiceName());
|
||||
apiService.setShowName(serviceConfig.getShowName());
|
||||
apiService.setPort(serviceConfig.getPort());
|
||||
List<ApiCodeConfig.ControllerInfo> controllerInfoList = serviceConfig.getControllerInfoList();
|
||||
// 准备解析接口文件
|
||||
for (ApiCodeConfig.ControllerInfo controllerInfo : controllerInfoList) {
|
||||
JavaProjectBuilder javaControllerBuilder = new JavaProjectBuilder();
|
||||
javaControllerBuilder.addSourceTree(new File(controllerInfo.getPath()));
|
||||
for (JavaClass javaClass : javaControllerBuilder.getClasses()) {
|
||||
if (controllerInfo.getSkipControllers() != null
|
||||
&& controllerInfo.getSkipControllers().contains(javaClass.getName())) {
|
||||
continue;
|
||||
}
|
||||
ApiClass apiClass = this.parseApiClass(controllerInfo, javaClass.getFullyQualifiedName(), serviceData);
|
||||
if (apiClass != null) {
|
||||
// 如果配置中,为当前ControllerInfo添加了groupName属性,
|
||||
// 所有的生成后接口都会位于serviceName/groupName子目录,否则,都直接位于当前服务的子目录。
|
||||
if (StrUtil.isBlank(apiClass.getGroupName())) {
|
||||
apiService.getDefaultGroupClassSet().add(apiClass);
|
||||
} else {
|
||||
Set<ApiClass> groupedClassList = apiService.getGroupedClassMap()
|
||||
.computeIfAbsent(apiClass.getGroupName(), k -> new TreeSet<>());
|
||||
groupedClassList.add(apiClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return apiService;
|
||||
}
|
||||
|
||||
private ApiClass parseApiClass(
|
||||
ApiCodeConfig.ControllerInfo controllerInfo,
|
||||
String classFullname,
|
||||
InternalServiceData serviceData) {
|
||||
// 去包含工程全部Class的Map中,找到当前ControllerClass。
|
||||
// 之所以这样做,主要是因为全工程分析controller文件,会包含更多更精确的对象关联信息。
|
||||
JavaClass controllerClass = this.projectJavaClassMap.get(classFullname);
|
||||
List<JavaAnnotation> classAnnotations = controllerClass.getAnnotations();
|
||||
boolean hasControllerAnnotation = false;
|
||||
String requestPath = "";
|
||||
for (JavaAnnotation annotation : classAnnotations) {
|
||||
String annotationName = annotation.getType().getValue();
|
||||
if (this.isRequestMapping(annotationName) && annotation.getNamedParameter(VALUE_PROP) != null) {
|
||||
requestPath = StrUtil.removeAll(
|
||||
annotation.getNamedParameter(VALUE_PROP).toString(), "\"");
|
||||
if (requestPath.equals(PATH_SEPERATOR) || StrUtil.isBlank(requestPath)) {
|
||||
requestPath = "";
|
||||
} else {
|
||||
requestPath = normalizePath(requestPath);
|
||||
}
|
||||
}
|
||||
if (isController(annotationName)) {
|
||||
hasControllerAnnotation = true;
|
||||
}
|
||||
}
|
||||
if (!hasControllerAnnotation) {
|
||||
return null;
|
||||
}
|
||||
requestPath = serviceData.getServiceRequestPath() + requestPath;
|
||||
ApiClass apiClass = new ApiClass();
|
||||
apiClass.setName(controllerClass.getName());
|
||||
apiClass.setFullName(controllerClass.getFullyQualifiedName());
|
||||
apiClass.setComment(controllerClass.getComment());
|
||||
apiClass.setGroupName(controllerInfo.getGroupName());
|
||||
apiClass.setRequestPath(requestPath);
|
||||
List<ApiMethod> methodList = this.parseApiMethodList(apiClass, controllerClass);
|
||||
apiClass.setMethodList(methodList);
|
||||
return apiClass;
|
||||
}
|
||||
|
||||
private boolean needToIgnore(JavaMethod method) {
|
||||
return !method.isPublic() || method.isStatic() || IGNORED_API_METHOD_SET.contains(method.getName());
|
||||
}
|
||||
|
||||
private List<ApiMethod> parseApiMethodList(ApiClass apiClass, JavaClass javaClass) {
|
||||
List<ApiMethod> apiMethodList = new LinkedList<>();
|
||||
List<JavaMethod> methodList = javaClass.getMethods();
|
||||
for (JavaMethod method : methodList) {
|
||||
if (this.needToIgnore(method)) {
|
||||
continue;
|
||||
}
|
||||
List<JavaAnnotation> methodAnnotations = method.getAnnotations();
|
||||
Tuple2<String, String> result = this.parseRequestPathAndHttpMethod(methodAnnotations);
|
||||
String methodRequestPath = result.getFirst();
|
||||
String httpMethod = result.getSecond();
|
||||
if (StrUtil.isNotBlank(methodRequestPath)) {
|
||||
ApiMethod apiMethod = new ApiMethod();
|
||||
apiMethod.setName(method.getName());
|
||||
apiMethod.setComment(method.getComment());
|
||||
apiMethod.setHttpMethod(httpMethod);
|
||||
methodRequestPath = StrUtil.removeAll(methodRequestPath, "\"");
|
||||
methodRequestPath = apiClass.getRequestPath() + normalizePath(methodRequestPath);
|
||||
apiMethod.setRequestPath(methodRequestPath);
|
||||
apiMethod.setPathList(StrUtil.splitTrim(apiMethod.getRequestPath(), PATH_SEPERATOR));
|
||||
if (apiMethod.getRequestPath().contains("/listDict")) {
|
||||
apiMethod.setListDictUrl(true);
|
||||
} else if (apiMethod.getRequestPath().endsWith("/list")
|
||||
|| apiMethod.getRequestPath().endsWith("/listWithGroup")
|
||||
|| apiMethod.getRequestPath().contains("/listNotIn")
|
||||
|| apiMethod.getRequestPath().contains("/list")) {
|
||||
apiMethod.setListUrl(true);
|
||||
} else if (apiMethod.getRequestPath().contains("/doLogin")) {
|
||||
apiMethod.setLoginUrl(true);
|
||||
}
|
||||
JavaClass returnClass = method.getReturns();
|
||||
if (returnClass.isVoid()) {
|
||||
apiMethod.setReturnString("void");
|
||||
} else {
|
||||
apiMethod.setReturnString(returnClass.getGenericValue());
|
||||
}
|
||||
apiMethodList.add(apiMethod);
|
||||
List<ApiArgument> apiArgumentList = this.parseApiMethodArgumentList(method);
|
||||
apiMethod.setArgumentList(apiArgumentList);
|
||||
this.classifyArgumentList(apiMethod, apiArgumentList);
|
||||
}
|
||||
}
|
||||
return apiMethodList;
|
||||
}
|
||||
|
||||
private void classifyArgumentList(ApiMethod apiMethod, List<ApiArgument> apiArgumentList) {
|
||||
for (ApiArgument arg : apiArgumentList) {
|
||||
if (arg.getAnnotationType() == ApiArgumentAnnotationType.REQUEST_PARAM) {
|
||||
if (arg.uploadFileParam) {
|
||||
apiMethod.getUploadParamArgumentList().add(arg);
|
||||
} else {
|
||||
apiMethod.getQueryParamArgumentList().add(arg);
|
||||
}
|
||||
}
|
||||
if (arg.getAnnotationType() != ApiArgumentAnnotationType.REQUEST_PARAM) {
|
||||
apiMethod.getJsonParamArgumentList().add(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Tuple2<String, String> parseRequestPathAndHttpMethod(List<JavaAnnotation> methodAnnotations) {
|
||||
for (JavaAnnotation annotation : methodAnnotations) {
|
||||
String annotationName = annotation.getType().getValue();
|
||||
if (GET_MAPPING.equals(annotationName) || FULL_GET_MAPPING.equals(annotationName)) {
|
||||
String methodRequestPath = annotation.getNamedParameter(VALUE_PROP).toString();
|
||||
String httpMethod = "GET";
|
||||
return new Tuple2<>(methodRequestPath, httpMethod);
|
||||
}
|
||||
if (POST_MAPPING.equals(annotationName) || FULL_POST_MAPPING.equals(annotationName)) {
|
||||
String methodRequestPath = annotation.getNamedParameter(VALUE_PROP).toString();
|
||||
String httpMethod = "POST";
|
||||
return new Tuple2<>(methodRequestPath, httpMethod);
|
||||
}
|
||||
}
|
||||
return new Tuple2<>(null, null);
|
||||
}
|
||||
|
||||
private List<ApiArgument> parseApiMethodArgumentList(JavaMethod javaMethod) {
|
||||
List<ApiArgument> apiArgumentList = new LinkedList<>();
|
||||
List<JavaParameter> parameterList = javaMethod.getParameters();
|
||||
if (CollUtil.isEmpty(parameterList)) {
|
||||
return apiArgumentList;
|
||||
}
|
||||
for (JavaParameter parameter : parameterList) {
|
||||
String typeName = parameter.getType().getValue();
|
||||
// 该类型的参数为Validator的验证结果对象,因此忽略。
|
||||
if ("BindingResult".equals(typeName) || this.isServletArgument(typeName)) {
|
||||
continue;
|
||||
}
|
||||
ApiArgument apiArgument = this.parseApiMethodArgument(parameter);
|
||||
apiArgumentList.add(apiArgument);
|
||||
}
|
||||
return apiArgumentList;
|
||||
}
|
||||
|
||||
private String parseMethodArgmentComment(JavaParameter parameter) {
|
||||
String comment = null;
|
||||
JavaExecutable executable = parameter.getExecutable();
|
||||
List<DocletTag> tags = executable.getTagsByName("param");
|
||||
if (CollUtil.isNotEmpty(tags)) {
|
||||
for (DocletTag tag : tags) {
|
||||
if (tag.getValue().startsWith(parameter.getName())) {
|
||||
comment = StrUtil.removePrefix(tag.getValue(), parameter.getName()).trim();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return comment;
|
||||
}
|
||||
|
||||
private ApiArgument parseApiMethodArgument(JavaParameter parameter) {
|
||||
String typeName = parameter.getType().getValue();
|
||||
ApiArgument apiArgument = new ApiArgument();
|
||||
ApiArgumentAnnotation argumentAnnotation =
|
||||
this.parseArgumentAnnotationTypeAndName(parameter.getAnnotations(), parameter.getName());
|
||||
apiArgument.setAnnotationType(argumentAnnotation.getType());
|
||||
apiArgument.setName(argumentAnnotation.getName());
|
||||
apiArgument.setTypeName(typeName);
|
||||
apiArgument.setFullTypeName(parameter.getFullyQualifiedName());
|
||||
if (argumentAnnotation.getType() == ApiArgumentAnnotationType.REQUEST_PARAM) {
|
||||
apiArgument.setRequired(argumentAnnotation.isRequired());
|
||||
}
|
||||
String comment = parseMethodArgmentComment(parameter);
|
||||
apiArgument.setComment(comment);
|
||||
// 文件上传字段,是必填参数。
|
||||
if ("MultipartFile".equals(typeName)) {
|
||||
apiArgument.setUploadFileParam(true);
|
||||
apiArgument.setRequired(true);
|
||||
return apiArgument;
|
||||
}
|
||||
// 对于内置类型,则无需继续处理了。所有和内置类型参数相关的处理,应该在之前完成。
|
||||
if (this.verifyAndSetBuiltinParam(apiArgument, typeName)) {
|
||||
return apiArgument;
|
||||
}
|
||||
// 判断是否为集合类型的参数。
|
||||
if (this.isCollectionType(typeName)) {
|
||||
apiArgument.setCollectionParam(true);
|
||||
if (parameter.getType() instanceof DefaultJavaParameterizedType) {
|
||||
DefaultJavaParameterizedType javaType = (DefaultJavaParameterizedType) parameter.getType();
|
||||
JavaType genericType = javaType.getActualTypeArguments().get(0);
|
||||
ApiModel apiModel = this.buildApiModelForArgument(genericType.getFullyQualifiedName());
|
||||
apiArgument.setModelData(apiModel);
|
||||
apiArgument.setFullTypeName(parameter.getGenericFullyQualifiedName());
|
||||
apiArgument.setTypeName(parameter.getGenericValue());
|
||||
}
|
||||
} else {
|
||||
ApiModel apiModel = this.buildApiModelForArgument(parameter.getFullyQualifiedName());
|
||||
apiArgument.setModelData(apiModel);
|
||||
}
|
||||
return apiArgument;
|
||||
}
|
||||
|
||||
private boolean verifyAndSetBuiltinParam(ApiArgument apiArgument, String typeName) {
|
||||
if ("MyOrderParam".equals(typeName)) {
|
||||
apiArgument.setOrderParam(true);
|
||||
} else if ("MyPageParam".equals(typeName)) {
|
||||
apiArgument.setPageParam(true);
|
||||
} else if ("MyGroupParam".equals(typeName)) {
|
||||
apiArgument.setGroupParam(true);
|
||||
} else if ("MyQueryParam".equals(typeName)) {
|
||||
apiArgument.setQueryParam(true);
|
||||
} else if ("MyAggregationParam".equals(typeName)) {
|
||||
apiArgument.setAggregationParam(true);
|
||||
}
|
||||
return apiArgument.isOrderParam()
|
||||
|| apiArgument.isPageParam()
|
||||
|| apiArgument.isGroupParam()
|
||||
|| apiArgument.isQueryParam()
|
||||
|| apiArgument.isAggregationParam();
|
||||
}
|
||||
|
||||
private ApiArgumentAnnotation parseArgumentAnnotationTypeAndName(
|
||||
List<JavaAnnotation> annotationList, String defaultName) {
|
||||
ApiArgumentAnnotation argumentAnnotation = new ApiArgumentAnnotation();
|
||||
argumentAnnotation.setType(ApiArgumentAnnotationType.REQUEST_PARAM);
|
||||
argumentAnnotation.setName(defaultName);
|
||||
for (JavaAnnotation annotation : annotationList) {
|
||||
String annotationName = annotation.getType().getValue();
|
||||
if ("RequestBody".equals(annotationName)) {
|
||||
argumentAnnotation.setType(ApiArgumentAnnotationType.REQUEST_BODY);
|
||||
return argumentAnnotation;
|
||||
} else if ("MyRequestBody".equals(annotationName)) {
|
||||
String annotationValue = this.getArgumentNameFromAnnotationValue(annotation, VALUE_PROP);
|
||||
argumentAnnotation.setType(ApiArgumentAnnotationType.MY_REQUEST_BODY);
|
||||
argumentAnnotation.setName(annotationValue != null ? annotationValue : defaultName);
|
||||
return argumentAnnotation;
|
||||
} else if ("RequestParam".equals(annotationName)) {
|
||||
String annotationValue = this.getArgumentNameFromAnnotationValue(annotation, VALUE_PROP);
|
||||
argumentAnnotation.setType(ApiArgumentAnnotationType.REQUEST_PARAM);
|
||||
argumentAnnotation.setName(annotationValue != null ? annotationValue : defaultName);
|
||||
String requiredValue = this.getArgumentNameFromAnnotationValue(annotation, REQUIRED_PROP);
|
||||
if (StrUtil.isNotBlank(requiredValue)) {
|
||||
argumentAnnotation.setRequired(Boolean.parseBoolean(requiredValue));
|
||||
}
|
||||
return argumentAnnotation;
|
||||
}
|
||||
}
|
||||
// 缺省为@RequestParam
|
||||
return argumentAnnotation;
|
||||
}
|
||||
|
||||
private String getArgumentNameFromAnnotationValue(JavaAnnotation annotation, String attribute) {
|
||||
Object value = annotation.getNamedParameter(attribute);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
String paramAlias = value.toString();
|
||||
if (StrUtil.isNotBlank(paramAlias)) {
|
||||
paramAlias = StrUtil.removeAll(paramAlias, "\"");
|
||||
}
|
||||
return paramAlias;
|
||||
}
|
||||
|
||||
private ApiModel buildApiModelForArgument(String fullJavaClassName) {
|
||||
// 先从当前服务内的Model中找,如果参数是Model类型的对象,微服务和单体行为一致。
|
||||
ApiModel apiModel = apiProject.getFullNameModelMap().get(fullJavaClassName);
|
||||
if (apiModel != null) {
|
||||
return apiModel;
|
||||
}
|
||||
// 判断工程全局对象映射中是否包括该对象类型,如果不包含,就直接返回了。
|
||||
JavaClass modelClass = projectJavaClassMap.get(fullJavaClassName);
|
||||
if (modelClass == null) {
|
||||
return apiModel;
|
||||
}
|
||||
// 先行解析对象中的字段。
|
||||
apiModel = parseModel(modelClass);
|
||||
apiProject.getFullNameModelMap().put(fullJavaClassName, apiModel);
|
||||
return apiModel;
|
||||
}
|
||||
|
||||
private ApiModel parseModel(JavaClass javaClass) {
|
||||
ApiModel apiModel = new ApiModel();
|
||||
apiModel.setName(javaClass.getName());
|
||||
apiModel.setFullName(javaClass.getFullyQualifiedName());
|
||||
apiModel.setComment(javaClass.getComment());
|
||||
apiModel.setFieldList(new LinkedList<>());
|
||||
List<JavaField> fieldList = javaClass.getFields();
|
||||
for (JavaField field : fieldList) {
|
||||
if (field.isStatic()) {
|
||||
continue;
|
||||
}
|
||||
ApiField apiField = new ApiField();
|
||||
apiField.setName(field.getName());
|
||||
apiField.setComment(field.getComment());
|
||||
apiField.setTypeName(field.getType().getSimpleName());
|
||||
apiModel.getFieldList().add(apiField);
|
||||
}
|
||||
return apiModel;
|
||||
}
|
||||
|
||||
private void verifyConfigData() {
|
||||
if (StrUtil.isBlank(config.getProjectName())) {
|
||||
throw new ApiCodeConfigParseException("ProjectName field can't be EMPTY.");
|
||||
}
|
||||
if (StrUtil.isBlank(config.getBasePackage())) {
|
||||
throw new ApiCodeConfigParseException("BasePackage field can't be EMPTY.");
|
||||
}
|
||||
if (StrUtil.isBlank(config.getProjectRootPath())) {
|
||||
throw new ApiCodeConfigParseException("ProjectRootPath field can't be EMPTY.");
|
||||
}
|
||||
if (!FileUtil.exist(config.getProjectRootPath())) {
|
||||
throw new ApiCodeConfigParseException(
|
||||
"ProjectRootPath doesn't exist, please check ./resources/export-api-config.json as DEFAULT.");
|
||||
}
|
||||
if (config.getMicroService() == null) {
|
||||
throw new ApiCodeConfigParseException("MicroService field can't be NULL.");
|
||||
}
|
||||
if (CollUtil.isEmpty(config.getServiceList())) {
|
||||
throw new ApiCodeConfigParseException("ServiceList field can't be EMPTY.");
|
||||
}
|
||||
this.verifyServiceConfig(config.getServiceList());
|
||||
}
|
||||
|
||||
private void verifyServiceConfig(List<ApiCodeConfig.ServiceConfig> serviceConfigList) {
|
||||
Set<String> serviceNameSet = new HashSet<>(8);
|
||||
Set<String> servicePathSet = new HashSet<>(8);
|
||||
for (ApiCodeConfig.ServiceConfig serviceConfig : serviceConfigList) {
|
||||
if (StrUtil.isBlank(serviceConfig.getServiceName())) {
|
||||
throw new ApiCodeConfigParseException("One of the ServiceName Field in Services List is NULL.");
|
||||
}
|
||||
String serviceName = serviceConfig.getServiceName();
|
||||
if (StrUtil.isBlank(serviceConfig.getServicePath())) {
|
||||
throw new ApiCodeConfigParseException(
|
||||
"The ServicePath Field in Service [" + serviceName + "] is NULL.");
|
||||
}
|
||||
if (serviceNameSet.contains(serviceName)) {
|
||||
throw new ApiCodeConfigParseException("The ServiceName [" + serviceName + "] is duplicated.");
|
||||
}
|
||||
serviceNameSet.add(serviceName);
|
||||
if (servicePathSet.contains(serviceConfig.getServicePath())) {
|
||||
throw new ApiCodeConfigParseException(
|
||||
"The ServicePath [" + serviceConfig.getServicePath() + "] is duplicated.");
|
||||
}
|
||||
servicePathSet.add(serviceConfig.getServicePath());
|
||||
if (StrUtil.isBlank(serviceConfig.getPort())) {
|
||||
throw new ApiCodeConfigParseException(
|
||||
"The Port Field in Service [" + serviceName + "] is NULL.");
|
||||
}
|
||||
this.verifyServiceControllerConfig(serviceConfig.getControllerInfoList(), serviceName);
|
||||
}
|
||||
}
|
||||
|
||||
private void verifyServiceControllerConfig(
|
||||
List<ApiCodeConfig.ControllerInfo> controllerInfoList, String serviceName) {
|
||||
if (CollUtil.isEmpty(controllerInfoList)) {
|
||||
throw new ApiCodeConfigParseException(
|
||||
"The ControllerInfoList Field of Service [" + serviceName + "] is EMPTY");
|
||||
}
|
||||
for (ApiCodeConfig.ControllerInfo controllerInfo : controllerInfoList) {
|
||||
if (StrUtil.isBlank(controllerInfo.getPath())) {
|
||||
throw new ApiCodeConfigParseException(
|
||||
"One of the ControllerInfo.Path Field of Service [" + serviceName + "] is EMPTY");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void normalizeConfigPath() {
|
||||
config.setProjectRootPath(normalizePath(config.getProjectRootPath()));
|
||||
for (ApiCodeConfig.ServiceConfig serviceConfig : config.getServiceList()) {
|
||||
serviceConfig.setServicePath(config.getProjectRootPath() + normalizePath(serviceConfig.getServicePath()));
|
||||
for (ApiCodeConfig.ControllerInfo controllerInfo : serviceConfig.getControllerInfoList()) {
|
||||
controllerInfo.setPath(serviceConfig.getServicePath() + normalizePath(controllerInfo.getPath()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizePath(String path) {
|
||||
if (!path.startsWith(PATH_SEPERATOR)) {
|
||||
path = PATH_SEPERATOR + path;
|
||||
}
|
||||
return StrUtil.removeSuffix(path, PATH_SEPERATOR);
|
||||
}
|
||||
|
||||
private boolean isCollectionType(String typeName) {
|
||||
return "List".equals(typeName) || "Set".equals(typeName) || "Collection".equals(typeName);
|
||||
}
|
||||
|
||||
private boolean isServletArgument(String typeName) {
|
||||
return "HttpServletResponse".equals(typeName) || "HttpServletRequest".equals(typeName);
|
||||
}
|
||||
|
||||
private boolean isController(String annotationName) {
|
||||
return "Controller".equals(annotationName)
|
||||
|| "org.springframework.stereotype.Controller".equals(annotationName)
|
||||
|| "RestController".equals(annotationName)
|
||||
|| "org.springframework.web.bind.annotation.RestController".equals(annotationName);
|
||||
}
|
||||
|
||||
private boolean isRequiredColumn(String annotationName) {
|
||||
return "NotNull".equals(annotationName)
|
||||
|| "javax.validation.constraints.NotNull".equals(annotationName)
|
||||
|| "NotBlank".equals(annotationName)
|
||||
|| "javax.validation.constraints.NotBlank".equals(annotationName)
|
||||
|| "NotEmpty".equals(annotationName)
|
||||
|| "javax.validation.constraints.NotEmpty".equals(annotationName);
|
||||
}
|
||||
|
||||
private boolean isRequestMapping(String name) {
|
||||
return REQUEST_MAPPING.equals(name) || FULL_REQUEST_MAPPING.equals(name);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ApiProject {
|
||||
private String projectName;
|
||||
private Boolean microService;
|
||||
private List<ApiService> serviceList;
|
||||
private Map<String, ApiModel> fullNameModelMap = new HashMap<>(32);
|
||||
private Map<String, ApiModel> simpleNameModelMap = new HashMap<>(32);
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ApiService {
|
||||
private String serviceName;
|
||||
private String showName;
|
||||
private String port;
|
||||
private Set<ApiClass> defaultGroupClassSet = new TreeSet<>();
|
||||
private Map<String, Set<ApiClass>> groupedClassMap = new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ApiClass implements Comparable<ApiClass> {
|
||||
private String name;
|
||||
private String fullName;
|
||||
private String groupName;
|
||||
private String comment;
|
||||
private String requestPath;
|
||||
private List<ApiMethod> methodList;
|
||||
|
||||
@Override
|
||||
public int compareTo(ApiClass o) {
|
||||
return this.name.compareTo(o.name);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ApiMethod {
|
||||
private String name;
|
||||
private String comment;
|
||||
private String returnString;
|
||||
private String requestPath;
|
||||
private String httpMethod;
|
||||
private boolean listDictUrl = false;
|
||||
private boolean listUrl = false;
|
||||
private boolean loginUrl = false;
|
||||
private List<String> pathList = new LinkedList<>();
|
||||
private List<ApiArgument> argumentList;
|
||||
private List<ApiArgument> queryParamArgumentList = new LinkedList<>();
|
||||
private List<ApiArgument> jsonParamArgumentList = new LinkedList<>();
|
||||
private List<ApiArgument> uploadParamArgumentList = new LinkedList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ApiArgument {
|
||||
private String name;
|
||||
private String typeName;
|
||||
private String fullTypeName;
|
||||
private String comment;
|
||||
private Integer annotationType;
|
||||
private boolean required = true;
|
||||
private boolean uploadFileParam = false;
|
||||
private boolean collectionParam = false;
|
||||
private boolean orderParam = false;
|
||||
private boolean pageParam = false;
|
||||
private boolean groupParam = false;
|
||||
private boolean queryParam = false;
|
||||
private boolean aggregationParam = false;
|
||||
private boolean jsonData = false;
|
||||
private ApiModel modelData;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ApiArgumentAnnotation {
|
||||
private String name;
|
||||
private Integer type;
|
||||
private boolean required = true;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ApiModel {
|
||||
private String name;
|
||||
private String fullName;
|
||||
private String comment;
|
||||
private List<ApiField> fieldList;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ApiField {
|
||||
private String name;
|
||||
private String comment;
|
||||
private String typeName;
|
||||
private boolean requiredColumn = false;
|
||||
}
|
||||
|
||||
public static final class ApiArgumentAnnotationType {
|
||||
public static final int REQUEST_PARAM = 0;
|
||||
public static final int REQUEST_BODY = 1;
|
||||
public static final int MY_REQUEST_BODY = 2;
|
||||
|
||||
private ApiArgumentAnnotationType() {
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class InternalServiceData {
|
||||
private String serviceRequestPath = "";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.orangeforms.apidoc.tools.exception;
|
||||
|
||||
/**
|
||||
* 解析接口信息配置对象中的异常。
|
||||
*
|
||||
* @author Jerry
|
||||
* @date 2021-06-06
|
||||
*/
|
||||
public class ApiCodeConfigParseException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 构造函数。
|
||||
*/
|
||||
public ApiCodeConfigParseException() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数。
|
||||
*
|
||||
* @param msg 错误信息。
|
||||
*/
|
||||
public ApiCodeConfigParseException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.orangeforms.apidoc.tools.exception;
|
||||
|
||||
/**
|
||||
* 解析Mybatis XML Mapper中的异常。
|
||||
*
|
||||
* @author Jerry
|
||||
* @date 2021-06-06
|
||||
*/
|
||||
public class MapperParseException extends RuntimeException {
|
||||
|
||||
/**
|
||||
* 构造函数。
|
||||
*/
|
||||
public MapperParseException() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造函数。
|
||||
*
|
||||
* @param msg 错误信息。
|
||||
*/
|
||||
public MapperParseException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package com.orangeforms.apidoc.tools.export;
|
||||
|
||||
import com.orangeforms.apidoc.tools.codeparser.ApiCodeParser;
|
||||
import com.orangeforms.apidoc.tools.util.FreeMarkerUtils;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.TemplateException;
|
||||
import freemarker.template.TemplateExceptionHandler;
|
||||
import freemarker.template.TemplateModelException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 根据代码解析后的工程对象数据,导出到Markdown格式的接口文档文件。
|
||||
*
|
||||
* @author Jerry
|
||||
* @date 2021-06-06
|
||||
*/
|
||||
public class ApiDocExporter {
|
||||
|
||||
private final Configuration config;
|
||||
|
||||
public ApiDocExporter() throws TemplateModelException {
|
||||
config = new Configuration(Configuration.VERSION_2_3_28);
|
||||
config.setNumberFormat("0.####");
|
||||
config.setClassicCompatible(true);
|
||||
config.setAPIBuiltinEnabled(true);
|
||||
config.setClassForTemplateLoading(ApiPostmanExporter.class, "/templates/");
|
||||
config.setDefaultEncoding("UTF-8");
|
||||
config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
|
||||
config.setSharedVariable("freemarkerUtils", new FreeMarkerUtils());
|
||||
config.unsetCacheStorage();
|
||||
config.clearTemplateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Markdown格式的API接口文档。
|
||||
*
|
||||
* @param apiProject 解析后的工程对象。
|
||||
* @param outputFile 生成后的、包含全路径的输出文件名。
|
||||
* @throws IOException 文件操作异常。
|
||||
* @throws TemplateException 模板实例化异常。
|
||||
*/
|
||||
public void doGenerate(ApiCodeParser.ApiProject apiProject, String outputFile) throws IOException, TemplateException {
|
||||
Map<String, Object> paramMap = new HashMap<>(1);
|
||||
paramMap.put("project", apiProject);
|
||||
List<ApiCodeParser.ApiService> newServiceList = new LinkedList<>();
|
||||
if (apiProject.getMicroService()) {
|
||||
// 在微服务场景中,我们需要把upms服务放到最前面显示。
|
||||
for (ApiCodeParser.ApiService apiService : apiProject.getServiceList()) {
|
||||
if ("upms".equals(apiService.getServiceName())) {
|
||||
newServiceList.add(apiService);
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (ApiCodeParser.ApiService apiService : apiProject.getServiceList()) {
|
||||
if (!"upms".equals(apiService.getServiceName())) {
|
||||
newServiceList.add(apiService);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ApiCodeParser.ApiService appService = apiProject.getServiceList().get(0);
|
||||
ApiCodeParser.ApiService newUpmsService = new ApiCodeParser.ApiService();
|
||||
newUpmsService.setDefaultGroupClassSet(appService.getGroupedClassMap().get("upms"));
|
||||
newUpmsService.setServiceName("upms");
|
||||
newUpmsService.setShowName("用户权限模块");
|
||||
newServiceList.add(newUpmsService);
|
||||
ApiCodeParser.ApiService newAppService = new ApiCodeParser.ApiService();
|
||||
newAppService.setDefaultGroupClassSet(appService.getGroupedClassMap().get("app"));
|
||||
newAppService.setServiceName("app");
|
||||
newAppService.setShowName("业务应用模块");
|
||||
newServiceList.add(newAppService);
|
||||
}
|
||||
apiProject.setServiceList(newServiceList);
|
||||
FileUtils.forceMkdirParent(new File(outputFile));
|
||||
config.getTemplate("./api-doc.md.ftl").process(paramMap, new FileWriter(outputFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.orangeforms.apidoc.tools.export;
|
||||
|
||||
import com.orangeforms.apidoc.tools.codeparser.ApiCodeParser;
|
||||
import com.orangeforms.apidoc.tools.util.FreeMarkerUtils;
|
||||
import freemarker.template.Configuration;
|
||||
import freemarker.template.TemplateException;
|
||||
import freemarker.template.TemplateExceptionHandler;
|
||||
import freemarker.template.TemplateModelException;
|
||||
import org.apache.commons.io.FileUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 根据代码解析后的工程对象数据,导出到Postman支持的JSON格式的文件。
|
||||
*
|
||||
* @author Jerry
|
||||
* @date 2021-06-06
|
||||
*/
|
||||
public class ApiPostmanExporter {
|
||||
|
||||
private final Configuration config;
|
||||
|
||||
public ApiPostmanExporter() throws TemplateModelException {
|
||||
config = new Configuration(Configuration.VERSION_2_3_28);
|
||||
config.setNumberFormat("0.####");
|
||||
config.setClassicCompatible(true);
|
||||
config.setAPIBuiltinEnabled(true);
|
||||
config.setClassForTemplateLoading(ApiPostmanExporter.class, "/templates/");
|
||||
config.setDefaultEncoding("UTF-8");
|
||||
config.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
|
||||
config.setSharedVariable("freemarkerUtils", new FreeMarkerUtils());
|
||||
config.unsetCacheStorage();
|
||||
config.clearTemplateCache();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Postman支持的JSON文档。
|
||||
* @param apiProject 解析后的工程对象。
|
||||
* @param outputFile 生成后的、包含全路径的输出文件名。
|
||||
* @throws IOException 文件操作异常。
|
||||
* @throws TemplateException 模板实例化异常。
|
||||
*/
|
||||
public void doGenerate(ApiCodeParser.ApiProject apiProject, String outputFile) throws IOException, TemplateException {
|
||||
Map<String, Object> paramMap = new HashMap<>(1);
|
||||
paramMap.put("project", apiProject);
|
||||
FileUtils.forceMkdirParent(new File(outputFile));
|
||||
config.getTemplate("./postman_collection.json.ftl").process(paramMap, new FileWriter(outputFile));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.orangeforms.apidoc.tools.util;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 仅供Freemarker模板内部使用的Java工具函数。
|
||||
*
|
||||
* @author Jerry
|
||||
* @date 2021-06-06
|
||||
*/
|
||||
public class FreeMarkerUtils {
|
||||
|
||||
/**
|
||||
* 生成GUID。
|
||||
*
|
||||
* @return 生成后的GUID。
|
||||
*/
|
||||
public static String generateGuid() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有构造函数,明确标识该常量类的作用。
|
||||
*/
|
||||
public FreeMarkerUtils() {
|
||||
// FreeMarker的工具对象,Sonarqube建议给出空构造的注释。
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"projectName": "橙单工作流开源版",
|
||||
"basePackage": "com.orangeforms",
|
||||
"projectRootPath": "这里请使用当前工程的根目录,如:e:/xxx/OrangeDemo 或者 /Users/xxx/OrangeDemo",
|
||||
"microService": "false",
|
||||
"serviceList": [
|
||||
{
|
||||
"serviceName": "application-webadmin",
|
||||
"showName": "后台管理服务",
|
||||
"servicePath": "/application-webadmin",
|
||||
"port": "8082",
|
||||
"controllerInfoList": [
|
||||
{
|
||||
"path": "/src/main/java/com/orangeforms/webadmin/app/controller",
|
||||
"groupName": "app"
|
||||
},
|
||||
{
|
||||
"path": "/src/main/java/com/orangeforms/webadmin/upms/controller",
|
||||
"groupName": "upms"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
## 用户登录
|
||||
### 登录接口
|
||||
#### 登录
|
||||
- **URI:** /admin/upms/login/doLogin
|
||||
- **Type:** GET
|
||||
- **Content-Type:** multipart/form-data
|
||||
- **Request-Headers:**
|
||||
Name|Type|Description
|
||||
--|--|--
|
||||
Authorization|String|身份验证的Token
|
||||
- **Request-Parameters:**
|
||||
Parameter|Type|Required|Description
|
||||
--|--|--|--
|
||||
loginName|string|true|用户名
|
||||
password|string|true|加密后的用户密码
|
||||
|
||||
#### 退出
|
||||
- **URI:** /admin/upms/login/logout
|
||||
- **Type:** POST
|
||||
- **Content-Type:** application/json; chartset=utf-8
|
||||
- **Request-Headers:**
|
||||
Name|Type|Description
|
||||
--|--|--
|
||||
Authorization|String|身份验证的Token
|
||||
|
||||
#### 修改密码
|
||||
- **URI:** /admin/upms/login/changePassword
|
||||
- **Type:** POST
|
||||
- **Content-Type:** application/json; chartset=utf-8
|
||||
- **Request-Headers:**
|
||||
Name|Type|Description
|
||||
--|--|--
|
||||
Authorization|String|身份验证的Token
|
||||
- **Request-Parameters:**
|
||||
Parameter|Type|Required|Description
|
||||
--|--|--|--
|
||||
oldPass|string|true|加密后的原用户密码
|
||||
newPass|string|true|加密后的新用户密码
|
||||
<#list project.serviceList as service>
|
||||
|
||||
## ${service.showName}
|
||||
<#list service.defaultGroupClassSet as apiClass>
|
||||
### ${apiClass.name}
|
||||
<#list apiClass.methodList as apiMethod>
|
||||
#### ${apiMethod.name}
|
||||
- **URI:** ${apiMethod.requestPath}
|
||||
- **Type:** ${apiMethod.httpMethod}
|
||||
- **Content-Type:** <#if apiMethod.httpMethod == "GET" || apiMethod.queryParamArgumentList?size gt 0 || apiMethod.uploadParamArgumentList?size gt 0>multipart/form-data<#else>application/json; chartset=utf-8</#if>
|
||||
- **Request-Headers:**
|
||||
Name|Type|Description
|
||||
--|--|--
|
||||
Authorization|String|身份验证的Token
|
||||
<#if apiMethod.queryParamArgumentList?size gt 0 || apiMethod.uploadParamArgumentList?size gt 0>
|
||||
- **Request-Parameters:**
|
||||
Parameter|Type|Required|Description
|
||||
--|--|--|--
|
||||
<#list apiMethod.queryParamArgumentList as apiArgument>
|
||||
<#if apiArgument.modelData??>
|
||||
<#list apiArgument.modelData.tableFieldList as apiField>
|
||||
${apiField.name}|${apiField.typeName}|<#if apiMethod.listDictUrl>false<#else><#if apiField.requiredColumn>true<#else>false</#if></#if>|${apiField.comment}
|
||||
</#list>
|
||||
<#else>
|
||||
${apiArgument.name}|${apiArgument.typeName}|<#if apiMethod.listDictUrl>false<#else><#if apiArgument.required>true<#else>false</#if></#if>|${apiArgument.comment}
|
||||
</#if><#-- apiArgument.modelData?? -->
|
||||
</#list>
|
||||
</#if>
|
||||
<#list apiMethod.uploadParamArgumentList as apiArgument>
|
||||
${apiArgument.name}|File|true|${apiArgument.comment}
|
||||
</#list>
|
||||
<#if apiMethod.jsonParamArgumentList?size gt 0>
|
||||
- **Request-Body:**
|
||||
``` json
|
||||
{
|
||||
<#list apiMethod.jsonParamArgumentList as apiArgument>
|
||||
<#if apiArgument.modelData??>
|
||||
<#if apiArgument.collectionParam>
|
||||
"${apiArgument.name}" : [
|
||||
{
|
||||
<#if apiMethod.listUrl>
|
||||
<#list apiArgument.modelData.filteredFieldList as apiField>
|
||||
"${apiField.name}" : "${apiField.typeName} | false | <#if apiField.name == "searchString">模糊搜索字符串。<#else>${apiField.comment}</#if>"<#if apiField_has_next>,</#if>
|
||||
</#list>
|
||||
<#else><#-- apiMethod.listUrl -->
|
||||
<#list apiArgument.modelData.tableFieldList as apiField>
|
||||
<#if !apiMethod.addUrl || !apiField.primaryKey>
|
||||
"${apiField.name}" : "${apiField.typeName} | <#if apiField.requiredColumn>true<#else>false</#if> | ${apiField.comment}"<#if apiField_has_next>,</#if>
|
||||
</#if>
|
||||
</#list>
|
||||
</#if><#-- apiMethod.listUrl -->
|
||||
}
|
||||
]<#if apiArgument_has_next>,</#if>
|
||||
<#else><#-- apiArgument.collectionParam -->
|
||||
"${apiArgument.name}" : {
|
||||
<#if apiMethod.listUrl>
|
||||
<#list apiArgument.modelData.filteredFieldList as apiField>
|
||||
"${apiField.name}" : "${apiField.typeName} | false | <#if apiField.name == "searchString">模糊搜索字符串。<#else>${apiField.comment}</#if>"<#if apiField_has_next>,</#if>
|
||||
</#list>
|
||||
<#else><#-- apiMethod.listUrl -->
|
||||
<#list apiArgument.modelData.tableFieldList as apiField>
|
||||
<#if !apiMethod.addUrl || !apiField.primaryKey>
|
||||
"${apiField.name}" : "${apiField.typeName} | <#if apiField.requiredColumn>true<#else>false</#if> | ${apiField.comment}"<#if apiField_has_next>,</#if>
|
||||
</#if>
|
||||
</#list>
|
||||
</#if><#-- apiMethod.listUrl -->
|
||||
}<#if apiArgument_has_next>,</#if>
|
||||
</#if><#-- apiArgument.collectionParam -->
|
||||
<#elseif apiArgument.orderParam>
|
||||
"${apiArgument.name}" : [
|
||||
{
|
||||
"fieldName" : "String | false | 排序字段名",
|
||||
"asc" : "Boolean | false | 是否升序"
|
||||
}
|
||||
]<#if apiArgument_has_next>,</#if>
|
||||
<#elseif apiArgument.groupParam>
|
||||
"${apiArgument.name}" : [
|
||||
{
|
||||
"fieldName" : "String | false | 分组字段名",
|
||||
"aliasName" : "String | false | 分组字段别名",
|
||||
"dateAggregateBy" : "String | false | 是否按照日期聚合,可选项(day|month|year)"
|
||||
}
|
||||
]<#if apiArgument_has_next>,</#if>
|
||||
<#elseif apiArgument.pageParam>
|
||||
"${apiArgument.name}" : {
|
||||
"pageNum": "Integer | false | 分页页号",
|
||||
"pageSize": "Integer | false | 每页数据量"
|
||||
}<#if apiArgument_has_next>,</#if>
|
||||
<#elseif apiArgument.queryParam || apiArgument.aggregationParam>
|
||||
${apiArgument.name}" : {
|
||||
|
||||
}<#if apiArgument_has_next>,</#if>
|
||||
<#else><#-- apiArgument.modelData?? -->
|
||||
<#if apiArgument.collectionParam>
|
||||
"${apiArgument.name}" : [ "${apiArgument.typeName} | ${apiArgument.required}<#if apiArgument.comment??> | ${apiArgument.comment}</#if>" ]<#if apiArgument_has_next>,</#if>
|
||||
<#else>
|
||||
"${apiArgument.name}" : "${apiArgument.typeName} | ${apiArgument.required}<#if apiArgument.comment??> | ${apiArgument.comment}</#if>"<#if apiArgument_has_next>,</#if>
|
||||
</#if>
|
||||
</#if><#-- apiArgument.modelData?? -->
|
||||
</#list>
|
||||
}
|
||||
```
|
||||
</#if>
|
||||
</#list><#-- apiClass.methodList as apiMethod -->
|
||||
</#list><#-- upmsClassList as apiClass -->
|
||||
</#list>
|
||||
@@ -0,0 +1,42 @@
|
||||
<#import "postman_common.ftl" as Common>
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "92b51dc5-3611-49ac-8d94-a0718dba5bf1",
|
||||
"name": "${project.projectName}",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||
},
|
||||
"item": [
|
||||
<#list project.serviceList as service>
|
||||
{
|
||||
"name": "${service.serviceName}",
|
||||
"item": [
|
||||
<#if service.groupedClassMap?size gt 0>
|
||||
<#list service.groupedClassMap?keys as groupName>
|
||||
<#assign groupedClassList=service.groupedClassMap[groupName] />
|
||||
{
|
||||
"name": "${groupName}",
|
||||
"item": [
|
||||
<#list groupedClassList as apiClass>
|
||||
{
|
||||
<@Common.generateControllerRequest service apiClass 7/>
|
||||
}<#if apiClass_has_next>,</#if>
|
||||
</#list>
|
||||
],
|
||||
"protocolProfileBehavior": {},
|
||||
"_postman_isSubFolder": true
|
||||
}<#if groupName_has_next>,</#if>
|
||||
</#list>
|
||||
</#if>
|
||||
<#list service.defaultGroupClassSet as apiClass>
|
||||
{
|
||||
<@Common.generateControllerRequest service apiClass 5/>
|
||||
}<#if apiClass_has_next>,</#if>
|
||||
</#list>
|
||||
],
|
||||
"protocolProfileBehavior": {},
|
||||
"_postman_isSubFolder": true
|
||||
}<#if service_has_next>,</#if>
|
||||
</#list><#-- project.serviceList as service -->
|
||||
],
|
||||
"protocolProfileBehavior": {}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
<#macro doIndent level><#if level != 0><#list 0..(level-1) as i> </#list></#if></#macro>
|
||||
|
||||
<#macro generateControllerRequest service apiClass indentLevel>
|
||||
<@doIndent indentLevel/>"name": "${apiClass.name}",
|
||||
<@doIndent indentLevel/>"item": [
|
||||
<#list apiClass.methodList as apiMethod>
|
||||
<@doIndent indentLevel/> {
|
||||
<@doIndent indentLevel/> "name": "${apiMethod.name}",
|
||||
<#if apiMethod.loginUrl>
|
||||
<@doIndent indentLevel/> "event": [
|
||||
<@doIndent indentLevel/> {
|
||||
<@doIndent indentLevel/> "listen": "test",
|
||||
<@doIndent indentLevel/> "script": {
|
||||
<@doIndent indentLevel/> "id": "${freemarkerUtils.generateGuid()}",
|
||||
<@doIndent indentLevel/> "type": "text/javascript",
|
||||
<@doIndent indentLevel/> "exec": [
|
||||
<@doIndent indentLevel/> "pm.test(\"登录操作\", function () {",
|
||||
<@doIndent indentLevel/> " var jsonData = pm.response.json();",
|
||||
<@doIndent indentLevel/> " var token = jsonData.data.tokenData;",
|
||||
<@doIndent indentLevel/> " pm.environment.set(\"token\", token);",
|
||||
<@doIndent indentLevel/> " console.log(\"login token \" + token);",
|
||||
<@doIndent indentLevel/> "});",
|
||||
<@doIndent indentLevel/> ""
|
||||
<@doIndent indentLevel/> ]
|
||||
<@doIndent indentLevel/> }
|
||||
<@doIndent indentLevel/> },
|
||||
<@doIndent indentLevel/> {
|
||||
<@doIndent indentLevel/> "listen": "prerequest",
|
||||
<@doIndent indentLevel/> "script": {
|
||||
<@doIndent indentLevel/> "id": "${freemarkerUtils.generateGuid()}",
|
||||
<@doIndent indentLevel/> "type": "text/javascript",
|
||||
<@doIndent indentLevel/> "exec": [
|
||||
<@doIndent indentLevel/> ""
|
||||
<@doIndent indentLevel/> ]
|
||||
<@doIndent indentLevel/> }
|
||||
<@doIndent indentLevel/> }
|
||||
<@doIndent indentLevel/> ],
|
||||
</#if>
|
||||
<@doIndent indentLevel/> "request": {
|
||||
<@doIndent indentLevel/> "method": "${apiMethod.httpMethod}",
|
||||
<#if apiMethod.loginUrl>
|
||||
<@doIndent indentLevel/> "header": [],
|
||||
<#else>
|
||||
<@doIndent indentLevel/> "header": [
|
||||
<@doIndent indentLevel/> {
|
||||
<@doIndent indentLevel/> "key": "Authorization",
|
||||
<@doIndent indentLevel/> "value": "{{token}}",
|
||||
<@doIndent indentLevel/> "type": "text"
|
||||
<@doIndent indentLevel/> }
|
||||
<@doIndent indentLevel/> ],
|
||||
</#if>
|
||||
<@doIndent indentLevel/> "url": {
|
||||
<@doIndent indentLevel/> "raw": "http://{{host}}:${service.port}/${apiMethod.requestPath}",
|
||||
<@doIndent indentLevel/> "protocol": "http",
|
||||
<@doIndent indentLevel/> "host": [
|
||||
<@doIndent indentLevel/> "{{host}}"
|
||||
<@doIndent indentLevel/> ],
|
||||
<@doIndent indentLevel/> "port": "${service.port}",
|
||||
<@doIndent indentLevel/> "path": [
|
||||
<#list apiMethod.pathList as path>
|
||||
<@doIndent indentLevel/> "${path}"<#if path_has_next>,</#if>
|
||||
</#list>
|
||||
<@doIndent indentLevel/> ]<#if apiMethod.queryParamArgumentList?size gt 0>,</#if>
|
||||
<#if apiMethod.queryParamArgumentList?size gt 0>
|
||||
<@doIndent indentLevel/> "query": [
|
||||
<#list apiMethod.queryParamArgumentList as apiArgument>
|
||||
<#if apiArgument.modelData??>
|
||||
<#list apiArgument.modelData.tableFieldList as apiField>
|
||||
<@doIndent indentLevel/> {
|
||||
<@doIndent indentLevel/> "key": "${apiField.name}",
|
||||
<@doIndent indentLevel/> "value": ""
|
||||
<@doIndent indentLevel/> }<#if apiArgument_has_next || apiField_has_next>,</#if>
|
||||
</#list>
|
||||
<#else>
|
||||
<@doIndent indentLevel/> {
|
||||
<@doIndent indentLevel/> "key": "${apiArgument.name}",
|
||||
<@doIndent indentLevel/> "value": ""
|
||||
<@doIndent indentLevel/> }<#if apiArgument_has_next>,</#if>
|
||||
</#if>
|
||||
</#list>
|
||||
<@doIndent indentLevel/> ]
|
||||
</#if>
|
||||
<@doIndent indentLevel/> }<#if (apiMethod.httpMethod == "POST" && apiMethod.jsonParamArgumentList?size gt 0) || apiMethod.uploadParamArgumentList?size gt 0>,</#if>
|
||||
<#if apiMethod.uploadParamArgumentList?size gt 0>
|
||||
<@doIndent indentLevel/> "body": {
|
||||
<@doIndent indentLevel/> "mode": "formdata",
|
||||
<@doIndent indentLevel/> "formdata": [
|
||||
<#list apiMethod.uploadParamArgumentList as apiArgument>
|
||||
<@doIndent indentLevel/> {
|
||||
<@doIndent indentLevel/> "key": "${apiArgument.name}",
|
||||
<@doIndent indentLevel/> "type": "file",
|
||||
<@doIndent indentLevel/> "src": []
|
||||
<@doIndent indentLevel/> }<#if apiArgument_has_next>,</#if>
|
||||
</#list>
|
||||
<@doIndent indentLevel/> ]
|
||||
<@doIndent indentLevel/> }<#if apiMethod.httpMethod == "POST" && apiMethod.jsonParamArgumentList?size gt 0>,</#if>
|
||||
</#if><#-- apiMethod.uploadParamArgumentList?size gt 0 -->
|
||||
<#if apiMethod.httpMethod == "POST" && apiMethod.jsonParamArgumentList?size gt 0>
|
||||
<@doIndent indentLevel/> "body": {
|
||||
<@doIndent indentLevel/> "mode": "raw",
|
||||
<#if !apiMethod.loginUrl>
|
||||
<@doIndent indentLevel/> "raw": "{\n<#list apiMethod.jsonParamArgumentList as apiArgument><#if apiArgument.modelData??><#if apiArgument.collectionParam>\t\"${apiArgument.name}\" : [\n\t\t{\n<#list apiArgument.modelData.fieldList as apiField><#if apiMethod.listUrl>\t\t\t\"${apiField.name}\" : \"\"<#if apiField_has_next>,</#if>\n<#else>\t\t\t\"${apiField.name}\" : \"<#if apiField.typeName == "Integer" || apiField.typeName == "Long">0</#if>\"<#if apiField_has_next>,</#if>\n</#if><#-- apiMethod.listUrl --></#list>\t\t}\n\t]<#if apiArgument_has_next>,</#if>\n<#else><#-- apiArgument.collectionParam -->\t\"${apiArgument.name}\" : {\n<#list apiArgument.modelData.fieldList as apiField><#if apiMethod.listUrl>\t\t\"${apiField.name}\" : \"\"<#if apiField_has_next>,</#if>\n<#else>\t\t\"${apiField.name}\" : \"<#if apiField.typeName == "Integer" || apiField.typeName == "Long">0</#if>\"<#if apiField_has_next>,</#if>\n</#if><#-- apiMethod.listUrl --></#list>\t}<#if apiArgument_has_next>,</#if>\n</#if><#-- apiArgument.collectionParam --><#elseif apiArgument.orderParam>\t\"${apiArgument.name}\" : [\n\t\t{\n\t\t\t\"fieldName\" : \"\",\n\t\t\t\"asc\" : \"true\"\n\t\t}\n\t]<#if apiArgument_has_next>,</#if>\n<#elseif apiArgument.groupParam>\t\"${apiArgument.name}\" : [\n\t\t{\n\t\t\t\"fieldName\" : \"\",\n\t\t\t\"aliasName\" : \"\",\n\t\t\t\"dateAggregateBy\" : \"\"\n\t\t}\n\t]<#if apiArgument_has_next>,</#if>\n<#elseif apiArgument.pageParam>\t\"${apiArgument.name}\" : {\n\t\t\"pageNum\": \"1\",\n\t\t\"pageSize\": \"10\"\n\t}<#if apiArgument_has_next>,</#if>\n<#elseif apiArgument.queryParam || apiArgument.aggregationParam>\t\"${apiArgument.name}\" : {\n\t}<#if apiArgument_has_next>,</#if>\n<#else><#if apiArgument.collectionParam>\t\"${apiArgument.name}\" : [ ]<#if apiArgument_has_next>,</#if>\n<#else>\t\"${apiArgument.name}\" : \"\"<#if apiArgument_has_next>,</#if>\n</#if></#if><#-- apiArgument.modelData?? --></#list><#-- apiMethod.jsonParamArgumentList?size gt 0 -->}\n",
|
||||
<#else>
|
||||
<@doIndent indentLevel/> "raw": "{\n \"loginName\":\"admin\",\n \"password\":\"IP3ccke3GhH45iGHB5qP9p7iZw6xUyj28Ju10rnBiPKOI35sc%2BjI7%2FdsjOkHWMfUwGYGfz8ik31HC2Ruk%2Fhkd9f6RPULTHj7VpFdNdde2P9M4mQQnFBAiPM7VT9iW3RyCtPlJexQ3nAiA09OqG%2F0sIf1kcyveSrulxembARDbDo%3D\"\n}",
|
||||
</#if>
|
||||
<@doIndent indentLevel/> "options": {
|
||||
<@doIndent indentLevel/> "raw": {
|
||||
<@doIndent indentLevel/> "language": "json"
|
||||
<@doIndent indentLevel/> }
|
||||
<@doIndent indentLevel/> }
|
||||
<@doIndent indentLevel/> }
|
||||
</#if>
|
||||
<@doIndent indentLevel/> },
|
||||
<@doIndent indentLevel/> "response": []
|
||||
<@doIndent indentLevel/> }<#if apiMethod_has_next>,</#if>
|
||||
</#list><#-- apiClass.methodList as apiMethod -->
|
||||
<@doIndent indentLevel/>],
|
||||
<@doIndent indentLevel/>"protocolProfileBehavior": {},
|
||||
<@doIndent indentLevel/>"_postman_isSubFolder": true
|
||||
</#macro>
|
||||
17
images/orange-demo-activiti-service/framework/pom.xml
Normal file
17
images/orange-demo-activiti-service/framework/pom.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>com.orangeforms</groupId>
|
||||
<artifactId>DemoFlow</artifactId>
|
||||
<version>1.0.0</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>framework</artifactId>
|
||||
<packaging>pom</packaging>
|
||||
|
||||
<modules>
|
||||
<module>apidoc-tools</module>
|
||||
</modules>
|
||||
</project>
|
||||
Reference in New Issue
Block a user