Skip to content

Commit 03b9c27

Browse files
authored
Merge pull request #683 from internetarchive/ato/beandoc
docs: replace javalang with an annotation processor
2 parents ab589e6 + 89e5ecc commit 03b9c27

File tree

9 files changed

+438
-183
lines changed

9 files changed

+438
-183
lines changed

.readthedocs.yml

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
11
version: 2
22
build:
3-
os: ubuntu-22.04
3+
os: ubuntu-24.04
44
tools:
5-
python: "3.12"
5+
python: "3.13"
6+
apt_packages:
7+
- default-jdk
8+
- maven
9+
jobs:
10+
pre_build:
11+
- mvn compile
612
sphinx:
713
configuration: docs/conf.py
814
python:
915
install:
10-
- requirements: docs/requirements.txt
16+
- requirements: docs/requirements.txt

docgen/pom.xml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?xml version="1.0"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
3+
<parent>
4+
<groupId>org.archive</groupId>
5+
<artifactId>heritrix</artifactId>
6+
<version>3.11.1-SNAPSHOT</version>
7+
</parent>
8+
<modelVersion>4.0.0</modelVersion>
9+
<groupId>org.archive.heritrix</groupId>
10+
<artifactId>heritrix-docgen</artifactId>
11+
<packaging>jar</packaging>
12+
<name>Heritrix 3: 'docgen' subproject</name>
13+
<dependencies>
14+
<dependency>
15+
<groupId>com.fasterxml.jackson.core</groupId>
16+
<artifactId>jackson-databind</artifactId>
17+
<version>2.20.0</version>
18+
</dependency>
19+
</dependencies>
20+
<build>
21+
<plugins>
22+
<plugin>
23+
<groupId>org.apache.maven.plugins</groupId>
24+
<artifactId>maven-compiler-plugin</artifactId>
25+
<configuration>
26+
<!-- prevent attempting to run docgen on itself -->
27+
<proc>none</proc>
28+
<annotationProcessorPaths combine.self="override"/>
29+
</configuration>
30+
</plugin>
31+
</plugins>
32+
</build>
33+
</project>
Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
/*
2+
* This file is part of the Heritrix web crawler (crawler.archive.org).
3+
*
4+
* Licensed to the Internet Archive (IA) by one or more individual
5+
* contributors.
6+
*
7+
* The IA licenses this file to You under the Apache License, Version 2.0
8+
* (the "License"); you may not use this file except in compliance with
9+
* the License. You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
*/
19+
20+
package org.archive.crawler;
21+
22+
import com.fasterxml.jackson.annotation.JsonProperty;
23+
import com.fasterxml.jackson.databind.ObjectMapper;
24+
import com.sun.source.tree.*;
25+
import com.sun.source.util.Trees;
26+
27+
import javax.annotation.processing.*;
28+
import javax.lang.model.SourceVersion;
29+
import javax.lang.model.element.Element;
30+
import javax.lang.model.element.ExecutableElement;
31+
import javax.lang.model.element.TypeElement;
32+
import javax.lang.model.element.VariableElement;
33+
import javax.lang.model.type.DeclaredType;
34+
import javax.lang.model.type.TypeKind;
35+
import javax.lang.model.type.TypeMirror;
36+
import javax.lang.model.util.ElementFilter;
37+
import javax.tools.StandardLocation;
38+
import java.beans.Introspector;
39+
import java.io.IOException;
40+
import java.io.Writer;
41+
import java.util.HashMap;
42+
import java.util.LinkedHashMap;
43+
import java.util.Map;
44+
import java.util.Set;
45+
import java.util.regex.Pattern;
46+
47+
@SupportedSourceVersion(SourceVersion.RELEASE_17)
48+
@SupportedAnnotationTypes("*")
49+
public class BeanDocProcessor extends AbstractProcessor {
50+
private Trees trees;
51+
private Map<String, Bean> classes = new LinkedHashMap<>();
52+
private final Pattern DOC_CLEANUP = Pattern.compile("^\\s*@(?:author|version|see).*", Pattern.MULTILINE);
53+
54+
@Override
55+
public synchronized void init(ProcessingEnvironment processingEnv) {
56+
super.init(processingEnv);
57+
this.trees = Trees.instance(processingEnv);
58+
}
59+
60+
private String getDocComment(Element element) {
61+
var path = trees.getPath(element);
62+
if (path == null) return null; // no source available
63+
String docComment = trees.getDocComment(path);
64+
if (docComment == null) return null;
65+
docComment = docComment.replace("\n ", "\n"); // remove indent
66+
return DOC_CLEANUP.matcher(docComment).replaceAll("").trim();
67+
}
68+
69+
private class FieldInfo {
70+
private String docComment;
71+
private Object initializer;
72+
73+
public FieldInfo(VariableElement field) {
74+
VariableTree vt = (VariableTree) trees.getTree(field);
75+
if (vt != null) {
76+
ExpressionTree init = vt.getInitializer();
77+
if (init instanceof LiteralTree lit) {
78+
initializer = lit.getValue();
79+
}
80+
}
81+
82+
docComment = getDocComment(field);
83+
}
84+
}
85+
86+
public class Bean {
87+
public final String superclass;
88+
public final String description;
89+
public final HashMap<String, Property> properties;
90+
91+
Bean(TypeElement type) {
92+
superclass = getSuperclass(type);
93+
description = getDocComment(type);
94+
properties = getProperties(type);
95+
}
96+
}
97+
98+
public class Property {
99+
@JsonProperty("default")
100+
public Object defaultValue;
101+
public String description;
102+
public String type;
103+
}
104+
105+
@Override
106+
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment round) {
107+
for (var root : round.getRootElements()) {
108+
if (root.getKind().isClass()) {
109+
processClass((TypeElement) root);
110+
}
111+
}
112+
113+
if (round.processingOver()) {
114+
writeJsonOutput();
115+
}
116+
117+
return false;
118+
}
119+
120+
private void writeJsonOutput() {
121+
var objectMapper = new ObjectMapper();
122+
objectMapper.setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL);
123+
try (Writer writer = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "",
124+
"META-INF/heritrix-beans.json").openWriter()) {
125+
objectMapper.writeValue(writer, classes);
126+
} catch (IOException e) {
127+
e.printStackTrace();
128+
}
129+
}
130+
131+
private void processClass(TypeElement type) {
132+
String className = processingEnv.getElementUtils().getBinaryName(type).toString();
133+
classes.put(className, new Bean(type));
134+
}
135+
136+
private HashMap<String, Property> getProperties(TypeElement type) {
137+
// Visit fields
138+
var fields = new HashMap<String, FieldInfo>();
139+
for (VariableElement field : ElementFilter.fieldsIn(type.getEnclosedElements())) {
140+
fields.put(field.getSimpleName().toString(), new FieldInfo(field));
141+
}
142+
143+
// Visit initializers like `setMyProperty(5)`
144+
var initializers = new HashMap<String, Object>();
145+
ClassTree classTree = trees.getTree(type);
146+
if (classTree != null) {
147+
for (Tree member : classTree.getMembers()) {
148+
if (member.getKind() == Tree.Kind.BLOCK) {
149+
BlockTree block = (BlockTree) member;
150+
if (block.isStatic()) continue; // skip static initializers
151+
for (StatementTree stmt : block.getStatements()) {
152+
if (stmt instanceof ExpressionStatementTree est
153+
&& est.getExpression() instanceof MethodInvocationTree mit) {
154+
ExpressionTree select = mit.getMethodSelect();
155+
if (select instanceof IdentifierTree identifier) {
156+
String name = identifier.getName().toString();
157+
if (name.startsWith("set") && mit.getArguments().size() == 1) {
158+
String prop = Introspector.decapitalize(name.substring(3));
159+
ExpressionTree arg = mit.getArguments().get(0);
160+
if (arg instanceof LiteralTree lit) {
161+
initializers.put(prop, lit.getValue());
162+
}
163+
}
164+
}
165+
}
166+
}
167+
}
168+
}
169+
}
170+
171+
// Visit setters
172+
var properties = new HashMap<String, Property>();
173+
for (ExecutableElement method : ElementFilter.methodsIn(type.getEnclosedElements())) {
174+
String methodName = method.getSimpleName().toString();
175+
if (methodName.startsWith("set")
176+
&& method.getModifiers().contains(javax.lang.model.element.Modifier.PUBLIC)
177+
&& method.getParameters().size() == 1) {
178+
String propertyName = Introspector.decapitalize(methodName.substring(3));
179+
180+
var property = new Property();
181+
property.description = getDocComment(method);
182+
property.type = method.getParameters().get(0).asType().toString();
183+
184+
var field = fields.get(propertyName);
185+
if (field != null) {
186+
if (property.description == null) property.description = field.docComment;
187+
if (field.initializer != null) property.defaultValue = field.initializer;
188+
}
189+
190+
var initValue = initializers.get(propertyName);
191+
if (initValue != null) property.defaultValue = initValue;
192+
193+
properties.put(propertyName, property);
194+
}
195+
}
196+
return properties;
197+
}
198+
199+
private static String getSuperclass(TypeElement type) {
200+
TypeMirror superType = type.getSuperclass();
201+
if (superType == null || superType.getKind() != TypeKind.DECLARED) return null;
202+
TypeElement superElement = (TypeElement) ((DeclaredType) superType).asElement();
203+
String superName = superElement.getQualifiedName().toString();
204+
if (superName.equals("java.lang.Object")) return null;
205+
return superName;
206+
}
207+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.archive.crawler.BeanDocProcessor

0 commit comments

Comments
 (0)