com.ly.doc.helper.DocBuildHelper Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of smart-doc Show documentation
Show all versions of smart-doc Show documentation
Smart-doc is a tool that supports both JAVA RESTFUL API and Apache Dubbo RPC interface document
generation.
/*
* 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;
if (helper.gitHelper.isGitRepo()) {
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;
}
public boolean notGitRepo() {
return gitHelper.notGitRepo();
}
}