001/**
002 * Copyright (c) 2015-2022, Michael Yang 杨福海 (fuhai999@gmail.com).
003 * <p>
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 * <p>
008 * http://www.apache.org/licenses/LICENSE-2.0
009 * <p>
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package io.jboot.apidoc;
017
018import com.alibaba.fastjson.JSONObject;
019import com.alibaba.fastjson.parser.Feature;
020import com.jfinal.core.Controller;
021import com.jfinal.kit.JsonKit;
022import com.jfinal.kit.Ret;
023import com.jfinal.kit.StrKit;
024import com.jfinal.plugin.activerecord.Page;
025import io.jboot.apidoc.annotation.Api;
026import io.jboot.apidoc.annotation.ApiOper;
027import io.jboot.apidoc.annotation.ApiResp;
028import io.jboot.apidoc.annotation.ApiResps;
029import io.jboot.utils.*;
030
031import java.io.File;
032import java.lang.reflect.Method;
033import java.lang.reflect.Modifier;
034import java.util.*;
035
036public class ApiDocManager {
037
038    private static final ApiDocManager me = new ApiDocManager();
039
040    public static ApiDocManager me() {
041        return me;
042    }
043
044    //渲染器
045    private ApiDocRender render = ApiDocRender.MARKDOWN_RENDER;
046
047    //每个类对于的属性名称,一般支持从数据库读取 字段配置 填充,来源于 api-remarks.json
048    private Map<String, Map<String, String>> modelFieldRemarks = new HashMap<>();
049    private Map<String, Map<String, String>> defaultModelFieldRemarks = new HashMap<>();
050
051    //ClassType Mocks,来源于 api-mock.json
052    private Map<String, Object> classTypeMockDatas = new HashMap<>();
053    private Map<Class<?>, ApiMockBuilder> classTypeMockBuilders = new HashMap<>();
054
055    //ApiOperation 排序方式
056    private Comparator<ApiOperation> operationComparator;
057
058    private ApiDocManager() {
059        initDefaultClassTypeMockBuilder();
060    }
061
062    private void initDefaultClassTypeMockBuilder() {
063        addClassTypeMockBuilders(Ret.class, ApiMockBuilders.retBuilder);
064        addClassTypeMockBuilders(Map.class, ApiMockBuilders.mapBuilder);
065        addClassTypeMockBuilders(List.class, ApiMockBuilders.listBuilder);
066        addClassTypeMockBuilders(Page.class, ApiMockBuilders.pageBuilder);
067        addClassTypeMockBuilders(String.class, ApiMockBuilders.stringBuilder);
068    }
069
070    public ApiDocRender getRender() {
071        return render;
072    }
073
074    public void setRender(ApiDocRender render) {
075        this.render = render;
076    }
077
078    public Map<String, Map<String, String>> getModelFieldRemarks() {
079        return modelFieldRemarks;
080    }
081
082    public void setModelFieldRemarks(Map<String, Map<String, String>> modelFieldRemarks) {
083        this.modelFieldRemarks = modelFieldRemarks;
084    }
085
086    public void addModelFieldRemarks(String classOrSimpleName, Map<String, String> fieldNames) {
087        this.modelFieldRemarks.put(classOrSimpleName, fieldNames);
088    }
089
090    public Map<String, Object> getClassTypeMockDatas() {
091        return classTypeMockDatas;
092    }
093
094    public void setClassTypeMockDatas(Map<String, Object> classTypeMockDatas) {
095        this.classTypeMockDatas = classTypeMockDatas;
096    }
097
098    public void addClassTypeMocks(String classType, Object mockData) {
099        this.classTypeMockDatas.put(classType, mockData);
100    }
101
102    public Object getClassTypeMockData(String classType) {
103        return this.classTypeMockDatas.get(classType);
104    }
105
106    public Comparator<ApiOperation> getOperationComparator() {
107        return operationComparator;
108    }
109
110    public void setOperationComparator(Comparator<ApiOperation> operationComparator) {
111        this.operationComparator = operationComparator;
112    }
113
114    public Map<Class<?>, ApiMockBuilder> getClassTypeMockBuilders() {
115        return classTypeMockBuilders;
116    }
117
118    public void setClassTypeMockBuilders(Map<Class<?>, ApiMockBuilder> classTypeMockBuilders) {
119        this.classTypeMockBuilders = classTypeMockBuilders;
120    }
121
122    public void addClassTypeMockBuilders(Class<?> forClass, ApiMockBuilder builder) {
123        this.classTypeMockBuilders.put(forClass, builder);
124    }
125
126
127    String buildMockJson(ClassType classType, Method method) {
128        return ApiDocUtil.prettyJson(JsonKit.toJson(doBuildMockObject(classType, method, 0)));
129    }
130
131
132    Object doBuildMockObject(ClassType classType, Method method, int level) {
133        Object retObject = getClassTypeMockData(classType.toString());
134
135        if (retObject == null) {
136            retObject = getClassTypeMockData(classType.getMainClass().getName());
137        }
138
139        if (retObject == null) {
140            retObject = getClassTypeMockData(StrKit.firstCharToLowerCase(classType.getMainClass().getSimpleName()));
141        }
142
143        if (retObject != null) {
144            return retObject;
145        }
146
147        for (Class<?> aClass : classTypeMockBuilders.keySet()) {
148            if (aClass.isAssignableFrom(classType.getMainClass())) {
149                Object object = classTypeMockBuilders.get(aClass).build(classType, method, level);
150                if (object != null) {
151                    return object;
152                }
153            }
154        }
155        return null;
156    }
157
158
159    Map<String, List<ApiResponse>> buildRemarks(ClassType classType, Method method) {
160        Map<String, List<ApiResponse>> retMap = new LinkedHashMap<>();
161        doBuildRemarks(retMap, classType, method, 0);
162        return retMap;
163    }
164
165
166    private void doBuildRemarks(Map<String, List<ApiResponse>> retMap, ClassType classType, Method method, int level) {
167        Class<?> mainClass = classType.getMainClass();
168
169        Set<ApiResponse> apiResponses = new LinkedHashSet<>();
170
171        //根据默认的配置构建
172        doBuildRemarksByDefault(apiResponses, classType, method);
173
174        List<Class<?>> dataTypeClasses = null;
175
176        //根据方法的 @Resp 来构建
177        if (level == 0) {
178            dataTypeClasses = doBuildRemarksByMethodAnnotation(apiResponses, method);
179        }
180
181        //根据配置文件来构建
182        doBuildRemarksByConfig(apiResponses, classType, method);
183
184        if (!apiResponses.isEmpty()) {
185            retMap.put(mainClass.getSimpleName(), new LinkedList<>(apiResponses));
186        }
187
188        //必须执行在 retMap.put 之后,才能保证 remarks 表格在文档中的顺序
189        if (dataTypeClasses != null) {
190            for (Class<?> dataType : dataTypeClasses) {
191                doBuildRemarks(retMap, new ClassType(dataType), method, level + 1);
192            }
193        }
194
195
196        ClassType[] types = classType.getGenericTypes();
197        if (types != null) {
198            for (ClassType type : types) {
199                doBuildRemarks(retMap, type, method, level + 1);
200            }
201        }
202    }
203
204
205    private void doBuildRemarksByDefault(Set<ApiResponse> apiResponses, ClassType classType, Method method1) {
206        Map<String, String> defaultModelRemarks = defaultModelFieldRemarks.get(classType.getMainClass().getName());
207        if (defaultModelRemarks == null || defaultModelRemarks.isEmpty()) {
208            return;
209        }
210
211        List<Method> getterMethods = ReflectUtil.searchMethodList(classType.getMainClass(), m -> m.getParameterCount() == 0
212                && m.getReturnType() != void.class
213                && Modifier.isPublic(m.getModifiers())
214                && (m.getName().startsWith("get") || m.getName().startsWith("is"))
215                && !"getClass".equals(m.getName())
216        );
217
218        Map<String, Method> filedAndMethodMap = new HashMap<>();
219        for (Method getterMethod : getterMethods) {
220            filedAndMethodMap.put(ApiDocUtil.getterMethod2Field(getterMethod), getterMethod);
221        }
222
223        for (String key : defaultModelRemarks.keySet()) {
224
225            ApiResponse apiResponse = new ApiResponse();
226            apiResponse.setField(key);
227            apiResponse.setRemarks(defaultModelRemarks.get(key));
228
229            Method getterMethod = filedAndMethodMap.get(key);
230            if (getterMethod != null) {
231                apiResponse.setDataAndClassType(getterMethod.getReturnType());
232            }
233            //若没有 getter 方法,一般情况下 map 或者 ret 等
234            //此时,需要通过 Mock 数据来对 key 的 dataType 进行推断
235            else {
236                Object object = doBuildMockObject(classType, method1, 0);
237                if (object instanceof Map) {
238                    Object value = ((Map<?, ?>) object).get(key);
239                    if (value != null) {
240                        apiResponse.setDataAndClassType(value.getClass());
241                    }
242                }
243            }
244
245            apiResponses.add(apiResponse);
246        }
247
248    }
249
250
251    private List<Class<?>> doBuildRemarksByMethodAnnotation(Set<ApiResponse> apiResponses, Method method) {
252
253        List<ApiResponse> apiResponses1 = new LinkedList<>();
254        List<Class<?>> dataTypes = new ArrayList<>();
255
256        ApiResps apiResps = method.getAnnotation(ApiResps.class);
257        if (apiResps != null) {
258            for (ApiResp apiResp : apiResps.value()) {
259                apiResponses1.add(new ApiResponse(apiResp));
260                dataTypes.add(apiResp.dataType());
261            }
262        }
263
264        ApiResp apiResp = method.getAnnotation(ApiResp.class);
265        if (apiResp != null) {
266            apiResponses1.add(new ApiResponse(apiResp));
267            dataTypes.add(apiResp.dataType());
268        }
269
270        apiResponses.addAll(apiResponses1);
271        return dataTypes;
272    }
273
274
275    private void doBuildRemarksByConfig(Set<ApiResponse> apiResponses, ClassType classType, Method method1) {
276        Map<String, String> configRemarks = getConfigRemarks(classType.getMainClass());
277        if (configRemarks == null || configRemarks.isEmpty()) {
278            return;
279        }
280
281        List<Method> getterMethods = ReflectUtil.searchMethodList(classType.getMainClass(), m -> m.getParameterCount() == 0
282                && m.getReturnType() != void.class
283                && Modifier.isPublic(m.getModifiers())
284                && (m.getName().startsWith("get") || m.getName().startsWith("is"))
285                && !"getClass".equals(m.getName())
286        );
287
288        Map<String, Method> filedAndMethodMap = new HashMap<>();
289        for (Method getterMethod : getterMethods) {
290            filedAndMethodMap.put(ApiDocUtil.getterMethod2Field(getterMethod), getterMethod);
291        }
292
293
294        for (String key : configRemarks.keySet()) {
295
296            ApiResponse apiResponse = new ApiResponse();
297            apiResponse.setField(key);
298            apiResponse.setRemarks(configRemarks.get(key));
299
300            Method getterMethod = filedAndMethodMap.get(key);
301            if (getterMethod != null) {
302                apiResponse.setDataAndClassType(getterMethod.getReturnType());
303            }
304            //若没有 getter 方法,一般情况下 map 或者 ret 等
305            //此时,需要通过 Mock 数据来对 key 的 dataType 进行推断
306            else {
307                Object object = doBuildMockObject(classType, method1, 0);
308                if (object instanceof Map) {
309                    Object value = ((Map<?, ?>) object).get(key);
310                    if (value != null) {
311                        apiResponse.setDataAndClassType(value.getClass());
312                    }
313                }
314            }
315
316            apiResponses.add(apiResponse);
317        }
318    }
319
320    private Map<String, String> getConfigRemarks(Class<?> clazz) {
321        Map<String, String> ret = modelFieldRemarks.get(clazz.getName());
322        return ret != null ? ret : modelFieldRemarks.get(StrKit.firstCharToLowerCase(clazz.getSimpleName()));
323    }
324
325
326    /**
327     * 生成 API 文档
328     *
329     * @param config
330     */
331    public void genDocs(ApiDocConfig config) {
332        List<Class> controllerClasses = ClassScanner.scanClass(aClass -> Controller.class.isAssignableFrom(aClass) && aClass.getAnnotation(Api.class) != null);
333        if (controllerClasses.isEmpty()) {
334            return;
335        }
336
337        initMockJson(config);
338        initModelRemarks(config);
339
340        List<ApiDocument> apiDocuments = new ArrayList<>();
341
342        //所有 Controller 构建为同一个文档
343        if (config.isAllInOneEnable()) {
344            ApiDocument document = new ApiDocument();
345            document.setValue(config.getAllInOneTitle());
346            document.setNotes(config.getAllInOneNotes());
347            document.setFilePath(config.getAllInOneFilePath());
348
349            buildAllInOneDocument(document, controllerClasses, config);
350
351            apiDocuments.add(document);
352        }
353        //不同的 Controller 构建为不同的文档
354        else {
355            for (Class<?> controllerClass : controllerClasses) {
356                if (StrUtil.isNotBlank(config.getPackagePrefix())) {
357                    if (controllerClass.getName().startsWith(config.getPackagePrefix())) {
358                        apiDocuments.add(buildDocument(controllerClass, config));
359                    }
360                } else {
361                    apiDocuments.add(buildDocument(controllerClass, config));
362                }
363            }
364        }
365
366        if (render != null) {
367            render.render(apiDocuments, config);
368        }
369    }
370
371
372    private void initMockJson(ApiDocConfig config) {
373        File mockJsonFile = new File(config.getMockJsonPathAbsolute());
374        if (mockJsonFile.exists()) {
375            String mockJsonString = FileUtil.readString(mockJsonFile);
376            JSONObject mockJsonObject = JSONObject.parseObject(mockJsonString, Feature.OrderedField);
377            for (String classTypeKey : mockJsonObject.keySet()) {
378                this.classTypeMockDatas.put(classTypeKey, mockJsonObject.get(classTypeKey));
379            }
380        }
381    }
382
383
384    private void initModelRemarks(ApiDocConfig config) {
385
386        Map<String, String> pageRemarks = new LinkedHashMap<>();
387        pageRemarks.put("totalRow", "总行数");
388        pageRemarks.put("pageNumber", "当前页码");
389        pageRemarks.put("firstPage", "是否是第一页");
390        pageRemarks.put("lastPage", "是否是最后一页");
391        pageRemarks.put("totalPage", "总页数");
392        pageRemarks.put("pageSize", "每页数据量");
393        pageRemarks.put("list", "数据列表");
394        defaultModelFieldRemarks.put(Page.class.getName(), pageRemarks);
395
396
397        Map<String, String> retRemarks = new LinkedHashMap<>();
398        retRemarks.put("state", "状态,成功 ok,失败 fail");
399        defaultModelFieldRemarks.put(Ret.class.getName(), retRemarks);
400
401        Map<String, String> apiRetRemarks = new LinkedHashMap<>();
402        apiRetRemarks.put("state", "状态,成功 ok,失败 fail");
403        apiRetRemarks.put("errorCode", "错误码,可能返回 null");
404        apiRetRemarks.put("message", "错误消息");
405        apiRetRemarks.put("data", "数据");
406        defaultModelFieldRemarks.put(ApiRet.class.getName(), apiRetRemarks);
407
408
409        File modelJsonFile = new File(config.getRemarksJsonPathAbsolute());
410        if (modelJsonFile.exists()) {
411            String modelJsonString = FileUtil.readString(modelJsonFile);
412            JSONObject modelJsonObject = JSONObject.parseObject(modelJsonString, Feature.OrderedField);
413            for (String classOrSimpleName : modelJsonObject.keySet()) {
414                Map<String, String> remarks = new LinkedHashMap<>();
415                JSONObject modelRemarks = modelJsonObject.getJSONObject(classOrSimpleName);
416                modelRemarks.forEach((k, v) -> remarks.put(k, String.valueOf(v)));
417                addModelFieldRemarks(classOrSimpleName, remarks);
418            }
419        }
420    }
421
422
423    private void buildAllInOneDocument(ApiDocument document, List<Class> controllerClasses, ApiDocConfig config) {
424        for (Class<?> controllerClass : controllerClasses) {
425            buildOperation(document, controllerClass, config);
426        }
427
428        List<ApiOperation> operations = document.getApiOperations();
429        if (operations != null) {
430            if (operationComparator != null) {
431                operations.sort(operationComparator);
432            } else {
433                operations.sort(Comparator.comparing(ApiOperation::getActionKey));
434            }
435        }
436    }
437
438
439    private ApiDocument buildDocument(Class<?> controllerClass, ApiDocConfig config) {
440
441        Api api = controllerClass.getAnnotation(Api.class);
442        ApiDocument document = new ApiDocument();
443        document.setControllerClass(controllerClass);
444        document.setValue(api.value());
445        document.setNotes(api.notes());
446        document.setFilePathByControllerPath(ApiDocUtil.getControllerPath(controllerClass));
447
448
449        String filePath = api.filePath();
450        if (StrUtil.isNotBlank(filePath)) {
451            document.setFilePath(filePath);
452        }
453
454        buildOperation(document, controllerClass, config);
455
456        List<ApiOperation> operations = document.getApiOperations();
457        if (operations != null) {
458            if (operationComparator != null) {
459                operations.sort(operationComparator);
460            } else {
461                operations.sort(new Comparator<ApiOperation>() {
462                    @Override
463                    public int compare(ApiOperation o1, ApiOperation o2) {
464                        return o1.getOrderNo() == o2.getOrderNo() ? o1.getMethod().getName().compareTo(o2.getMethod().getName()) : o1.getOrderNo() - o2.getOrderNo();
465                    }
466                });
467            }
468        }
469
470        return document;
471    }
472
473
474    private void buildOperation(ApiDocument document, Class<?> controllerClass, ApiDocConfig config) {
475
476        List<Method> methods = ReflectUtil.searchMethodList(controllerClass,
477                method -> method.getAnnotation(ApiOper.class) != null && Modifier.isPublic(method.getModifiers()));
478
479        String controllerPath = ApiDocUtil.getControllerPath(controllerClass);
480        HttpMethod defaultHttpMethod = ApiDocUtil.getControllerMethod(controllerClass);
481
482        for (Method method : methods) {
483            ApiOper apiOper = method.getAnnotation(ApiOper.class);
484
485            ApiOperation apiOperation = new ApiOperation();
486            apiOperation.setControllerClass(controllerClass);
487
488            Class<?> containerClass = apiOper.containerClass() != void.class ? apiOper.containerClass() : config.getDefaultContainerClass();
489            apiOperation.setMethodAndInfo(method, controllerPath, ApiDocUtil.getMethodHttpMethods(method, defaultHttpMethod), containerClass);
490
491
492            apiOperation.setValue(apiOper.value());
493            apiOperation.setNotes(apiOper.notes());
494            apiOperation.setParaNotes(apiOper.paraNotes());
495            apiOperation.setOrderNo(apiOper.orderNo());
496            apiOperation.setContentType(apiOper.contentType());
497
498            document.addOperation(apiOperation);
499        }
500
501
502        Api api = controllerClass.getAnnotation(Api.class);
503        if (api != null) {
504            Class<?>[] collectClasses = api.collect();
505            for (Class<?> collectClass : collectClasses) {
506
507                List<Method> collectMethods = ReflectUtil.searchMethodList(collectClass,
508                        method -> method.getAnnotation(ApiOper.class) != null && Modifier.isPublic(method.getModifiers()));
509
510                String collectControllerPath = ApiDocUtil.getControllerPath(collectClass);
511                HttpMethod collectDefaultHttpMethod = ApiDocUtil.getControllerMethod(controllerClass);
512
513                for (Method method : collectMethods) {
514                    ApiOper apiOper = method.getAnnotation(ApiOper.class);
515
516                    ApiOperation apiOperation = new ApiOperation();
517                    apiOperation.setControllerClass(collectClass);
518
519                    Class<?> containerClass = apiOper.containerClass() != void.class ? apiOper.containerClass() : config.getDefaultContainerClass();
520                    apiOperation.setMethodAndInfo(method, collectControllerPath, ApiDocUtil.getMethodHttpMethods(method, collectDefaultHttpMethod), containerClass);
521
522
523                    apiOperation.setValue(apiOper.value());
524                    apiOperation.setNotes(apiOper.notes());
525                    apiOperation.setParaNotes(apiOper.paraNotes());
526                    apiOperation.setOrderNo(apiOper.orderNo());
527                    apiOperation.setContentType(apiOper.contentType());
528
529                    document.addOperation(apiOperation);
530                }
531            }
532        }
533    }
534
535
536}