Last active
April 30, 2017 18:13
-
-
Save ndemengel/ea7cf31b4a6a6a7a7cc643529f04a037 to your computer and use it in GitHub Desktop.
Using QDox to generate a documentation of our Spring/RabbitMQ setup
This file contains 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 com.thoughtworks.qdox.JavaProjectBuilder; | |
import com.thoughtworks.qdox.model.*; | |
import org.junit.Test; | |
import java.io.*; | |
import java.text.MessageFormat; | |
import java.util.*; | |
import static java.util.Arrays.asList; | |
import static java.util.Arrays.stream; | |
import static java.util.Comparator.comparing; | |
import static java.util.Optional.ofNullable; | |
import static java.util.stream.Collectors.toList; | |
public class TestEventsDocGenerator { | |
private static final String CONTEXT_PREFIX = "com.hopwork"; | |
@Test | |
public void buildEventsReport() throws Exception { | |
JavaProjectBuilder builder = new JavaProjectBuilder(); | |
builder.setErrorHandler(new QDoxErrorHandler()); | |
// Adding all .java files in a source tree (recursively). | |
stream(new File("..").listFiles(File::isDirectory)) | |
.map(d -> new File(d, "src/main/java")) | |
.filter(File::exists) | |
.forEach(builder::addSourceTree); | |
Map<String, EventDoc> events = searchForEvents(builder); | |
printEvents(events); | |
} | |
private void printEvents(Map<String, EventDoc> events) { | |
StringWriter out = new StringWriter(); | |
try (PrintWriter writer = new PrintWriter(out)) { | |
printEvents(writer, sortedEvents(events)); | |
} | |
String template = Utils.readFile("events-template.html"); | |
String title = "Events"; | |
String content = out.toString(); | |
Utils.writeFile("target/doc/", "events.html", MessageFormat.format(template, title, content)); | |
} | |
private List<EventDoc> sortedEvents(Map<String, EventDoc> events) { | |
return events.values().stream() | |
.peek(EventDoc::sortConsumers) | |
.peek(EventDoc::sortEventFields) | |
.sorted(comparing(EventDoc::getEventClassName)) | |
.collect(toList()); | |
} | |
private Map<String, EventDoc> searchForEvents(JavaProjectBuilder builder) { | |
Map<String, EventDoc> events = new HashMap<>(); | |
JavaClass baseEventClass = builder.getClassByName("com.hopwork.core.events.Event"); | |
baseEventClass.getDerivedClasses().forEach(eventClass -> { | |
if (eventClass.isAbstract()) { | |
return; | |
} | |
EventDoc doc = new EventDoc(eventClass.getPackageName(), eventClass.getName(), eventClass.getComment()); | |
JavaField exchangeNameField = eventClass.getFieldByName("EXCHANGE_NAME"); | |
if (exchangeNameField != null) { | |
doc.setExchangeName(withoutQuotes(exchangeNameField.getInitializationExpression())); | |
} | |
eventClass.getFields().forEach(f -> { | |
if (!f.isStatic()) { | |
String type = asList("java.lang", "java.util").contains(f.getType().getPackageName()) | |
? f.getType().getName() | |
: f.getType().getFullyQualifiedName(); | |
doc.addEventField(type, f.getName()); | |
} | |
}); | |
events.put(eventClass.getFullyQualifiedName(), doc); | |
}); | |
findConsumers(builder, events); | |
findPublishers(builder, events); | |
return events; | |
} | |
private void findPublishers(JavaProjectBuilder builder, Map<String, EventDoc> events) { | |
builder.getSources().stream() | |
.filter(s -> s.getPackageName().startsWith(CONTEXT_PREFIX)) | |
.forEach(source -> { | |
findMatches(events, source.getPackageName(), source.getImports()).forEach(eventDoc -> { | |
source.getClasses().forEach(clazz -> { | |
clazz.getMethods().stream() | |
.filter(m -> m.getSourceCode().contains("new " + eventDoc.getEventClassName())) | |
.forEach(m -> { | |
eventDoc.addPublisher(clazz.getPackageName(), clazz.getName(), m.getCallSignature()); | |
}); | |
}); | |
}); | |
}); | |
} | |
private List<EventDoc> findMatches(Map<String, EventDoc> events, String packageName, List<String> imports) { | |
return events.values().stream() | |
.filter(doc -> doc.getEventPackageName().equals(packageName) | |
|| imports.contains(doc.getEventPackageName() + "." + doc.getEventClassName())) | |
.collect(toList()); | |
} | |
private void findConsumers(JavaProjectBuilder builder, Map<String, EventDoc> events) { | |
for (JavaPackage p : builder.getPackages()) { | |
if (!p.getName().startsWith(CONTEXT_PREFIX)) { | |
continue; | |
} | |
for (JavaClass clazz : p.getClasses()) { | |
for (JavaMethod method : clazz.getMethods()) { | |
for (JavaAnnotation annotation : method.getAnnotations()) { | |
final JavaClass type = annotation.getType(); | |
if (type.getFullyQualifiedName().contains("RabbitEventListener")) { | |
String queue = (String) annotation.getNamedParameter("queue"); | |
String delay = (String) annotation.getNamedParameter("delay"); | |
method.getParameterTypes(true).stream() | |
.map(paramType -> events.get(paramType.getFullyQualifiedName())) | |
.filter(Objects::nonNull) | |
.findFirst() | |
.ifPresent(eventDoc -> | |
eventDoc.addConsumer( | |
clazz.getPackageName(), | |
clazz.getName(), | |
method.getName(), | |
withoutQuotes(queue), | |
ofNullable(delay).flatMap(d -> resolveConstant(d, clazz, builder)) | |
) | |
); | |
} | |
} | |
} | |
} | |
} | |
} | |
private Optional<String> resolveConstant(String constantLiteral, JavaClass userClass, JavaProjectBuilder builder) { | |
String[] parts = constantLiteral.split("\\."); | |
if (parts.length != 2) { | |
return Optional.of(constantLiteral); | |
} | |
String constantFieldName = parts[1]; | |
return resolveConstantHolderFqn(constantLiteral, userClass) | |
.map(builder::getClassByName) | |
.map(constantHolderClass -> { | |
JavaField constantField = constantHolderClass.getFieldByName(constantFieldName); | |
return constantField.getInitializationExpression(); | |
}); | |
} | |
private Optional<String> resolveConstantHolderFqn(String constantLiteral, JavaClass userClass) { | |
String[] parts = constantLiteral.split("\\."); | |
if (parts.length != 2) { | |
return Optional.of(constantLiteral); | |
} | |
String constantHolderClassName = parts[0]; | |
return userClass.getSource().getImports().stream() | |
.filter(imp -> imp.endsWith(constantHolderClassName)) | |
.findFirst(); | |
} | |
private String withoutQuotes(String str) { | |
return str.replaceAll("\"", ""); | |
} | |
private void printEvents(PrintWriter writer, List<EventDoc> events) { | |
writer.println("<h1>Events</h1>"); | |
for (EventDoc event : events) { | |
writer.println("<section class='event-doc'>"); | |
writer.print("<h2 class='searchable'>"); | |
writer.print(event.getEventClassName()); | |
writer.print(" <small>(<tt>"); | |
writer.print(event.getEventPackageName()); | |
writer.println("</tt>)</small></h2>"); | |
event.getDocumentation().ifPresent(doc -> { | |
writer.print("<p>"); | |
writer.print(doc); | |
writer.println("</p>"); | |
}); | |
if (!event.getEventFields().isEmpty()) { | |
writer.println("<pre class='event-fields'>"); | |
event.getEventFields().forEach(f -> { | |
writer.print(f.getType()); | |
writer.print(" "); | |
writer.println(f.getName()); | |
}); | |
writer.println("</pre>"); | |
} | |
writer.print("<p>Exchange: <tt class='exchange searchable'>"); | |
writer.print(event.getExchangeName()); | |
writer.println("</tt></p>"); | |
writer.println("<p>Consumers: "); | |
if (event.getConsumers().isEmpty()) { | |
writer.println("none found</p>"); | |
} else { | |
writer.println("</p><ul>"); | |
event.getConsumers().forEach(c -> { | |
writer.print("<li><tt class='searchable'>"); | |
writer.print(c.getClassName()); | |
writer.print("#"); | |
writer.print(c.getMethodName()); | |
writer.print("</tt> <small>(<tt class='searchable'>"); | |
writer.print(c.getPackageName()); | |
writer.println("</tt>)</small>"); | |
writer.print("<br>Queue: <tt class='queue searchable'>"); | |
writer.print(c.getQueueName()); | |
writer.print("</tt>"); | |
c.getDelay().ifPresent(d -> { | |
writer.print(" (delay: "); | |
writer.print(d.replaceAll("_", " ")); | |
writer.print("ms"); | |
writer.print(")"); | |
}); | |
writer.println("</li>"); | |
}); | |
writer.println("</ul>"); | |
} | |
writer.println(); | |
writer.println("<p>Publishers: "); | |
if (event.getPublishers().isEmpty()) { | |
writer.println("none found</p>"); | |
} else { | |
writer.println("</p><ul>"); | |
event.getPublishers().forEach(p -> { | |
writer.print("<li><tt class='searchable'>"); | |
writer.print(p.getClassName()); | |
writer.print("#"); | |
writer.print(p.getMethodCallSignature()); | |
writer.print("</tt> <small>(<tt class='searchable'>"); | |
writer.print(p.getPackageName()); | |
writer.println("</tt>)</small></li>"); | |
}); | |
writer.println("</ul>"); | |
} | |
writer.println("</section>"); | |
} | |
} | |
} |
This file contains 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 java.util.ArrayList; | |
import java.util.List; | |
import java.util.Optional; | |
import static java.util.Collections.unmodifiableList; | |
import static java.util.Comparator.comparing; | |
import static java.util.Optional.ofNullable; | |
public final class EventDoc { | |
private final String eventPackageName; | |
private final String eventClassName; | |
private final Optional<String> documentation; | |
private String exchangeName; | |
private List<EventConsumer> consumers = new ArrayList<>(); | |
private List<EventPublisher> publishers = new ArrayList<>(); | |
private List<Field> eventFields = new ArrayList<>(); | |
public EventDoc(String eventPackageName, String eventClassName, String documentation) { | |
this.eventPackageName = eventPackageName; | |
this.eventClassName = eventClassName; | |
this.documentation = ofNullable(documentation); | |
} | |
public void setExchangeName(String exchangeName) { | |
this.exchangeName = exchangeName; | |
} | |
public void addConsumer(String packageName, String className, String methodName, String queueName, Optional<String> delay) { | |
consumers.add(new EventConsumer(packageName, className, methodName, queueName, delay)); | |
} | |
public void addEventField(String type, String name) { | |
eventFields.add(new Field(type, name)); | |
} | |
public void addPublisher(String packageName, String className, String methodCallSignature) { | |
publishers.add(new EventPublisher(packageName, className, methodCallSignature)); | |
} | |
public List<EventConsumer> getConsumers() { | |
return unmodifiableList(consumers); | |
} | |
public String getEventClassName() { | |
return eventClassName; | |
} | |
public Optional<String> getDocumentation() { | |
return documentation; | |
} | |
public String getEventPackageName() { | |
return eventPackageName; | |
} | |
public String getExchangeName() { | |
return exchangeName; | |
} | |
public List<Field> getEventFields() { | |
return unmodifiableList(eventFields); | |
} | |
public List<EventPublisher> getPublishers() { | |
return unmodifiableList(publishers); | |
} | |
@Override | |
public String toString() { | |
return String.format("%s.%s(exchange=%s, consumers=%s, publishers=%s)", eventPackageName, eventClassName, exchangeName, consumers, publishers); | |
} | |
public void sortConsumers() { | |
consumers.sort(comparing(EventConsumer::getClassName)); | |
} | |
public void sortEventFields() { | |
eventFields.sort(comparing(Field::getName)); | |
} | |
public static class EventConsumer { | |
private final String packageName; | |
private final String className; | |
private final String methodName; | |
private final String queueName; | |
private final Optional<String> delay; | |
public EventConsumer(String packageName, String className, String methodName, String queueName, Optional<String> delay) { | |
this.packageName = packageName; | |
this.className = className; | |
this.methodName = methodName; | |
this.queueName = queueName; | |
this.delay = delay; | |
} | |
public String getClassName() { | |
return className; | |
} | |
public Optional<String> getDelay() { | |
return delay; | |
} | |
public String getMethodName() { | |
return methodName; | |
} | |
public String getPackageName() { | |
return packageName; | |
} | |
public String getQueueName() { | |
return queueName; | |
} | |
@Override | |
public String toString() { | |
return String.format("%s.%s#%s(queue=%s%s)", packageName, className, methodName, queueName, delay.map(d -> ",delay=" + d).orElse("")); | |
} | |
} | |
public static class EventPublisher { | |
private final String packageName; | |
private final String className; | |
private final String methodCallSignature; | |
public EventPublisher(String packageName, String className, String methodCallSignature) { | |
this.packageName = packageName; | |
this.className = className; | |
this.methodCallSignature = methodCallSignature; | |
} | |
public String getClassName() { | |
return className; | |
} | |
public String getMethodCallSignature() { | |
return methodCallSignature; | |
} | |
public String getPackageName() { | |
return packageName; | |
} | |
@Override | |
public String toString() { | |
return String.format("%s.%s#%s", packageName, className, methodCallSignature); | |
} | |
} | |
public static class Field { | |
private final String type; | |
private final String name; | |
public Field(String type, String name) { | |
this.type = type; | |
this.name = name; | |
} | |
public String getType() { | |
return type; | |
} | |
public String getName() { | |
return name; | |
} | |
} | |
} |
This file contains 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 java.io.*; | |
public class Utils { | |
public static String readFile(final String filename) { | |
String lineSep = System.lineSeparator(); | |
StringBuilder buffer = new StringBuilder(); | |
try (InputStream is = Utils.class.getResourceAsStream(filename); | |
BufferedReader in = new BufferedReader(new InputStreamReader(is))) { | |
String str; | |
while ((str = in.readLine()) != null) { | |
buffer.append(System.lineSeparator()); | |
buffer.append(str); | |
} | |
} catch (IOException e) { | |
throw new RuntimeException(e); | |
} | |
return buffer.toString(); | |
} | |
public static void writeFile(String path, String filename, String content) { | |
File parent = new File(path); | |
parent.mkdir(); | |
String outputFileName = path + filename; | |
try (FileOutputStream fos = new FileOutputStream(outputFileName); | |
PrintWriter w = new PrintWriter(new BufferedWriter(new OutputStreamWriter(fos, "ISO-8859-1")))) { | |
w.println(content); | |
} catch (IOException e) { | |
throw new RuntimeException(e); | |
} | |
} | |
} |
This file contains 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
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>{0}</title> | |
<style> | |
.event-fields '{ | |
display: inline-block; | |
margin: 0 0 10px 40px; | |
padding: 5px 10px; | |
border-radius: 4px; | |
background-color: #eee; | |
'} | |
.exchange, .queue '{ | |
background-color: #eee; | |
'} | |
.exchange '{ | |
color: royalblue; | |
'} | |
.queue '{ | |
color: green; | |
'} | |
</style> | |
<script> | |
function search(searchInput) '{ | |
const docs = document.querySelectorAll(".event-doc"); | |
for (let i = 0; i < docs.length; i++) { | |
const doc = docs[i]; | |
const docSearchableChildren = doc.querySelectorAll(".searchable"); | |
let anyChildContainsText = false; | |
for (let j = 0; j < docSearchableChildren.length; j++) { | |
const child = docSearchableChildren[j]; | |
if (child.textContent.indexOf(searchInput.value) !== -1) { | |
anyChildContainsText = true; | |
} | |
} | |
doc.style.display = anyChildContainsText ? "block" : "none"; | |
} | |
'} | |
</script> | |
</head> | |
<body> | |
<div> | |
<input name="q" size="40" style="float: right;" onkeyup="search(this)" | |
placeholder="Search for events, exchanges queues..."> | |
</div> | |
{1} | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment