Skip to content

Instantly share code, notes, and snippets.

@ndemengel
Last active April 30, 2017 18:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ndemengel/ea7cf31b4a6a6a7a7cc643529f04a037 to your computer and use it in GitHub Desktop.
Save ndemengel/ea7cf31b4a6a6a7a7cc643529f04a037 to your computer and use it in GitHub Desktop.
Using QDox to generate a documentation of our Spring/RabbitMQ setup
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>");
}
}
}
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;
}
}
}
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);
}
}
}
<!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