Commit 21d946f4 authored by René Schöne's avatar René Schöne
Browse files

Merge branch 'feature/stereotypes-and-more' into 'dev'

working on better visualization

Closes #1, #11, and #13

See merge request jastadd/relast2uml!9
parents 8e910ed0 4c20df2c
Pipeline #12834 passed with stages
in 3 minutes and 11 seconds
......@@ -55,7 +55,7 @@ ragdoc_build:
needs:
- build
script:
- JAVA_FILES=$(find dumpAstWithPlantuml/src/ -name '*.java')
- JAVA_FILES=$(find dumpAst/src/ -name '*.java')
- /ragdoc-builder/start-builder.sh -excludeGenerated -d data/ $JAVA_FILES
artifacts:
paths:
......
......@@ -14,8 +14,10 @@ plugins {
apply plugin: 'jastadd'
dependencies {
jastadd2 "org.jastadd:jastadd:2.3.4"
implementation group: 'com.github.spullara.mustache.java', name: 'compiler', version: "${mustache_java_version}"
jastadd2 "org.jastadd:jastadd:2.3.5"
implementation fileTree(include: ['plantuml.jar'], dir: '../libs')
implementation group: 'com.github.spullara.mustache.java', name: 'compiler', version: "0.9.10"
implementation group: 'org.yaml', name: 'snakeyaml', version: '1.27'
}
File dumpAstGrammar = file('./src/main/jastadd/DumpAst.relast')
......
DumpAst ::= DumpNode* <PackageName> BuildConfig PrintConfig ;
rel DumpAst.RootNode -> DumpNode ;
BuildConfig ::= StyleInformation GlobalPatternCollection:PatternCollection
ExcludeTypePattern:TypePatternCollectionMapping* IncludeTypePattern:TypePatternCollectionMapping*
<TypeIgnorePattern> <IncludeEmptyString:boolean> <ExcludeNullNodes:boolean> <Debug:boolean>;
TypePatternCollectionMapping ::= <TypeRegex> PatternCollection ;
PatternCollection ::= <TokenPattern> <ChildPattern> <RelationPattern> <AttributePattern> <NonterminalAttributePattern> ;
StyleInformation ::= <NameMethod:StyleMethod> <BackgroundColorMethod:StyleMethod> <TextColorMethod:StyleMethod>;
PrintConfig ::= <Scale:double> <Version> <OrderChildren:boolean> Header* ;
Header ::= <Value> ;
rel DumpAst.RootNode? -> DumpNode ;
DumpNode ::= DumpChildNode* DumpToken* DumpRelation*
<Name> <Label> <BackgroundColor> <TextColor> <Object:Object> <Invisible:boolean>
<Name> <Label> <BackgroundColor> <TextColor> <Object:Object> <Invisible:boolean> <Computed:boolean> <ManualStereotypes>
/InvisiblePath/ ;
InnerDumpNode ;
rel InnerDumpNode.DumpNode <-> DumpNode.ContainerOfInner ;
......@@ -52,3 +42,21 @@ ListRelationMethod : AnalysedMethod ;
abstract TokenMethod : AnalysedMethod ;
IntrinsicTokenMethod : TokenMethod ;
AttributeMethod : TokenMethod ;
BuildConfig ::= StyleInformation
GlobalPatternCollection:PatternCollection
ExcludeTypePattern:TypePatternCollectionMapping*
IncludeTypePattern:TypePatternCollectionMapping*
<TypeIgnorePattern>
<IncludeEmptyString:boolean>
<ExcludeNullNodes:boolean>
<Debug:boolean>;
TypePatternCollectionMapping ::= <TypeRegex> PatternCollection ;
PatternCollection ::= <TokenPattern> <ChildPattern> <RelationPattern> <AttributePattern> <NonterminalAttributePattern> ;
StyleInformation ::= <NameMethod:StyleMethod> <BackgroundColorMethod:StyleMethod> <TextColorMethod:StyleMethod> <StereotypeMethod:StyleMethod> <ComputedColor>;
PrintConfig ::= Header*
<Scale:double>
<Version>
<OrderChildren:boolean> ;
Header ::= <Value> ;
aspect GenerationBackend {
class DumpAst {
private enum Source {
RELATION, INVISIBLE_PARENT, PARENT, ROOT
enum Source {
ROOT, NORMAL, RELATION
}
class TransformationOptions {
// todo should be read-only, i.e., copy-on-write
public Source source;
public boolean invisible;
public boolean allowNullObjects;
public boolean computed;
public TransformationOptions asRelation() {
return fromSource(Source.RELATION, false);
}
public TransformationOptions asRoot() {
return fromSource(Source.ROOT, false).allowNullObjectsOnce();
}
public TransformationOptions asNormal(boolean shouldBeInvisible) {
return fromSource(Source.NORMAL, shouldBeInvisible);
}
public TransformationOptions computed(boolean computed) {
if (computed == this.computed) {
return this;
}
return new TransformationOptions(this.source, this.invisible, this.allowNullObjects, computed);
}
public TransformationOptions allowNullObjectsOnce() {
return new TransformationOptions(this.source, this.invisible, !DumpAst.this.getBuildConfig().getExcludeNullNodes(), this.computed);
}
private TransformationOptions fromSource(Source source, boolean shouldBeInvisible) {
return new TransformationOptions(source, this.invisible || shouldBeInvisible, this.computed);
}
private TransformationOptions(Source source, boolean invisible, boolean computed) {
this(source, invisible, false, computed);
}
private TransformationOptions(Source source, boolean invisible, boolean allowNullObjects, boolean computed) {
this.source = source;
this.invisible = invisible;
this.allowNullObjects = allowNullObjects;
this.computed = computed;
}
}
}
private TransformationOptions DumpAst.options() {
return new TransformationOptions(Source.NORMAL, false, false);
}
// --- transform --- (need to be a method, because it alters the AST while traversing the object structure)
protected DumpNode DumpAst.transform(TransformationTransferInformation tti, Object obj)
throws java.lang.reflect.InvocationTargetException, IllegalAccessException, NoSuchMethodException {
DumpNode result = transform(tti, obj, Source.ROOT);
DumpNode result = transform(tti, obj, options().asRoot());
setRootNode(result);
// post-process relationTargetsUnprocessed
boolean someAreUnprocessed = true;
......@@ -17,21 +65,18 @@ aspect GenerationBackend {
java.util.Map<DumpNode, Boolean> copy = new java.util.HashMap<>(tti.relationTargetsUnprocessed);
for (java.util.Map.Entry<DumpNode, Boolean> entry : copy.entrySet()) {
if (entry.getValue()) {
transform(tti, entry.getKey().getObject(), Source.ROOT);
transform(tti, entry.getKey().getObject(), options().asRoot());
someAreUnprocessed = true;
}
}
}
return result;
}
protected DumpNode DumpAst.transform(TransformationTransferInformation tti, Object obj, Source source)
throws java.lang.reflect.InvocationTargetException, IllegalAccessException, NoSuchMethodException {
return transform(tti, obj, source, false);
}
protected DumpNode DumpAst.transform(
TransformationTransferInformation tti, Object obj, Source source, boolean allowNullObj)
TransformationTransferInformation tti, Object obj, TransformationOptions options)
throws java.lang.reflect.InvocationTargetException, IllegalAccessException, NoSuchMethodException {
if (obj == null && !allowNullObj) {
if (obj == null && !options.allowNullObjects) {
return null;
}
DumpNode node;
......@@ -44,7 +89,7 @@ aspect GenerationBackend {
objClassName = obj.getClass().getSimpleName();
}
if (node != null) {
if (source == Source.RELATION) {
if (options.source == Source.RELATION) {
return node;
}
// either processing as parent, or later as root-node. so mark it as processed.
......@@ -53,6 +98,7 @@ aspect GenerationBackend {
node = new DumpNode();
node.setObject(obj);
node.setName("node" + (tti.nodeCounter++));
node.setComputed(options.computed);
tti.transformed.put(obj, node);
this.addDumpNode(node);
}
......@@ -61,11 +107,11 @@ aspect GenerationBackend {
}
applyStyle(node);
// do not process node further if coming from a relation
if (source == Source.RELATION) {
if (options.source == Source.RELATION) {
tti.relationTargetsUnprocessed.put(node, true);
return node;
}
if (source == Source.INVISIBLE_PARENT || !isTypeEnabled(objClassName)) {
if (options.invisible || !isTypeEnabled(objClassName)) {
node.setInvisible(true);
}
if (obj == null) {
......@@ -78,7 +124,7 @@ aspect GenerationBackend {
// -- singleChild --
Object target = containmentMethod.getMethod().invoke(obj);
String childName = containmentMethod.getName();
DumpNode targetNode = transform(tti, target, nextSource(source, !isChildEnabled(objClassName, childName)), !getBuildConfig().getExcludeNullNodes());
DumpNode targetNode = transform(tti, target, options.asNormal(!isChildEnabled(objClassName, childName)).allowNullObjectsOnce());
if (targetNode != null) {
DumpNormalChildNode normalChild = new DumpNormalChildNode();
normalChild.setName(childName);
......@@ -95,7 +141,7 @@ aspect GenerationBackend {
boolean shouldBeInvisisble = !isChildEnabled(objClassName, childName);
listChild.setName(childName);
for (Object target : targetList) {
DumpNode targetNode = transform(tti, target, nextSource(source, shouldBeInvisisble));
DumpNode targetNode = transform(tti, target, options.asNormal(shouldBeInvisisble));
if (target != null && targetNode != null) {
listChild.addInnerDumpNode(new InnerDumpNode().setDumpNode(targetNode));
}
......@@ -112,24 +158,26 @@ aspect GenerationBackend {
// -- singleChild --
Object target = otherMethod.getMethod().invoke(obj);
String childName = otherMethod.getName();
DumpNode targetNode = transform(tti, target, nextSource(source, !isChildEnabled(objClassName, childName)), !getBuildConfig().getExcludeNullNodes());
boolean computed = otherMethod.asSingleChildMethod().isNTASingleChildMethod();
DumpNode targetNode = transform(tti, target, options.asNormal(!isChildEnabled(objClassName, childName)).computed(computed).allowNullObjectsOnce());
if (targetNode != null) {
DumpNormalChildNode normalChild = new DumpNormalChildNode();
normalChild.setName(childName);
normalChild.setDumpNode(targetNode);
normalChild.setComputed(otherMethod.asSingleChildMethod().isNTASingleChildMethod());
normalChild.setComputed(computed);
node.addDumpChildNode(normalChild);
}
} else if (otherMethod.isListChildMethod()) {
// -- listChild --
Iterable<?> targetList = (Iterable<?>) otherMethod.getMethod().invoke(obj);
DumpListChildNode listChild = new DumpListChildNode();
listChild.setComputed(otherMethod.asListChildMethod().isNTAListChildMethod());
boolean computed = otherMethod.asListChildMethod().isNTAListChildMethod();
listChild.setComputed(computed);
String childName = otherMethod.getName();
boolean shouldBeInvisisble = !isChildEnabled(objClassName, childName);
listChild.setName(childName);
for (Object target : targetList) {
DumpNode targetNode = transform(tti, target, nextSource(source, shouldBeInvisisble));
DumpNode targetNode = transform(tti, target, options.asNormal(shouldBeInvisisble).computed(computed));
if (target != null && targetNode != null) {
listChild.addInnerDumpNode(new InnerDumpNode().setDumpNode(targetNode));
}
......@@ -140,7 +188,7 @@ aspect GenerationBackend {
} else if (otherMethod.isSingleRelationMethod()) {
// -- singleRelation --
Object target = otherMethod.getMethod().invoke(obj);
DumpNode targetNode = transform(tti, target, Source.RELATION);
DumpNode targetNode = transform(tti, target, options.asRelation());
if (target != null && targetNode != null) {
DumpNormalRelation normalRelation = new DumpNormalRelation();
normalRelation.setName(otherMethod.getName());
......@@ -153,7 +201,7 @@ aspect GenerationBackend {
DumpListRelation listRelation = new DumpListRelation();
listRelation.setName(otherMethod.getName());
for (Object target : targetList) {
DumpNode targetNode = transform(tti, target, Source.RELATION);
DumpNode targetNode = transform(tti, target, options.asRelation());
if (target != null && targetNode != null) {
listRelation.addInnerDumpNode(new InnerDumpNode(targetNode));
}
......@@ -165,7 +213,7 @@ aspect GenerationBackend {
// -- token --
Object target = otherMethod.getMethod().invoke(obj);
if (target != null) {
DumpNode targetNode = transform(tti, target, Source.RELATION);
DumpNode targetNode = transform(tti, target, options.asRelation());
DumpToken token = null;
// TODO check, if Iterable.isAssignableFrom(target.getClass()).
// if so, check isAstNode for first non-null to add DumpReferenceToken's, otherwise DumpValueToken's
......@@ -196,11 +244,7 @@ aspect GenerationBackend {
node.setLabel(getBuildConfig().getStyleInformation().getNameMethod().get(obj));
node.setBackgroundColor(getBuildConfig().getStyleInformation().getBackgroundColorMethod().get(obj));
node.setTextColor(getBuildConfig().getStyleInformation().getTextColorMethod().get(obj));
}
private Source DumpAst.nextSource(Source currentSource, boolean shouldBeInvisible) {
return currentSource == Source.INVISIBLE_PARENT ? Source.INVISIBLE_PARENT :
(shouldBeInvisible ? Source.INVISIBLE_PARENT : Source.PARENT);
node.setManualStereotypes(getBuildConfig().getStyleInformation().getStereotypeMethod().get(obj));
}
syn nta ClassAnalysisResult DumpAst.analyzeClass(java.lang.Class<?> clazz) {
......@@ -491,8 +535,8 @@ aspect GenerationBackend {
return p.matcher(input).matches();
}
// --- version --- (mustache has buildConfig as context)
syn String BuildConfig.version() = printConfig().getVersion();
// --- debug --- (mustache has printConfig as context)
syn boolean PrintConfig.debug() = buildConfig().getDebug();
private static String DumpAst.invokeName(java.lang.annotation.Annotation annotation) {
return (String) invokeMethod("name", annotation);
......@@ -509,11 +553,15 @@ aspect GenerationBackend {
}
}
// --- isNull ---
syn boolean DumpNode.isNull() {
return getObject() == null;
}
// --- isAstNode ---
syn boolean DumpNode.isAstNode() {
if (getObject() == null) {
// this is only possible for normal child nodes, and they are ast nodes
return true;
return false;
}
Class<?> clazz = getObject().getClass();
for (java.lang.reflect.Method method : clazz.getMethods()) {
......@@ -562,6 +610,30 @@ aspect GenerationBackend {
}
}
// --- stereotypeList ---
syn String DumpNode.stereotypeList() {
StringBuilder sb = new StringBuilder(getManualStereotypes());
if (!automaticStereotypes().isEmpty()) {
// add automatic stereotypes, if there are any
if (!getManualStereotypes().isEmpty()) {
// add a comma between manual and automatic, if both are present
sb.append(",");
}
sb.append(automaticStereotypes());
}
String manualAndAutomaticStereotypes = sb.toString();
if (manualAndAutomaticStereotypes.isEmpty()) {
return "";
}
sb = new StringBuilder();
for (String stereotype : manualAndAutomaticStereotypes.split(",")) {
sb.append(" <<").append(stereotype).append(">>");
}
return sb.toString();
}
// --- manualAndAutomaticStereotypes ---
syn String DumpNode.automaticStereotypes() = getComputed() ? "NTA" : "";
// --- myChildren ---
syn java.util.List<DumpNode> DumpNode.myChildren() {
java.util.List<DumpNode> result = new java.util.ArrayList<>();
......@@ -597,94 +669,19 @@ aspect GenerationBackend {
int nodeCounter = 0;
}
syn String DumpAst.toYaml(boolean prependCreationComment) {
Document doc = new Document();
doc.setRootElement(getRootNode().toYaml());
return doc.prettyPrint(prependCreationComment);
}
syn MappingElement DumpNode.toYaml() {
MappingElement result = new MappingElement();
// tokens are key-value-pairs
for (DumpToken token : getDumpTokenList()) {
if (token.isDumpValueToken()) {
result.put(token.getName(), makeValueElement(token.asDumpValueToken().getValue()));
} else {
result.put(token.getName(), token.asDumpReferenceToken().getValue().toYaml());
}
}
// children
for (DumpChildNode child : getDumpChildNodeList()) {
ListElement list = new ListElement();
for (DumpNode inner : child.innerNodes(true)) {
list.add(inner.toYaml());
}
if (child.isList()) {
result.put(child.getName(), list);
} else if (list.getNumElement() == 1) {
result.put(child.getName(), list.getElement(0));
}
}
// relations
for (DumpRelation relation : getDumpRelationList()) {
ListElement list = new ListElement();
for (DumpNode inner : relation.innerNodes(true)) {
list.add(inner.toYaml());
}
if (relation.isList()) {
result.put(relation.getName(), list);
} else if (list.getNumElement() == 1) {
result.put(relation.getName(), list.getElement(0));
}
}
return result;
}
private static ComplexElement ASTNode.HELPER_COMPLEX_ELEMENT = new MappingElement();
protected static Element ASTNode.makeValueElement(Object obj) {
if (obj instanceof Integer) {
return ValueElement.of((int) obj);
} else if (obj instanceof Boolean) {
return ValueElement.of((boolean) obj);
} else {
return HELPER_COMPLEX_ELEMENT.makeStringElement(obj.toString());
}
}
public class AppendableWriter extends java.io.Writer {
private final StringBuilder sb;
public AppendableWriter(StringBuilder sb) {
this.sb = sb;
}
@Override
public void write(char[] chars, int off, int len) {
sb.append(chars, off, len);
}
@Override
public void write(String str) {
sb.append(str);
}
@Override
public void flush() {
}
@Override
public void close() {
}
}
static StyleInformation StyleInformation.createDefault() {
StyleInformation result = new StyleInformation();
result.setNameMethod(n -> n == null ? "null" : n.getClass().getSimpleName() + "@" + Integer.toHexString(n.hashCode()));
result.setBackgroundColorMethod(n -> "");
result.setTextColorMethod(n -> "");
result.setStereotypeMethod(n -> "");
result.setComputedColor("blue");
return result;
}
syn String DumpAst.computedColor() = getBuildConfig().getStyleInformation().getComputedColor();
@FunctionalInterface
public interface StyleMethod<ASTNODE> {
String get(ASTNODE node);
......
......@@ -347,6 +347,16 @@ public class DumpBuilder {
return this;
}
public <ASTNODE> DumpBuilder setStereotypeMethod(StyleMethod<ASTNODE> stereotypeMethod) {
buildConfig.getStyleInformation().setStereotypeMethod(stereotypeMethod);
return this;
}
public DumpBuilder setComputedColor(String color) {
buildConfig.getStyleInformation().setComputedColor(color);
return this;
}
public DumpBuilder setScale(double value) {
printConfig.setScale(value);
return this;
......@@ -369,7 +379,17 @@ public class DumpBuilder {
protected DumpAst build() {
if (result == null) {
result = new DumpAst();
result.setPackageName(this.packageName == null ? this.target.getClass().getPackage().getName() : this.packageName);
final String packageNameToUse;
if (this.packageName != null) {
packageNameToUse = this.packageName;
} else {
if (this.target == null) {
packageNameToUse = null;
} else {
packageNameToUse = this.target.getClass().getPackage().getName();
}
}
result.setPackageName(packageNameToUse);
result.setBuildConfig(this.buildConfig);
result.setPrintConfig(this.printConfig);
try {
......@@ -400,11 +420,38 @@ public class DumpBuilder {
}
public DumpBuilder dumpAsYaml(java.nio.file.Path destination, boolean prependCreationComment) throws java.io.IOException {
String content = build().toYaml(prependCreationComment);
String content = build().printYaml(prependCreationComment);
try (java.io.Writer writer = java.nio.file.Files.newBufferedWriter(destination)) {
writer.write(content);
}
return this;
}
/**
* Write out content as PNG image generated by plantuml.
* @param destination path of destination file
* @return this
* @throws java.io.IOException if an I/O error happend during opening or writing in that file
*/
public DumpBuilder dumpAsPNG(java.nio.file.Path destination) throws java.io.IOException {
String content = build().toPlantUml();
net.sourceforge.plantuml.SourceStringReader reader = new net.sourceforge.plantuml.SourceStringReader(content);
reader.outputImage(java.nio.file.Files.newOutputStream(destination));
return this;
}
/**
* Write out content as SVG image generated by plantuml.
* @param destination path of destination file
* @return this
* @throws java.io.IOException if an I/O error happend during opening or writing in that file
*/
public DumpBuilder dumpAsSVG(java.nio.file.Path destination) throws java.io.IOException {
String content = build().toPlantUml();
net.sourceforge.plantuml.SourceStringReader reader = new net.sourceforge.plantuml.SourceStringReader(content);
reader.outputImage(java.nio.file.Files.newOutputStream(destination),
new net.sourceforge.plantuml.FileFormatOption(net.sourceforge.plantuml.FileFormat.SVG));
return this;
}
}
}
......@@ -19,7 +19,35 @@ aspect GenerationMustache {
com.github.mustachejava.DefaultMustacheFactory mf = new com.github.mustachejava.DefaultMustacheFactory();
mf.setObjectHandler(roh);
com.github.mustachejava.Mustache m = mf.compile("dumpAst.mustache");
m.execute(new java.io.PrintWriter(new AppendableWriter(sb)), this);
String yaml = this.printYaml(false);
Object context = new org.yaml.snakeyaml.Yaml().load(new StringReader(yaml));
m.execute(new java.io.PrintWriter(new AppendableWriter(sb)), context);
return sb.toString();
}
public class AppendableWriter extends java.io.Writer {
private final StringBuilder sb;
public AppendableWriter(StringBuilder sb) {
this.sb = sb;
}
@Override
public void write(char[] chars, int off, int len) {
sb.append(chars, off, len);
}
@Override
public void write(String str) {
sb.append(str);
}
@Override
public void flush() {
}
@Override
public void close() {
}
}
}
aspect GenerationToYaml {
syn String DumpAst.printYaml(boolean prependCreationComment) {
Document doc = new Document();
doc.setRootElement(this.toYaml(false));
return doc.prettyPrint(prependCreationComment);
}
// todo: default impl should actually be abstract instead
syn MappingElement ASTNode.toYaml(boolean fromRelation) = new MappingElement();
static MappingElement ASTNode.safeToYaml(ASTNode node, boolean fromRelation) {
if (node == null) {
return null;
}
return node.toYaml(fromRelation);
}
syn MappingElement DumpAst.toYaml(boolean fromRelation) {
MappingElement result = new MappingElement();
// children
result.put("PrintConfig", safeToYaml(getPrintConfig(), fromRelation));
addYamledList(result, "DumpNodes", getDumpNodeList(), fromRelation);
// attributes
result.put("computedColor", computedColor());
return result;
}
static void ASTNode.addYamledList(MappingElement base, String key, Iterable<? extends ASTNode<?>> iterable, boolean fromRelation) {
ListElement innerList = new ListElement();
for (ASTNode node : iterable) {
innerList.add(safeToYaml(node, fromRelation));
}
base.put(key, innerList);
}
syn MappingElement PrintConfig.toYaml(boolean fromRelation) {
MappingElement result = new MappingElement();
// children
addYamledList(result, "Headers", getHeaderList(), fromRelation);
// tokens
result.put("scale", getScale());
result.put("version", getVersion());
result.put("orderChildren", getOrderChildren());
// attributes
result.put("debug", debug());
return result;
}
syn MappingElement Header.toYaml(boolean fromRelation) {
MappingElement result = new MappingElement();
// tokens
result.put("value", getValue());
return result;
}
syn MappingElement DumpNode.toYaml(boolean fromRelation) {
MappingElement result = new MappingElement();
// children
if (!fromRelation) {
addYamledList(result, "DumpChildNodes", getDumpChildNodeList(), fromRelation);
addYamledList(result, "DumpTokens", getDumpTokenList(), fromRelation);
addYamledList(result, "DumpRelations", getDumpRelationList(), fromRelation);
}
// tokens
result.put("name", getName());
if (!fromRelation) {
result.put("computed", getComputed());
result.put("label", getLabel());
result.put("backgroundColor", getBackgroundColor());
result.put("textColor", getTextColor());
result.put("invisible", getInvisible());
}
// attributes
if (!fromRelation) {
result.put("isNull", isNull());
result.put("isAstNode", isAstNode());
result.put("labelAndTextColor", labelAndTextColor());
result.put("stereotypeList", stereotypeList());
addYamledList(result, "myChildren", myChildren(), true);
}