diff --git a/pom.xml b/pom.xml index c4f7e0004..1a80c0a65 100644 --- a/pom.xml +++ b/pom.xml @@ -73,6 +73,11 @@ slf4j-api 2.0.7 + + org.eclipse.jgit + org.eclipse.jgit + 5.13.2.202306221912-r + diff --git a/src/main/java/com/ly/doc/extension/dict/DictionaryValuesResolver.java b/src/main/java/com/ly/doc/extension/dict/DictionaryValuesResolver.java index 58e984892..f5adce8d5 100644 --- a/src/main/java/com/ly/doc/extension/dict/DictionaryValuesResolver.java +++ b/src/main/java/com/ly/doc/extension/dict/DictionaryValuesResolver.java @@ -22,7 +22,6 @@ import com.power.common.model.EnumDictionary; -import javax.annotation.Nonnull; import java.util.Collection; import java.util.Collections; @@ -38,8 +37,7 @@ public interface DictionaryValuesResolver { * @param clazz dictionary class * @return the dictionary */ - @Nonnull - default Collection resolve(@Nonnull Class clazz) { + default Collection resolve(Class clazz) { return resolve(); } @@ -48,7 +46,6 @@ default Collection resolve(@Nonnull Class clazz * * @see #resolve(Class) */ - @Nonnull default Collection resolve() { return Collections.emptyList(); } diff --git a/src/main/java/com/ly/doc/factory/BuildTemplateFactory.java b/src/main/java/com/ly/doc/factory/BuildTemplateFactory.java index f1f0bb308..b439c92e7 100644 --- a/src/main/java/com/ly/doc/factory/BuildTemplateFactory.java +++ b/src/main/java/com/ly/doc/factory/BuildTemplateFactory.java @@ -21,6 +21,7 @@ package com.ly.doc.factory; import com.ly.doc.constants.FrameworkEnum; +import com.ly.doc.model.IDoc; import com.ly.doc.template.IDocBuildTemplate; /** @@ -35,10 +36,10 @@ public class BuildTemplateFactory { * @param API doc type * @return Implements of IDocBuildTemplate */ - public static IDocBuildTemplate getDocBuildTemplate(String framework) { + public static IDocBuildTemplate getDocBuildTemplate(String framework) { String className = FrameworkEnum.getClassNameByFramework(framework); try { - return (IDocBuildTemplate) Class.forName(className).newInstance(); + return (IDocBuildTemplate) Class.forName(className).newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { diff --git a/src/main/java/com/ly/doc/helper/DocBuildHelper.java b/src/main/java/com/ly/doc/helper/DocBuildHelper.java new file mode 100644 index 000000000..4d1d7bcc8 --- /dev/null +++ b/src/main/java/com/ly/doc/helper/DocBuildHelper.java @@ -0,0 +1,399 @@ +/* + * Copyright (C) 2018-2023 smart-doc + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.ly.doc.helper; + +import com.ly.doc.builder.ProjectDocConfigBuilder; +import com.ly.doc.model.ApiConfig; +import com.ly.doc.model.IDoc; +import com.ly.doc.model.IMethod; +import com.ly.doc.model.dependency.ApiDependency; +import com.ly.doc.model.dependency.DependencyTree; +import com.ly.doc.model.dependency.FileDiff; +import com.power.common.util.CollectionUtil; +import com.power.common.util.StringUtil; +import com.thoughtworks.qdox.JavaProjectBuilder; +import com.thoughtworks.qdox.model.JavaClass; +import com.thoughtworks.qdox.model.JavaType; +import org.eclipse.jgit.diff.DiffEntry; + +import java.io.File; +import java.util.*; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +/** + * @author Fio + */ +public class DocBuildHelper { + + private JavaProjectBuilder projectBuilder; + + /** + * {@link ApiConfig#getCodePath()} + */ + private String codePath; + + private DependencyTree dependencyTree; + + private final GitHelper gitHelper = GitHelper.create(); + + /** + * changed file list + * value set within {@link #getChangedFilesFromVCS(Predicate)} + * value get within {@link #mergeDependencyTree(List)} + */ + private Set fileDiffList = Collections.emptySet(); + + private DocBuildHelper() { + + } + + public static DocBuildHelper create(ProjectDocConfigBuilder configBuilder) { + ApiConfig apiConfig = configBuilder.getApiConfig(); + + String baseDir = apiConfig.getBaseDir(); + String codePath = apiConfig.getCodePath(); + + if (StringUtil.isEmpty(baseDir)) { + throw new RuntimeException("ERROR: The baseDir can't be empty."); + } + if (StringUtil.isEmpty(codePath)) { + throw new RuntimeException("ERROR: The codePath can't be empty."); + } + + DocBuildHelper helper = new DocBuildHelper(); + helper.projectBuilder = configBuilder.getJavaProjectBuilder(); + helper.codePath = codePath; + helper.dependencyTree = DependencyTree.detect(baseDir); + + return helper; + } + + /** + * Read the dependency-tree-file from baseDir + * + * @return DependencyTree instance + */ + public DependencyTree getDependencyTree() { + return dependencyTree; + } + + private void writeDependencyTree(List dependencyTree) { + if (gitHelper.notGitRepo()) { + return; + } + + String commitId = gitHelper.getLatestCommitId(); + + if (dependencyTree == null) { + dependencyTree = Collections.emptyList(); + } + + List mergedDependencyTree = mergeDependencyTree(dependencyTree); + this.dependencyTree.setConfig(commitId, mergedDependencyTree); + + DependencyTree.write(this.dependencyTree); + } + + private List mergeDependencyTree(List newDependencyTree) { + List oldDependencyTree = new ArrayList<>(this.dependencyTree.getDependencyTree()); + + // remove the deleted or deprecated dependencies + List deletedClazz = this.fileDiffList.stream() + // newQualifiedName equals /dev/null means the class is deleted + .filter(item -> "/dev/null".equals(item.getNewQualifiedName())) + .map(FileDiff::getOldQualifiedName) + .distinct() + .collect(Collectors.toList()); + List newDependencyApiClasses = newDependencyTree.stream() + .map(ApiDependency::getClazz).distinct() + .collect(Collectors.toList()); + List deprecatedClazz = this.fileDiffList.stream() + .filter(FileDiff::isEntryPoint) + .map(FileDiff::getNewQualifiedName) + .filter(item -> { + boolean contains = newDependencyApiClasses.contains(item); + if (contains) { + return false; + } + + try { + // This logic is copied from RpcDocBuildTemplate#handleJavaApiDoc. + // Used for mark deprecated api class correctly. + JavaClass cls = projectBuilder.getClassByName(item); + List clsImplements = cls.getImplements(); + if (CollectionUtil.isNotEmpty(clsImplements) && !cls.isInterface()) { + return clsImplements.stream() + .map(JavaType::getCanonicalName) + .noneMatch(newDependencyApiClasses::contains); + } + } catch (Exception ignore) { + } + + return false; + }) + .collect(Collectors.toList()); + oldDependencyTree.removeIf(dependency -> + deletedClazz.contains(dependency.getClazz()) + || deprecatedClazz.contains(dependency.getClazz()) + || deprecatedClazz.stream().anyMatch(deprecate -> + dependency.getDerivedClazz().contains(deprecate)) + ); + + // replace the old dependency tree with new dependency + oldDependencyTree.replaceAll(dependency -> { + String docClazz = dependency.getClazz(); + + ApiDependency apiDependency = newDependencyTree.stream() + .filter(newDependency -> docClazz.equals(newDependency.getClazz())) + .findFirst() + .orElse(dependency); + + // replace and remove from newDependencyTree + newDependencyTree.removeIf(newDependency -> newDependency.equals(apiDependency)); + + return apiDependency; + }); + + // add new dependency + if (CollectionUtil.isNotEmpty(newDependencyTree)) { + oldDependencyTree.addAll(newDependencyTree); + } + + return oldDependencyTree; + } + + /** + * Find and gather classes and their dependencies. + *
+ * When a class is modified within the git tree, and it is part of an endpoint argument or return value, + * this method will also include the classes containing these endpoints classes. + *
+ * If all modified classes are not part of the API dependency tree (e.g., they are services or mappers), + * this method will return an empty collection, as they do not impact the API documentation. + */ + public Set getChangedFilesFromVCS(Predicate isEntryPoint) { + String commitId = dependencyTree.getCommitId(); + List diff = new ArrayList<>(gitHelper.getDiff(commitId)); + Set uncommitted = new HashSet<>(gitHelper.getUncommitted()); + Set untracked = new HashSet<>(gitHelper.getUntracked()); + + if (CollectionUtil.isEmpty(diff) + && CollectionUtil.isEmpty(uncommitted) + && CollectionUtil.isEmpty(untracked)) { + return Collections.emptySet(); + } + + Set fileDiffList = getChangedFiles(diff, uncommitted, untracked); + populateRelatedClazzAndMarkEntryPoint(fileDiffList, isEntryPoint); + + this.fileDiffList = fileDiffList; + + return fileDiffList; + } + + private Set getChangedFiles(List diff, Set uncommitted, Set untracked) { + diff.removeIf(item -> !isSupportedSourceCodeType(item.getNewPath())); + uncommitted.removeIf(item -> !isSupportedSourceCodeType(item)); + untracked.removeIf(item -> !isSupportedSourceCodeType(item)); + + Set diffList = new HashSet<>(diff.size() + uncommitted.size() + untracked.size()); + + // diff in git tree + diff.forEach(entry -> { + FileDiff fileDiff = new FileDiff(); + + String changeType = entry.getChangeType().name(); + fileDiff.setChangeType(FileDiff.ChangeType.valueOf(changeType)); + fileDiff.setOldQualifiedName(toQualifiedName(entry.getOldPath())); + fileDiff.setNewQualifiedName(toQualifiedName(entry.getNewPath())); + + diffList.add(fileDiff); + }); + + // uncommitted changes + uncommitted.forEach(path -> { + FileDiff fileDiff = new FileDiff(); + + fileDiff.setChangeType(FileDiff.ChangeType.UNCOMMITTED); + fileDiff.setNewQualifiedName(toQualifiedName(path)); + + diffList.add(fileDiff); + }); + + // untracked changes + untracked.forEach(path -> { + FileDiff fileDiff = new FileDiff(); + + fileDiff.setChangeType(FileDiff.ChangeType.UNTRACKED); + fileDiff.setNewQualifiedName(toQualifiedName(path)); + + diffList.add(fileDiff); + }); + + return diffList; + } + + /** + * convert the relative path from git to package + */ + private String toQualifiedName(String relativePath) { + // /dev/null is git default path when a file is added or deleted + if ("/dev/null".equals(relativePath)) { + return relativePath; + } + + int index = relativePath.indexOf(this.codePath); + if (index < 0) { + return relativePath; + } + + String filePath = relativePath.substring(index + this.codePath.length() + 1); + if (StringUtil.isEmpty(filePath)) { + return relativePath; + } + + if (isSupportedSourceCodeType(filePath)) { + int lastIndex = filePath.lastIndexOf("."); + filePath = filePath.substring(0, lastIndex); + } + + return filePath.replace(File.separator, "."); + } + + private boolean isSupportedSourceCodeType(String path) { + // maybe there's a better way... + return path.endsWith(".java") + || path.endsWith(".kt") + || path.endsWith(".groovy") + || path.endsWith(".scala"); + } + + private void populateRelatedClazzAndMarkEntryPoint(Set diffList, Predicate isEntryPoint) { + List oldDependencyTree = this.dependencyTree.getDependencyTree(); + + if (CollectionUtil.isEmpty(oldDependencyTree)) { + return; + } + + // foreach the exist dependency tree, + // check whether it is entry point if clazzName is matched and get the related entry points + oldDependencyTree.forEach(dependency -> { + String clazz = dependency.getClazz(); + + Optional matchClazzOptional = diffList.stream() + .filter(item -> { + boolean equals = clazz.equals(item.getNewQualifiedName()); + if (equals) { + return true; + } + + List derivedClazz = dependency.getDerivedClazz(); + if (CollectionUtil.isEmpty(derivedClazz)) { + return false; + } + + return dependency.getDerivedClazz().contains(item.getNewQualifiedName()); + }) + .findFirst(); + if (matchClazzOptional.isPresent()) { + // mark the class is entry point(maybe now is not) + matchClazzOptional.get().setEntryPoint(true); + return; + } + + dependency.getApis().forEach(apiInfo -> { + boolean matchArgs = apiInfo.getArgs().stream() + .anyMatch( + item -> diffList.stream() + .anyMatch( + diff -> item.equals(diff.getNewQualifiedName()) + ) + ); + + boolean matchReturns = apiInfo.getReturns().stream() + .anyMatch( + item -> diffList.stream() + .anyMatch( + diff -> item.equals(diff.getNewQualifiedName()) + ) + ); + + if (matchArgs || matchReturns) { + FileDiff fileDiff = new FileDiff(); + + fileDiff.setChangeType(FileDiff.ChangeType.RELATED); + fileDiff.setNewQualifiedName(clazz); + fileDiff.setEntryPoint(true); + diffList.add(fileDiff); + } + }); + }); + + // check whether the others are entry point + diffList.stream() + .filter(item -> !item.isEntryPoint()) + .forEach(item -> { + boolean isEntry = isEntryPoint.test(item.getNewQualifiedName()); + item.setEntryPoint(isEntry); + }); + } + + public void rebuildDependencyTree(List apiList) { + List dependencyTree = buildDependencyTree(apiList); + writeDependencyTree(dependencyTree); + } + + private List buildDependencyTree(List apiList) { + if (CollectionUtil.isEmpty(apiList)) { + return Collections.emptyList(); + } + + List dependencyTree = new ArrayList<>(apiList.size()); + + for (T apiDoc : apiList) { + String docClass = apiDoc.getDocClass(); + List docMethods = apiDoc.getMethods(); + List apiInfoList = new ArrayList<>(docMethods.size()); + + // Get the derived classes which really used in api doc + List derivedClazz = docMethods.stream() + .map(IMethod::getDeclaringClass).filter(Objects::nonNull) + .map(JavaClass::getFullyQualifiedName) + .distinct().collect(Collectors.toList()); + + ApiDependency apiDependency = new ApiDependency(docClass, derivedClazz, apiInfoList); + dependencyTree.add(apiDependency); + + for (IMethod docMethod : docMethods) { + String methodName = docMethod.getMethodName(); + List argsClasses = docMethod.getArgsClasses(); + List returnClasses = docMethod.getReturnClasses(); + ApiDependency.ApiInfo apiInfo = new ApiDependency.ApiInfo(methodName, argsClasses, returnClasses); + + apiInfoList.add(apiInfo); + } + } + + return dependencyTree; + } + +} diff --git a/src/main/java/com/ly/doc/helper/GitHelper.java b/src/main/java/com/ly/doc/helper/GitHelper.java new file mode 100644 index 000000000..0ef806e3a --- /dev/null +++ b/src/main/java/com/ly/doc/helper/GitHelper.java @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2018-2023 smart-doc + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.ly.doc.helper; + +import com.power.common.util.StringUtil; +import org.eclipse.jgit.api.Git; +import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.diff.DiffEntry; +import org.eclipse.jgit.lib.ObjectId; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.Repository; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevWalk; +import org.eclipse.jgit.storage.file.FileRepositoryBuilder; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * A git util build on JGit + * + * @author Fio + */ +public class GitHelper { + + private Repository repository; + + private GitHelper() {} + + public static GitHelper create() { + GitHelper helper = new GitHelper(); + helper.repository = helper.findRepo(); + return helper; + } + + public List getDiff(String commitId) { + if (StringUtil.isEmpty(commitId) || notGitRepo()) { + return Collections.emptyList(); + } + + try (Git git = new Git(repository)) { + ObjectId commitObjectId = repository.resolve(commitId); + RevCommit commit = new RevWalk(repository).parseCommit(commitObjectId); + + ObjectId treeId = commit.getTree().getId(); + CanonicalTreeParser oldTreeIter = new CanonicalTreeParser(); + try (ObjectReader reader = repository.newObjectReader()) { + oldTreeIter.reset(reader, treeId); + } + + ObjectId currentTreeId = repository.resolve("HEAD^{tree}"); + CanonicalTreeParser newTreeIter = new CanonicalTreeParser(); + try (ObjectReader reader = repository.newObjectReader()) { + newTreeIter.reset(reader, currentTreeId); + } + + return git.diff() + .setNewTree(newTreeIter) + .setOldTree(oldTreeIter) + .call(); + } catch (IOException | GitAPIException e) { + throw new RuntimeException(e); + } + } + + public Set getUncommitted() { + if (notGitRepo()) { + return Collections.emptySet(); + } + + try (Git git = new Git(repository)) { + return git.status().call().getUncommittedChanges(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + + public Set getUntracked() { + if (notGitRepo()) { + return Collections.emptySet(); + } + + try (Git git = new Git(repository)) { + return git.status().call().getUntracked(); + } catch (GitAPIException e) { + throw new RuntimeException(e); + } + } + + public String getLatestCommitId() { + if (notGitRepo()) { + return ""; + } + + try { + ObjectId objectId = repository.resolve("HEAD"); + return objectId.getName(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * Check git repository, if not exist or io exception, return null + */ + private Repository findRepo() { + try { + return new FileRepositoryBuilder() + .readEnvironment() + .findGitDir() + .build(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public boolean notGitRepo() { + return repository == null; + } + + public String getWorkDir() { + return repository.getWorkTree().getAbsolutePath(); + } + +} diff --git a/src/main/java/com/ly/doc/model/ApiConfig.java b/src/main/java/com/ly/doc/model/ApiConfig.java index 53cefbc64..a6d2da89a 100644 --- a/src/main/java/com/ly/doc/model/ApiConfig.java +++ b/src/main/java/com/ly/doc/model/ApiConfig.java @@ -378,6 +378,16 @@ public static ApiConfig getInstance() { */ private String componentType; + /** + * whether to build the doc incrementally + */ + private boolean increment; + + /** + * the doc module absolute path, used for dependency tree file + */ + private String baseDir; + public String getComponentType() { return componentType; } @@ -995,4 +1005,20 @@ public ApiConfig setJarSourcePaths(List jarSourcePaths) { public void setJarSourcePaths(SourceCodePath... jarSourcePaths) { this.jarSourcePaths = CollectionUtil.asList(jarSourcePaths); } + + public boolean isIncrement() { + return increment; + } + + public void setIncrement(boolean increment) { + this.increment = increment; + } + + public String getBaseDir() { + return baseDir; + } + + public void setBaseDir(String baseDir) { + this.baseDir = baseDir; + } } diff --git a/src/main/java/com/ly/doc/model/ApiDoc.java b/src/main/java/com/ly/doc/model/ApiDoc.java index fe594d924..db39f7ac3 100644 --- a/src/main/java/com/ly/doc/model/ApiDoc.java +++ b/src/main/java/com/ly/doc/model/ApiDoc.java @@ -23,9 +23,10 @@ import java.util.*; +import com.power.common.util.CollectionUtil; import com.power.common.util.StringUtil; -public class ApiDoc implements Comparable { +public class ApiDoc implements IDoc, Comparable { /** * Order of controller @@ -245,4 +246,19 @@ public String toString() { sb.append('}'); return sb.toString(); } + + @Override + public String getDocClass() { + return this.packageName + "." + this.name; + } + + @Override + public List getMethods() { + if (CollectionUtil.isEmpty(this.list)) { + return Collections.emptyList(); + } + + return new ArrayList<>(this.list); + } + } diff --git a/src/main/java/com/ly/doc/model/ApiMethodDoc.java b/src/main/java/com/ly/doc/model/ApiMethodDoc.java index 7e50914ce..018af3e95 100644 --- a/src/main/java/com/ly/doc/model/ApiMethodDoc.java +++ b/src/main/java/com/ly/doc/model/ApiMethodDoc.java @@ -23,14 +23,16 @@ import java.io.Serializable; import java.util.*; +import com.ly.doc.utils.ParamUtil; import com.power.common.util.StringUtil; import com.ly.doc.constants.DocGlobalConstants; import com.ly.doc.model.request.ApiRequestExample; +import com.thoughtworks.qdox.model.JavaClass; /** * java api method info model. */ -public class ApiMethodDoc implements Serializable, Cloneable { +public class ApiMethodDoc implements IMethod, Serializable, Cloneable { private static final long serialVersionUID = 7211922919532562867L; @@ -549,4 +551,27 @@ public ApiMethodDoc clone() { throw new RuntimeException("clone apiMethodDoc is error", e); } } + + @Override + public JavaClass getDeclaringClass() { + return null; + } + + @Override + public String getMethodName() { + return this.name; + } + + @Override + public List getArgsClasses() { + ArrayList paramList = new ArrayList<>(this.pathParams); + paramList.addAll(this.requestParams); + paramList.addAll(this.queryParams); + return ParamUtil.extractQualifiedName(paramList); + } + + @Override + public List getReturnClasses() { + return ParamUtil.extractQualifiedName(this.responseParams); + } } diff --git a/src/main/java/com/ly/doc/model/IDoc.java b/src/main/java/com/ly/doc/model/IDoc.java new file mode 100644 index 000000000..173dfb17e --- /dev/null +++ b/src/main/java/com/ly/doc/model/IDoc.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018-2023 smart-doc + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.ly.doc.model; + +import java.util.List; + +/** + * @author Fio + */ +public interface IDoc { + + /** + * doc class qualified name + */ + String getDocClass(); + + /** + * + */ + List getMethods(); + +} diff --git a/src/main/java/com/ly/doc/model/IMethod.java b/src/main/java/com/ly/doc/model/IMethod.java new file mode 100644 index 000000000..7ee6c51ec --- /dev/null +++ b/src/main/java/com/ly/doc/model/IMethod.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2018-2023 smart-doc + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.ly.doc.model; + +import com.thoughtworks.qdox.model.JavaClass; + +import java.util.List; + +/** + * @author Fio + */ +public interface IMethod { + + JavaClass getDeclaringClass(); + + String getMethodName(); + + List getArgsClasses(); + + List getReturnClasses(); + +} diff --git a/src/main/java/com/ly/doc/model/RpcJavaMethod.java b/src/main/java/com/ly/doc/model/RpcJavaMethod.java index ff6c40117..f87f12e0f 100644 --- a/src/main/java/com/ly/doc/model/RpcJavaMethod.java +++ b/src/main/java/com/ly/doc/model/RpcJavaMethod.java @@ -20,6 +20,8 @@ */ package com.ly.doc.model; +import com.ly.doc.utils.ParamUtil; +import com.thoughtworks.qdox.model.JavaClass; import com.thoughtworks.qdox.model.JavaMethod; import com.thoughtworks.qdox.model.JavaType; @@ -30,7 +32,7 @@ * for rpc * @author yu 2020/1/29. */ -public class RpcJavaMethod { +public class RpcJavaMethod implements IMethod { /** * java method @@ -255,4 +257,24 @@ public RpcJavaMethod setActualTypesMap(Map actualTypesMap) { this.actualTypesMap = actualTypesMap; return this; } + + @Override + public JavaClass getDeclaringClass() { + return this.javaMethod.getDeclaringClass(); + } + + @Override + public String getMethodName() { + return this.name; + } + + @Override + public List getArgsClasses() { + return ParamUtil.extractQualifiedName(this.requestParams); + } + + @Override + public List getReturnClasses() { + return ParamUtil.extractQualifiedName(this.responseParams); + } } diff --git a/src/main/java/com/ly/doc/model/dependency/ApiDependency.java b/src/main/java/com/ly/doc/model/dependency/ApiDependency.java new file mode 100644 index 000000000..d00b62135 --- /dev/null +++ b/src/main/java/com/ly/doc/model/dependency/ApiDependency.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2018-2023 smart-doc + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.ly.doc.model.dependency; + +import java.util.List; +import java.util.Objects; +import java.util.List; + +/** + * @author Fio + */ +public class ApiDependency { + + /** + * The endpoint class's full qualified name + */ + private String clazz; + + /** + * Derived class full qualified name if the entry point class is interface + */ + private List derivedClazz; + + /** + * Api methods in the entry point class + */ + private List apis; + + public ApiDependency() { + } + + public ApiDependency(String clazz, List derivedClazz, List apis) { + this.clazz = clazz; + this.derivedClazz = derivedClazz; + this.apis = apis; + } + + /** + * Api method simple info + */ + public static class ApiInfo { + + /** + * Api method name + */ + private String method; + + /** + * Api method args + */ + private List args; + + /** + * Api method return,include the generics + */ + private List returns; + + public ApiInfo() { + } + + public ApiInfo(String method, List args, List returns) { + this.method = method; + this.args = args; + this.returns = returns; + } + + public String getMethod() { + return method; + } + + public void setMethod(String method) { + this.method = method; + } + + public List getArgs() { + return args; + } + + public void setArgs(List args) { + this.args = args; + } + + public List getReturns() { + return returns; + } + + public void setReturns(List returns) { + this.returns = returns; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ApiInfo)) return false; + ApiInfo apiInfo = (ApiInfo) o; + return Objects.equals(method, apiInfo.method) && Objects.equals(args, apiInfo.args) && Objects.equals(returns, apiInfo.returns); + } + + @Override + public int hashCode() { + return Objects.hash(method, args, returns); + } + } + + public String getClazz() { + return clazz; + } + + public void setClazz(String clazz) { + this.clazz = clazz; + } + + public List getDerivedClazz() { + return derivedClazz; + } + + public void setDerivedClazz(List derivedClazz) { + this.derivedClazz = derivedClazz; + } + + public List getApis() { + return apis; + } + + public void setApis(List apis) { + this.apis = apis; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ApiDependency)) return false; + ApiDependency that = (ApiDependency) o; + return Objects.equals(clazz, that.clazz) && Objects.equals(derivedClazz, that.derivedClazz) && Objects.equals(apis, that.apis); + } + + @Override + public int hashCode() { + return Objects.hash(clazz, derivedClazz, apis); + } +} diff --git a/src/main/java/com/ly/doc/model/dependency/DependencyTree.java b/src/main/java/com/ly/doc/model/dependency/DependencyTree.java new file mode 100644 index 000000000..1d2cdc140 --- /dev/null +++ b/src/main/java/com/ly/doc/model/dependency/DependencyTree.java @@ -0,0 +1,169 @@ +/* + * Copyright (C) 2018-2023 smart-doc + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.ly.doc.model.dependency; + +import com.ly.doc.utils.JsonUtil; +import com.power.common.util.FileUtil; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Fio + */ +public class DependencyTree { + + private static final String CONFIG_NAME = ".smart-doc-dependency.json"; + + /** + * dependency tree file + */ + private transient File configFile; + + /** + * The schema version for dependency tree format. + * Maybe you can rewrite the dependency-tree file when the format is incompatible. + */ + private String schema = "v1"; + + /** + * Git commit id when smart-doc build. + */ + private String commitId; + + /** + * Api methods dependency trees. + */ + private List dependencyTree; + + private DependencyTree() { + + } + + /** + * Create or Load dependency tree config. + * + * @param baseDir the dependency tree config file base directory + * @return DependencyTree + */ + public static DependencyTree detect(String baseDir) { + DependencyTree dependencyTree; + + File configFile = new File(baseDir + File.separator + CONFIG_NAME); + if (!configFile.exists()) { + dependencyTree = Support.create(configFile); + } else { + dependencyTree = Support.load(configFile); + } + + return dependencyTree; + } + + public static void write(DependencyTree dependencyTree) { + // do something when the dependency-tree format is changed dependent on the version + Support.writeFile(dependencyTree); + } + + public String getSchema() { + return schema; + } + + public File getConfigFile() { + return configFile; + } + + public String getCommitId() { + return commitId; + } + + public List getDependencyTree() { + return dependencyTree; + } + + public void setConfig(String commitId, List dependencyTree) { + this.commitId = commitId; + this.dependencyTree = dependencyTree; + } + + /** + * Provide some utility methods + */ + private static class Support { + + private static DependencyTree create(File configFile) { + if (!configFile.exists()) { + try { + Files.createFile(configFile.toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + DependencyTree instance = new DependencyTree(); + instance.configFile = configFile; + instance.setConfig("", Collections.emptyList()); + + writeFile(instance); + + return instance; + } + + private static DependencyTree load(File configFile) { + if (configFile == null || !configFile.exists()) { + return null; + } + + String content = readFile(configFile); + DependencyTree instance = JsonUtil.toObject(content, DependencyTree.class); + instance.configFile = configFile; + return instance; + } + + private static void writeFile(DependencyTree instance) { + List distinctDependency = instance.getDependencyTree() + .stream().distinct().collect(Collectors.toList()); + instance.setConfig(instance.getCommitId(), distinctDependency); + String content = JsonUtil.toPrettyJson(instance); + FileUtil.writeFileNotAppend(content, instance.configFile.getAbsolutePath()); + } + + private static String readFile(File configFile) { + try { + BufferedReader reader = new BufferedReader(new FileReader(configFile)); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + return builder.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + +} diff --git a/src/main/java/com/ly/doc/model/dependency/FileDiff.java b/src/main/java/com/ly/doc/model/dependency/FileDiff.java new file mode 100644 index 000000000..889fbefdc --- /dev/null +++ b/src/main/java/com/ly/doc/model/dependency/FileDiff.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2018-2023 smart-doc + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.ly.doc.model.dependency; + +import java.util.Objects; + +/** + * Mark the file change type + * + * @author Fio + */ +public class FileDiff { + + /** + * file change type + */ + private ChangeType changeType; + + /** + * old absolute path + */ + private String oldQualifiedName; + + /** + * new absolute path + */ + private String newQualifiedName; + + /** + * whether the class is entry point before build + */ + private boolean isEntryPoint = false; + + public enum ChangeType { + /** Add a new file to the project */ + ADD, + + /** Modify an existing file in the project (content and/or mode) */ + MODIFY, + + /** Delete an existing file from the project */ + DELETE, + + /** Rename an existing file to a new location */ + RENAME, + + /** Copy an existing file to a new location, keeping the original */ + COPY, + + /** + * File uncommitted, only with newPackagePath + */ + UNCOMMITTED, + + /** + * File untracked, only with newPackagePath + */ + UNTRACKED, + + /** + * The class related, only with newPackagePath + */ + RELATED; + + } + + public ChangeType getChangeType() { + return changeType; + } + + public void setChangeType(ChangeType changeType) { + this.changeType = changeType; + } + + public String getOldQualifiedName() { + return oldQualifiedName; + } + + public void setOldQualifiedName(String oldQualifiedName) { + this.oldQualifiedName = oldQualifiedName; + } + + public String getNewQualifiedName() { + return newQualifiedName; + } + + public void setNewQualifiedName(String newQualifiedName) { + this.newQualifiedName = newQualifiedName; + } + + public boolean isEntryPoint() { + return isEntryPoint; + } + + public void setEntryPoint(boolean entryPoint) { + isEntryPoint = entryPoint; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof FileDiff)) return false; + FileDiff fileDiff = (FileDiff) o; + return isEntryPoint == fileDiff.isEntryPoint && changeType == fileDiff.changeType && Objects.equals(oldQualifiedName, fileDiff.oldQualifiedName) && Objects.equals(newQualifiedName, fileDiff.newQualifiedName); + } + + @Override + public int hashCode() { + return Objects.hash(changeType, oldQualifiedName, newQualifiedName, isEntryPoint); + } +} diff --git a/src/main/java/com/ly/doc/model/rpc/RpcApiDoc.java b/src/main/java/com/ly/doc/model/rpc/RpcApiDoc.java index 56a2da6f5..0b151c2d3 100644 --- a/src/main/java/com/ly/doc/model/rpc/RpcApiDoc.java +++ b/src/main/java/com/ly/doc/model/rpc/RpcApiDoc.java @@ -20,15 +20,20 @@ */ package com.ly.doc.model.rpc; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; +import com.ly.doc.model.IDoc; +import com.ly.doc.model.IMethod; import com.ly.doc.model.RpcJavaMethod; +import com.power.common.util.CollectionUtil; /** * @author yu 2020/5/16. */ -public class RpcApiDoc implements Comparable { +public class RpcApiDoc implements IDoc, Comparable { /** * Order of controller @@ -197,4 +202,17 @@ public int compareTo(RpcApiDoc o) { } return name.compareTo(o.getName()); } + + @Override + public String getDocClass() { + return this.name; + } + + @Override + public List getMethods() { + if (CollectionUtil.isEmpty(this.list)) { + return Collections.emptyList(); + } + return new ArrayList<>(this.list); + } } diff --git a/src/main/java/com/ly/doc/template/IDocBuildTemplate.java b/src/main/java/com/ly/doc/template/IDocBuildTemplate.java index c4287a499..7c58a4e8b 100644 --- a/src/main/java/com/ly/doc/template/IDocBuildTemplate.java +++ b/src/main/java/com/ly/doc/template/IDocBuildTemplate.java @@ -20,34 +20,45 @@ */ package com.ly.doc.template; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; - -import com.ly.doc.model.ApiConfig; -import com.ly.doc.model.ApiDoc; -import com.ly.doc.model.ApiGroup; -import com.ly.doc.model.ApiMethodDoc; -import com.ly.doc.utils.DocUtil; -import com.power.common.util.CollectionUtil; -import com.power.common.util.StringUtil; import com.ly.doc.builder.ProjectDocConfigBuilder; import com.ly.doc.constants.TornaConstants; +import com.ly.doc.helper.DocBuildHelper; +import com.ly.doc.model.dependency.FileDiff; +import com.ly.doc.model.*; import com.ly.doc.model.annotation.FrameworkAnnotations; import com.ly.doc.utils.DocPathUtil; +import com.ly.doc.utils.DocUtil; +import com.power.common.util.CollectionUtil; +import com.power.common.util.StringUtil; +import com.thoughtworks.qdox.JavaProjectBuilder; +import com.thoughtworks.qdox.model.JavaClass; + +import java.util.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Predicate; +import java.util.stream.Collectors; /** * @author yu 2019/12/21. */ -public interface IDocBuildTemplate { +public interface IDocBuildTemplate { - List getApiData(ProjectDocConfigBuilder projectBuilder); + default List getApiData(ProjectDocConfigBuilder projectBuilder) { + DocBuildHelper docBuildHelper = DocBuildHelper.create(projectBuilder); - FrameworkAnnotations registeredAnnotations(); + preRender(docBuildHelper); + + Collection candidateClasses = getCandidateClasses(projectBuilder, docBuildHelper); + List apiList = renderApi(projectBuilder, candidateClasses); + + postRender(docBuildHelper, apiList); + + return apiList; + } + + List renderApi(ProjectDocConfigBuilder projectBuilder, Collection candidateClasses); + FrameworkAnnotations registeredAnnotations(); /** * handle group api docs @@ -93,8 +104,8 @@ default List handleApiGroup(List apiDocList, ApiConfig apiConfig continue; } List methodDocs = doc.getList().stream() - .filter(l -> DocPathUtil.matches(l.getPath(), group.getPaths(), null)) - .collect(Collectors.toList()); + .filter(l -> DocPathUtil.matches(l.getPath(), group.getPaths(), null)) + .collect(Collectors.toList()); doc.setList(methodDocs); } } @@ -113,4 +124,77 @@ default List handleApiGroup(List apiDocList, ApiConfig apiConfig finalApiDocs.forEach(group -> group.setOrder(order.getAndIncrement())); return finalApiDocs; } + + /** + * If build doc incrementally, we will filter the classes changed + * from the commit-id in increment-config-file. + * If not, we will return all classes. + * + * @param docBuilder docBuilder + * @param docBuildHelper incrementHelper + * @return the candidate classes + */ + default Collection getCandidateClasses(ProjectDocConfigBuilder docBuilder, DocBuildHelper docBuildHelper) { + ApiConfig apiConfig = docBuilder.getApiConfig(); + JavaProjectBuilder javaProjectBuilder = docBuilder.getJavaProjectBuilder(); + + if (!apiConfig.isIncrement()) { + return javaProjectBuilder.getClasses(); + } + + if (StringUtil.isEmpty(docBuildHelper.getDependencyTree().getCommitId())) { + // There is no commit-id, which means the user haven't built the whole project. + // We need to build the whole project this time, + // and record the latest commit-id and the newest api dependency tree. + return javaProjectBuilder.getClasses(); + } + + Set fileDiffList = docBuildHelper.getChangedFilesFromVCS(new Predicate() { + @Override + public boolean test(String s) { + return isEntryPoint(javaProjectBuilder, s); + } + }); + if (CollectionUtil.isEmpty(fileDiffList)) { + return Collections.emptyList(); + } + + Collection result = new ArrayList<>(fileDiffList.size()); + fileDiffList.forEach(item -> { + try { + JavaClass javaClass = javaProjectBuilder.getClassByName(item.getNewQualifiedName()); + result.add(javaClass); + } catch (Exception ignore) {} + }); + + return result; + } + + default void preRender(DocBuildHelper docBuildHelper) { + + } + + default void postRender(DocBuildHelper docBuildHelper, List apiList) { + docBuildHelper.rebuildDependencyTree(apiList); + } + + default boolean isEntryPoint(JavaProjectBuilder javaProjectBuilder, String javaClassName) { + if (StringUtil.isEmpty(javaClassName)) { + return false; + } + + JavaClass javaClass = null; + try { + javaClass = javaProjectBuilder.getClassByName(javaClassName); + } catch (Exception ignore) {} + + if (javaClass == null) { + return false; + } + + return isEntryPoint(javaClass, registeredAnnotations()); + } + + boolean isEntryPoint(JavaClass javaClass, FrameworkAnnotations frameworkAnnotations); + } diff --git a/src/main/java/com/ly/doc/template/IRestDocTemplate.java b/src/main/java/com/ly/doc/template/IRestDocTemplate.java index 8cd6f745d..f510a88cb 100644 --- a/src/main/java/com/ly/doc/template/IRestDocTemplate.java +++ b/src/main/java/com/ly/doc/template/IRestDocTemplate.java @@ -57,14 +57,14 @@ public interface IRestDocTemplate extends IBaseDocBuildTemplate { AtomicInteger atomicInteger = new AtomicInteger(1); default List processApiData(ProjectDocConfigBuilder projectBuilder, FrameworkAnnotations frameworkAnnotations, - List configApiReqParams, IRequestMappingHandler baseMappingHandler, IHeaderHandler headerHandler) { + List configApiReqParams, IRequestMappingHandler baseMappingHandler, IHeaderHandler headerHandler, + Collection javaClasses) { ApiConfig apiConfig = projectBuilder.getApiConfig(); List apiDocList = new ArrayList<>(); int order = 0; boolean setCustomOrder = false; - Collection classes = projectBuilder.getJavaProjectBuilder().getClasses(); // exclude class is ignore - for (JavaClass cls : classes) { + for (JavaClass cls : javaClasses) { if (StringUtil.isNotEmpty(apiConfig.getPackageFilters())) { // from smart config if (!DocUtil.isMatch(apiConfig.getPackageFilters(), cls)) { @@ -73,7 +73,7 @@ default List processApiData(ProjectDocConfigBuilder projectBuilder, Fram } // from tag DocletTag ignoreTag = cls.getTagByName(DocTags.IGNORE); - if (!defaultEntryPoint(cls, frameworkAnnotations) || Objects.nonNull(ignoreTag)) { + if (!isEntryPoint(cls, frameworkAnnotations) || Objects.nonNull(ignoreTag)) { continue; } String strOrder = JavaClassUtil.getClassTagsValue(cls, DocTags.ORDER, Boolean.TRUE); @@ -1068,16 +1068,11 @@ default boolean defaultEntryPoint(JavaClass cls, FrameworkAnnotations frameworkA } List classAnnotations = DocClassUtil.getAnnotations(cls); Map entryAnnotationMap = frameworkAnnotations.getEntryAnnotations(); - for (JavaAnnotation annotation : classAnnotations) { + + return classAnnotations.stream().anyMatch(annotation -> { String name = annotation.getType().getValue(); - if (entryAnnotationMap.containsKey(name)) { - return true; - } - if (isEntryPoint(cls, frameworkAnnotations)) { - return true; - } - } - return false; + return entryAnnotationMap.containsKey(name); + }); } default List getParentsClassMethods(ApiConfig apiConfig, ProjectDocConfigBuilder projectBuilder, JavaClass cls) { diff --git a/src/main/java/com/ly/doc/template/JaxrsDocBuildTemplate.java b/src/main/java/com/ly/doc/template/JaxrsDocBuildTemplate.java index 92302b8a3..1ddcf3f79 100644 --- a/src/main/java/com/ly/doc/template/JaxrsDocBuildTemplate.java +++ b/src/main/java/com/ly/doc/template/JaxrsDocBuildTemplate.java @@ -68,16 +68,15 @@ public class JaxrsDocBuildTemplate implements IDocBuildTemplate, IRestDo @Override - public List getApiData(ProjectDocConfigBuilder projectBuilder) { + public List renderApi(ProjectDocConfigBuilder projectBuilder, Collection candidateClasses) { ApiConfig apiConfig = projectBuilder.getApiConfig(); FrameworkAnnotations frameworkAnnotations = registeredAnnotations(); this.headers = apiConfig.getRequestHeaders(); List apiDocList = new ArrayList<>(); int order = 0; - Collection classes = projectBuilder.getJavaProjectBuilder().getClasses(); boolean setCustomOrder = false; // exclude class is ignore - for (JavaClass cls : classes) { + for (JavaClass cls : candidateClasses) { if (StringUtil.isNotEmpty(apiConfig.getPackageFilters())) { // from smart config if (!DocUtil.isMatch(apiConfig.getPackageFilters(), cls)) { @@ -283,6 +282,11 @@ public FrameworkAnnotations registeredAnnotations() { @Override public boolean isEntryPoint(JavaClass cls, FrameworkAnnotations frameworkAnnotations) { + boolean isDefaultEntryPoint = defaultEntryPoint(cls, frameworkAnnotations); + if (isDefaultEntryPoint) { + return true; + } + if (cls.isAnnotation() || cls.isEnum()) { return false; } diff --git a/src/main/java/com/ly/doc/template/RpcDocBuildTemplate.java b/src/main/java/com/ly/doc/template/RpcDocBuildTemplate.java index 4a50c1868..b49a8ce13 100644 --- a/src/main/java/com/ly/doc/template/RpcDocBuildTemplate.java +++ b/src/main/java/com/ly/doc/template/RpcDocBuildTemplate.java @@ -53,12 +53,12 @@ public class RpcDocBuildTemplate implements IDocBuildTemplate, IRpcDo private final AtomicInteger atomicInteger = new AtomicInteger(1); @Override - public List getApiData(ProjectDocConfigBuilder projectBuilder) { + public List renderApi(ProjectDocConfigBuilder projectBuilder, Collection candidateClasses) { ApiConfig apiConfig = projectBuilder.getApiConfig(); List apiDocList = new ArrayList<>(); int order = 0; boolean setCustomOrder = false; - for (JavaClass cls : projectBuilder.getJavaProjectBuilder().getClasses()) { + for (JavaClass cls : candidateClasses) { if (StringUtil.isNotEmpty(apiConfig.getPackageFilters())) { // check package if (!DocUtil.isMatch(apiConfig.getPackageFilters(), cls)) { diff --git a/src/main/java/com/ly/doc/template/SolonDocBuildTemplate.java b/src/main/java/com/ly/doc/template/SolonDocBuildTemplate.java index 64754a8d8..9d220e86d 100644 --- a/src/main/java/com/ly/doc/template/SolonDocBuildTemplate.java +++ b/src/main/java/com/ly/doc/template/SolonDocBuildTemplate.java @@ -61,13 +61,13 @@ public class SolonDocBuildTemplate implements IDocBuildTemplate, IRestDocTemplate { @Override - public List getApiData(ProjectDocConfigBuilder projectBuilder) { + public List renderApi(ProjectDocConfigBuilder projectBuilder, Collection candidateClasses) { ApiConfig apiConfig = projectBuilder.getApiConfig(); List configApiReqParams = Stream.of(apiConfig.getRequestHeaders(), apiConfig.getRequestParams()).filter(Objects::nonNull) .flatMap(Collection::stream).collect(Collectors.toList()); FrameworkAnnotations frameworkAnnotations = registeredAnnotations(); List apiDocList = processApiData(projectBuilder, frameworkAnnotations, configApiReqParams, - new SolonRequestMappingHandler(), new SolonRequestHeaderHandler()); + new SolonRequestMappingHandler(), new SolonRequestHeaderHandler(), candidateClasses); // sort if (apiConfig.isSortByTitle()) { Collections.sort(apiDocList); @@ -83,6 +83,11 @@ public boolean ignoreReturnObject(String typeName, List ignoreParams) { @Override public boolean isEntryPoint(JavaClass cls, FrameworkAnnotations frameworkAnnotations) { + boolean isDefaultEntryPoint = defaultEntryPoint(cls, frameworkAnnotations); + if (isDefaultEntryPoint) { + return true; + } + for (JavaAnnotation annotation : cls.getAnnotations()) { String name = annotation.getType().getValue(); if (SolonAnnotations.REMOTING.equals(name)) { diff --git a/src/main/java/com/ly/doc/template/SpringBootDocBuildTemplate.java b/src/main/java/com/ly/doc/template/SpringBootDocBuildTemplate.java index 294d83a64..9a48ab52b 100644 --- a/src/main/java/com/ly/doc/template/SpringBootDocBuildTemplate.java +++ b/src/main/java/com/ly/doc/template/SpringBootDocBuildTemplate.java @@ -44,13 +44,13 @@ public class SpringBootDocBuildTemplate implements IDocBuildTemplate, IRestDocTemplate { @Override - public List getApiData(ProjectDocConfigBuilder projectBuilder) { + public List renderApi(ProjectDocConfigBuilder projectBuilder, Collection candidateClasses) { ApiConfig apiConfig = projectBuilder.getApiConfig(); List configApiReqParams = Stream.of(apiConfig.getRequestHeaders(), apiConfig.getRequestParams()).filter(Objects::nonNull) .flatMap(Collection::stream).collect(Collectors.toList()); FrameworkAnnotations frameworkAnnotations = registeredAnnotations(); List apiDocList = this.processApiData(projectBuilder, frameworkAnnotations, - configApiReqParams, new SpringMVCRequestMappingHandler(), new SpringMVCRequestHeaderHandler()); + configApiReqParams, new SpringMVCRequestMappingHandler(), new SpringMVCRequestHeaderHandler(), candidateClasses); // sort if (apiConfig.isSortByTitle()) { Collections.sort(apiDocList); @@ -182,6 +182,11 @@ public FrameworkAnnotations registeredAnnotations() { @Override public boolean isEntryPoint(JavaClass javaClass, FrameworkAnnotations frameworkAnnotations) { + boolean isDefaultEntryPoint = defaultEntryPoint(javaClass, frameworkAnnotations); + if (isDefaultEntryPoint) { + return true; + } + if (javaClass.isAnnotation() || javaClass.isEnum()) { return false; } diff --git a/src/main/java/com/ly/doc/utils/DocClassUtil.java b/src/main/java/com/ly/doc/utils/DocClassUtil.java index 33ca6b215..3f2de971d 100644 --- a/src/main/java/com/ly/doc/utils/DocClassUtil.java +++ b/src/main/java/com/ly/doc/utils/DocClassUtil.java @@ -23,8 +23,6 @@ import java.lang.reflect.InvocationTargetException; import java.util.*; -import javax.annotation.Nonnull; - import com.ly.doc.model.ApiReturn; import com.power.common.util.StringUtil; import com.ly.doc.filter.ReturnTypeProcessor; @@ -356,7 +354,7 @@ public static List getAnnotations(JavaClass cls) { return classAnnotations; } - public static T newInstance(@Nonnull Class classWithNoArgsConstructor) { + public static T newInstance(Class classWithNoArgsConstructor) { try { return classWithNoArgsConstructor.getConstructor().newInstance(); } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | diff --git a/src/main/java/com/ly/doc/utils/JsonUtil.java b/src/main/java/com/ly/doc/utils/JsonUtil.java index 8a1700955..9bd3d179a 100644 --- a/src/main/java/com/ly/doc/utils/JsonUtil.java +++ b/src/main/java/com/ly/doc/utils/JsonUtil.java @@ -58,4 +58,8 @@ public static String toPrettyJson(Object src) { Gson gson = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create(); return gson.toJson(src); } + + public static T toObject(String json, Class clazz) { + return new Gson().fromJson(json, clazz); + } } diff --git a/src/main/java/com/ly/doc/utils/ParamUtil.java b/src/main/java/com/ly/doc/utils/ParamUtil.java index 43f6283ee..6230adde3 100644 --- a/src/main/java/com/ly/doc/utils/ParamUtil.java +++ b/src/main/java/com/ly/doc/utils/ParamUtil.java @@ -1,12 +1,12 @@ package com.ly.doc.utils; -import java.util.Map; -import java.util.Objects; +import java.util.*; import com.ly.doc.builder.ProjectDocConfigBuilder; import com.ly.doc.constants.DocGlobalConstants; import com.ly.doc.constants.DocTags; import com.ly.doc.model.ApiParam; +import com.power.common.util.CollectionUtil; import com.power.common.util.StringUtil; import com.thoughtworks.qdox.model.JavaClass; import com.thoughtworks.qdox.model.JavaField; @@ -41,4 +41,28 @@ public static String formatMockValue(String mock) { } return mock.replaceAll("\\\\",""); } + + public static List extractQualifiedName(List paramList) { + if (CollectionUtil.isEmpty(paramList)) { + return Collections.emptyList(); + } + + Set set = new HashSet<>(); + for (ApiParam param : paramList) { + String className = param.getClassName(); + + if (StringUtil.isEmpty(className)) { + continue; + } + + int index = className.indexOf("<"); + if (index > -1) { + className = className.substring(0, index); + } + + set.add(className); + } + + return new ArrayList<>(set); + } } diff --git a/src/test/java/com/ly/doc/util/DocClassUtilTest.java b/src/test/java/com/ly/doc/util/DocClassUtilTest.java index d4ef7e210..26cd00658 100644 --- a/src/test/java/com/ly/doc/util/DocClassUtilTest.java +++ b/src/test/java/com/ly/doc/util/DocClassUtilTest.java @@ -16,7 +16,7 @@ public class DocClassUtilTest { @Test public void testGetSimpleGicName() { char me = 'k'; - String className = "com.power.doc.controller.Teacher,com.power.doc.controller.Teacher,com.power.doc.controller.Teacher>"; + String className = "com.ly.doc.controller.Teacher,com.ly.doc.controller.Teacher,com.ly.doc.controller.Teacher>"; String[] arr = DocClassUtil.getSimpleGicName(className); // System.out.println("arr:"+ JSON.toJSONString(arr)); } diff --git a/src/test/java/com/ly/doc/util/ParamUtilTest.java b/src/test/java/com/ly/doc/util/ParamUtilTest.java index 4ce791814..e69e1c39c 100644 --- a/src/test/java/com/ly/doc/util/ParamUtilTest.java +++ b/src/test/java/com/ly/doc/util/ParamUtilTest.java @@ -1,12 +1,96 @@ package com.ly.doc.util; +import com.google.gson.Gson; +import com.google.gson.reflect.TypeToken; +import com.ly.doc.model.ApiParam; import com.ly.doc.utils.ParamUtil; import org.junit.jupiter.api.Test; +import java.util.List; + public class ParamUtilTest { @Test public void testFormatMockValue() { System.out.printf(ParamUtil.formatMockValue("*\\/5 * * *")); } + + @Test + public void testExtractQualifiedName() { + String paramJson = "[\n" + + " {\n" + + " \"className\": \"org.example.springboot.dto.PetDTO\",\n" + + " \"id\": 1,\n" + + " \"field\": \"name\",\n" + + " \"type\": \"string\",\n" + + " \"desc\": \"name\",\n" + + " \"required\": false,\n" + + " \"version\": \"-\",\n" + + " \"pid\": 0,\n" + + " \"pathParam\": false,\n" + + " \"queryParam\": false,\n" + + " \"value\": \"\",\n" + + " \"hasItems\": false,\n" + + " \"maxLength\": \"\",\n" + + " \"configParam\": false,\n" + + " \"selfReferenceLoop\": false\n" + + " },\n" + + " {\n" + + " \"className\": \"org.example.springboot.dto.PetDTO\",\n" + + " \"id\": 2,\n" + + " \"field\": \"age\",\n" + + " \"type\": \"int32\",\n" + + " \"desc\": \"age\",\n" + + " \"required\": false,\n" + + " \"version\": \"-\",\n" + + " \"pid\": 0,\n" + + " \"pathParam\": false,\n" + + " \"queryParam\": false,\n" + + " \"value\": \"0\",\n" + + " \"hasItems\": false,\n" + + " \"maxLength\": \"\",\n" + + " \"configParam\": false,\n" + + " \"selfReferenceLoop\": false\n" + + " },\n" + + " {\n" + + " \"className\": \"org.example.springboot.dto.PetDTO\",\n" + + " \"id\": 3,\n" + + " \"field\": \"master\",\n" + + " \"type\": \"object\",\n" + + " \"desc\": \"master\",\n" + + " \"required\": false,\n" + + " \"version\": \"-\",\n" + + " \"pid\": 0,\n" + + " \"pathParam\": false,\n" + + " \"queryParam\": false,\n" + + " \"value\": \"\",\n" + + " \"hasItems\": false,\n" + + " \"maxLength\": \"\",\n" + + " \"configParam\": false,\n" + + " \"selfReferenceLoop\": false\n" + + " },\n" + + " {\n" + + " \"className\": \"org.example.springboot.dto.UserDTO\",\n" + + " \"id\": 4,\n" + + " \"field\": \"└─username\",\n" + + " \"type\": \"string\",\n" + + " \"desc\": \"No comments found.\",\n" + + " \"required\": false,\n" + + " \"version\": \"-\",\n" + + " \"pid\": 3,\n" + + " \"pathParam\": false,\n" + + " \"queryParam\": false,\n" + + " \"value\": \"\",\n" + + " \"hasItems\": false,\n" + + " \"maxLength\": \"\",\n" + + " \"configParam\": false,\n" + + " \"selfReferenceLoop\": false\n" + + " }\n" + + "]"; + List paramList = new Gson().fromJson(paramJson, new TypeToken>() { + }); + List qualifiedList = ParamUtil.extractQualifiedName(paramList); + qualifiedList.forEach(System.out::println); + } + }