Last active
February 20, 2021 23:48
-
-
Save naosim/68b555db03ecfcc1eb7f74867ea98fdd to your computer and use it in GitHub Desktop.
Springフレームワークのエンドポイントを取得する 開発ドキュメント作成用ツール
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import org.assertj.core.api.exception.RuntimeIOException; | |
import org.springframework.beans.factory.config.BeanDefinition; | |
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; | |
import org.springframework.core.type.filter.RegexPatternTypeFilter; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RestController; | |
import java.io.File; | |
import java.io.IOException; | |
import java.lang.reflect.Method; | |
import java.nio.file.Files; | |
import java.util.*; | |
import java.util.concurrent.Callable; | |
import java.util.regex.Pattern; | |
import java.util.stream.Collectors; | |
import java.util.stream.Stream; | |
/** | |
* Springフレームワークのエンドポイントを取得する | |
* 開発ドキュメント作成用ツール | |
* | |
* usage: | |
* ``` | |
* List<ApiEndPointAnalyzer.ApiEndPoint> endPointList = ApiEndPointAnalyzer.findAllEndPoint(new ApiEndPointAnalyzer.Config( | |
* new File("."), | |
* "your.root.package" | |
* )); | |
* ``` | |
*/ | |
public class ApiEndPointAnalyzer { | |
public static List<ApiEndPoint> findAllEndPoint(Config config) { | |
SourceCode.Repository sourceCodeRepository = new SourceCode.Repository(config.rootDir); | |
sourceCodeRepository.init(); | |
return new Service( | |
sourceCodeRepository, | |
new Clazz.Repository(config.rootPackage) | |
).findAllEndPoint(); | |
} | |
static class Service { | |
private final SourceCode.Repository sourceCodeRepository; | |
private final Clazz.Repository classRepository; | |
public Service( | |
SourceCode.Repository sourceCodeRepository, | |
Clazz.Repository classRepository | |
) { | |
this.sourceCodeRepository = sourceCodeRepository; | |
this.classRepository = classRepository; | |
} | |
public List<ApiEndPoint> findAllEndPoint() { | |
return classRepository.findAllEndPoint().stream().map(e -> new ApiEndPoint( | |
e.getEndPointPath(), | |
e.getApiPackageName(), | |
sourceCodeRepository.findCommentFirstLine(e.getApiPackageName()) | |
)).collect(Collectors.toList()); | |
} | |
} | |
public static class Config { | |
final File rootDir; | |
final String rootPackage; | |
public Config(File rootDir, String rootPackage) { | |
this.rootDir = rootDir; | |
this.rootPackage = rootPackage; | |
} | |
} | |
static class ApiEndPoint { | |
private final String endPointPath; | |
private final PackageName packageName; | |
private final Optional<CommentFirstLine> commentFirstLine; | |
public ApiEndPoint(String endPointPath, PackageName packageName, Optional<CommentFirstLine> commentFirstLine) { | |
this.endPointPath = endPointPath; | |
this.packageName = packageName; | |
this.commentFirstLine = commentFirstLine; | |
} | |
public String getEndPointPath() { | |
return endPointPath; | |
} | |
public PackageName getPackageName() { | |
return packageName; | |
} | |
public Optional<CommentFirstLine> getCommentFirstLine() { | |
return commentFirstLine; | |
} | |
} | |
static class CommentFirstLine { | |
private final String value; | |
public CommentFirstLine(String value) { | |
this.value = value; | |
} | |
public String getValue() { | |
return value; | |
} | |
} | |
static class PackageName { | |
private final String value; | |
public PackageName(String value) { | |
this.value = value; | |
} | |
public String getValue() { | |
return value; | |
} | |
@Override | |
public boolean equals(Object o) { | |
if (this == o) return true; | |
if (o == null || getClass() != o.getClass()) return false; | |
PackageName that = (PackageName) o; | |
return Objects.equals(value, that.value); | |
} | |
@Override | |
public int hashCode() { | |
return Objects.hash(value); | |
} | |
} | |
static class SourceCode { | |
static class ClassCodeFile { | |
private final PackageName packageName; | |
private final Optional<CommentFirstLine> commentFirstLine; | |
public ClassCodeFile(PackageName packageName, Optional<CommentFirstLine> commentFirstLine) { | |
this.packageName = packageName; | |
this.commentFirstLine = commentFirstLine; | |
} | |
} | |
static class ClassCodeFileFactory { | |
private final String text; | |
public ClassCodeFileFactory(String text) { | |
this.text = text; | |
} | |
public Optional<ClassCodeFile> create() throws IOException { | |
if(!isClass()) { | |
return Optional.empty(); | |
} | |
return Optional.of(new ClassCodeFile( | |
getPackageName(), | |
getCommentFirstLine() | |
)); | |
//String text = Files.readAllLines(file.toPath()).stream().collect(Collectors.joining("\n")); | |
} | |
public boolean isClass() { | |
if(!this.text.contains("class ")) { | |
return false; | |
} | |
if(this.text.contains("interface ")) { | |
if(this.text.indexOf("class ") > this.text.indexOf("interface ")) { | |
return false; | |
} | |
} | |
return true; | |
} | |
public String getClassName() { | |
String afterClassText = text.split("class ")[1]; | |
return afterClassText.substring(0, Math.min(afterClassText.indexOf(" "), afterClassText.indexOf("{"))).trim(); | |
} | |
public PackageName getPackageName() { | |
return new PackageName(text.split("package ")[1].split(";")[0].trim() + "." + getClassName()); | |
} | |
public Optional<CommentFirstLine> getCommentFirstLine() { | |
String beforeClassText = this.text.split("class ")[0]; | |
if(!beforeClassText.contains("/**")) { | |
return Optional.empty(); | |
} | |
String firstLine = beforeClassText.substring(beforeClassText.indexOf("/**")).split("\n")[1]; | |
if(firstLine.indexOf("*") == -1) { | |
throw new RuntimeException(firstLine); | |
} | |
return Optional.of(new CommentFirstLine(firstLine.substring(firstLine.indexOf("*") + 1).trim())); | |
} | |
} | |
static class Repository { | |
private final File rootDir; | |
private final Map<PackageName, ClassCodeFile> map = new HashMap<>(); | |
public Repository(File rootDir) { | |
if(rootDir.isFile()) { | |
throw new RuntimeException("rootDirがファイルです。ディレクトリを指定してください。"); | |
} | |
this.rootDir = rootDir; | |
} | |
public Optional<ClassCodeFile> find(PackageName packageName) { | |
return Optional.ofNullable(map.get(packageName)); | |
} | |
public Optional<CommentFirstLine> findCommentFirstLine(PackageName packageName) { | |
return this.find(packageName).flatMap(v -> v.commentFirstLine); | |
} | |
public void init() { | |
// mapを作る | |
findAllJavaFile().stream().forEach(it -> { | |
try { | |
if(!it.getName().contains("Api")) { | |
return; | |
} | |
String text = Files.readAllLines(it.toPath()).stream().collect(Collectors.joining("\n")); | |
new ClassCodeFileFactory(text).create().ifPresent(v -> map.put(v.packageName, v)); | |
} catch (IOException e) { | |
throw new RuntimeIOException(e.getMessage()); | |
} | |
}); | |
} | |
List<File> findAllJavaFile() { | |
return findAllFile(this.rootDir).stream().filter(it -> { | |
if(!it.getName().contains(("."))) { | |
return false; | |
} | |
return it.getName().substring(it.getName().lastIndexOf(".")).equals(".java"); | |
}).collect(Collectors.toList()); | |
} | |
List<File> findAllFile(File dir) { | |
if(dir.isFile()) { | |
throw new RuntimeException("rootDirがファイルです。ディレクトリを指定してください。"); | |
} | |
List<File> result = new ArrayList<>(); | |
Stream.of(dir.listFiles()).forEach(it -> { | |
if(it.isFile()) { | |
result.add(it); | |
} else if (it.isDirectory()) { | |
result.addAll(this.findAllFile(it)); | |
} | |
}); | |
return result; | |
} | |
} | |
} | |
static class Clazz { | |
static class Repository { | |
private final String rootPackage; | |
public Repository(String rootPackage) { | |
this.rootPackage = rootPackage; | |
} | |
public List<EndPointInClass> findAllEndPoint() { | |
// セットアップ | |
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); | |
provider.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(".*"))); | |
// APIクラスの取得 | |
Set<ApiClass> apis = provider.findCandidateComponents(rootPackage).stream() | |
.map(ApiClass::create) | |
.filter(Optional::isPresent).map(Optional::get) | |
.collect(Collectors.toSet()); | |
// エンドポイント情報の取得 | |
List<EndPointInClass> result = new ArrayList<>(); | |
apis.forEach(api -> api.getEndPoints().forEach(endPoint -> result.add(endPoint))); | |
return result; | |
} | |
} | |
private static class ApiClass { | |
private final Class value; | |
private ApiClass(Class value) { | |
this.value = value; | |
} | |
public String getName() { | |
return this.value.getName(); | |
} | |
public List<EndPointInClass> getEndPoints() { | |
Method[] methods = this.value.getMethods(); | |
if(methods == null || methods.length == 0) { | |
throw new RuntimeException("RequestMappingがないパタンは非対応:" + this.getName()); | |
} | |
return Arrays.stream(methods) | |
.map(v -> Optional.ofNullable(v.getAnnotation(RequestMapping.class)).map(a -> new EndPointInClass(this.value, a))) | |
.filter(v -> v.isPresent()).map(v -> v.get()) | |
.collect(Collectors.toList()); | |
} | |
public static Optional<ApiClass> create(BeanDefinition bean) { | |
Class clazz = uncheckCall(() -> Class.forName(bean.getBeanClassName())); | |
// @RestControllerがあるか? | |
return Optional | |
.ofNullable(clazz.getAnnotation(RestController.class)) | |
.map(v -> new ApiClass(clazz)); | |
} | |
} | |
static class EndPointInClass { | |
private final Class clazz; | |
private final RequestMapping requestMapping; | |
public EndPointInClass(Class clazz, RequestMapping requestMapping) { | |
this.clazz = clazz; | |
this.requestMapping = requestMapping; | |
} | |
public PackageName getApiPackageName() { | |
return new PackageName(this.clazz.getName()); | |
} | |
public String getEndPointPath() { | |
if(this.requestMapping.value().length >= 2) { | |
throw new RuntimeException("エンドポイントのパスが2つあるパタンは非対応:" + this.getApiPackageName()); | |
} | |
return this.requestMapping.value()[0]; | |
} | |
} | |
private static <T> T uncheckCall(Callable<T> callable) { | |
try { | |
return callable.call(); | |
} catch (Exception e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment