You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@causeway.apache.org by ah...@apache.org on 2023/01/27 15:07:14 UTC
[causeway] branch master updated: CAUSEWAY-3344: adds relations to the Entity Relation Diagram
This is an automated email from the ASF dual-hosted git repository.
ahuber pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/causeway.git
The following commit(s) were added to refs/heads/master by this push:
new 1c280bf3c9 CAUSEWAY-3344: adds relations to the Entity Relation Diagram
1c280bf3c9 is described below
commit 1c280bf3c9bd2691e2757e615217d7545323b3bd
Author: Andi Huber <ah...@apache.org>
AuthorDate: Fri Jan 27 16:07:09 2023 +0100
CAUSEWAY-3344: adds relations to the Entity Relation Diagram
---
.../topics/domainobjects/EntityDiagramPage.java | 18 +-
.../docgen/topics/domainobjects/ObjectGraph.java | 289 +++++++++++++++++++++
.../docgen/topics/domainobjects/_DiagramUtils.java | 37 ++-
3 files changed, 319 insertions(+), 25 deletions(-)
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/EntityDiagramPage.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/EntityDiagramPage.java
index 8cbffe302c..e9ec6c1924 100644
--- a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/EntityDiagramPage.java
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/EntityDiagramPage.java
@@ -19,7 +19,6 @@
package org.apache.causeway.extensions.docgen.topics.domainobjects;
import java.util.Optional;
-import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.inject.Inject;
@@ -35,6 +34,7 @@ import org.apache.causeway.extensions.docgen.applib.HelpPage;
import org.apache.causeway.valuetypes.asciidoc.applib.value.AsciiDoc;
import lombok.RequiredArgsConstructor;
+import lombok.val;
@Component
@Named(CausewayModuleExtDocgen.NAMESPACE + ".EntityDiagramPage")
@@ -56,14 +56,20 @@ public class EntityDiagramPage implements HelpPage {
+ _DiagramUtils.plantumlBlock(entityTypesAsDiagram()));
}
+ /** governs which entities to include */
+ protected boolean accept(final ObjectSpecification objSpec) {
+ // exclude demo entities, so we don't overflow kroki.io limits in the causeway demo app
+ return !"demo".equals(objSpec.getLogicalType().getNamespace());
+ }
+
// -- HELPER
private String entityTypesAsDiagram() {
- return streamEntityTypes()
- .map(spec->_DiagramUtils.object(spec))
- .collect(Collectors.joining("\n"));
-
- //TODO add entity relations - that is, model the object graph
+ val objectGraph = new ObjectGraph();
+ streamEntityTypes()
+ .filter(this::accept)
+ .forEach(objSpec->objectGraph.registerObject(objSpec));
+ return objectGraph.render();
}
private Stream<ObjectSpecification> streamEntityTypes() {
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/ObjectGraph.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/ObjectGraph.java
new file mode 100644
index 0000000000..d33badb532
--- /dev/null
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/ObjectGraph.java
@@ -0,0 +1,289 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.causeway.extensions.docgen.topics.domainobjects;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import org.apache.causeway.applib.id.LogicalType;
+import org.apache.causeway.commons.internal.base._Refs;
+import org.apache.causeway.commons.internal.collections._Multimaps;
+import org.apache.causeway.commons.internal.collections._Multimaps.ListMultimap;
+import org.apache.causeway.commons.internal.collections._Sets;
+import org.apache.causeway.commons.internal.exceptions._Exceptions;
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
+import org.apache.causeway.core.metamodel.spec.feature.ObjectAssociation;
+
+import static org.apache.causeway.extensions.docgen.topics.domainobjects._DiagramUtils.doubleQuoted;
+
+import lombok.Value;
+import lombok.val;
+
+class ObjectGraph {
+
+ // -- FACTORIES
+
+ public static ObjectGraph.Object object(final String id, final ObjectSpecification objSpec) {
+ return new ObjectGraph.Object(id,
+ objSpec.getLogicalType().getNamespace(),
+ objSpec.getLogicalType().getLogicalTypeSimpleName(),
+ objSpec.isAbstract()
+ ? Optional.of("abstract")
+ : Optional.empty());
+ }
+
+ // -- GRAPH
+
+ @Value
+ private static class Context {
+
+ private final ListMultimap<String, LogicalType> logicalTypesByNamespace = _Multimaps.newListMultimap();
+ private final Map<LogicalType, ObjectGraph.Object> objectByLogicalType = new HashMap<>();
+ private final List<ObjectGraph.Relation> relations = new ArrayList<>();
+
+ public ObjectGraph.Object registerObject(final ObjectSpecification objSpec) {
+
+ val addFieldsLater = _Refs.booleanRef(false);
+
+ val obj = objectByLogicalType.computeIfAbsent(objSpec.getLogicalType(), logicalType->{
+ logicalTypesByNamespace.putElement(logicalType.getNamespace(), logicalType);
+ val newObjId = "o" + objectByLogicalType.size();
+ val newObj = object(newObjId, objSpec);
+ addFieldsLater.setValue(true);
+ return newObj;
+ });
+
+ if(addFieldsLater.isTrue()) {
+ objSpec.streamAssociations(MixedIn.EXCLUDED)
+ .peek(ass->{
+ val elementType = ass.getElementType();
+ if(elementType.isEntity()
+ || elementType.isAbstract()) {
+ val referencedObj = registerObject(elementType);
+
+ val thisCls = objSpec.getLogicalType().getCorrespondingClass();
+ val refCls = elementType.getLogicalType().getCorrespondingClass();
+ if(thisCls.equals(refCls)
+ || !refCls.isAssignableFrom(thisCls)) {
+ // we found a 1-x relation
+ registerRelation(
+ ass.isOneToOneAssociation()
+ ? ObjectGraph.Relation.RelationType.ONE_TO_ONE
+ : ObjectGraph.Relation.RelationType.ONE_TO_MANY,
+ obj.id, referencedObj.id, ass.getId());
+ }
+
+ }
+ })
+ .map(ObjectGraph.Object.Field::forAss)
+ .forEach(obj.getFields()::add);
+ }
+ return obj;
+ }
+
+ public ObjectGraph.Relation registerRelation(
+ final ObjectGraph.Relation.RelationType relationType,
+ final String fromId,
+ final String toId,
+ final String label) {
+ val relation = new ObjectGraph.Relation(relationType, fromId, toId, label);
+ relations.add(relation);
+ return relation;
+ }
+
+ public void createInheritanceRelations() {
+
+ final Set<ObjectGraph.Relation> inheritanceRelations = new HashSet<>();
+ final Set<ObjectGraph.Relation> markedForRemoval = new HashSet<>();
+
+ for(val e1 : objectByLogicalType.entrySet()) {
+ for(val e2 : objectByLogicalType.entrySet()) {
+ val type1 = e1.getKey();
+ val type2 = e2.getKey();
+ if(type1.equals(type2)) continue;
+ val cls1 = type1.getCorrespondingClass();
+ val cls2 = type2.getCorrespondingClass();
+ if(cls2.isAssignableFrom(cls1)) {
+ val o1 = e1.getValue();
+ val o2 = e2.getValue();
+ // we found an inheritance relation
+ val relation = new ObjectGraph.Relation(
+ ObjectGraph.Relation.RelationType.INHERITANCE,
+ o1.id, o2.id, "");
+ inheritanceRelations.add(relation);
+ }
+ }
+ }
+
+ // remove any inheritance relations that shortcut others
+ outer:
+ for(val r1 : inheritanceRelations) {
+ for(val r2 : inheritanceRelations) {
+ if(r1==r2) continue;
+ if(!r1.fromId.equals(r2.fromId)) continue;
+ for(val r3 : inheritanceRelations) {
+ if(r1==r3) continue;
+ if(r2==r3) continue;
+ if(!r1.toId.equals(r3.toId)) continue;
+ if(!r2.toId.equals(r3.fromId)) continue;
+
+ /*
+ * If there exists a non-direct path from [r1.from] to [r1.to],
+ * than r1 needs to be marked for removal.
+ *
+ * It is sufficient to check for paths of length 2 specifically.
+ *
+ * Such a path is found, if following condition is true
+ * [r1.from] == [r2.from]
+ * && [r1.to] == [r3.to]
+ * && [r2.to] == [r3.from].
+ */
+
+ markedForRemoval.add(r1);
+ continue outer;
+ }
+ }
+
+ }
+
+ relations.addAll(_Sets.minus(inheritanceRelations, markedForRemoval));
+ }
+
+ }
+
+ @Value
+ private static class Relation {
+ public static enum RelationType {
+ ONE_TO_ONE,
+ ONE_TO_MANY,
+ INHERITANCE,
+ }
+ private final RelationType relationType;
+ private final String fromId;
+ private final String toId;
+ private final String label;
+ public String render() {
+ switch(relationType) {
+ case ONE_TO_ONE:
+ return String.format("%s -> %s : %s", fromId, toId, label);
+ case ONE_TO_MANY:
+ return String.format("%s -> %s : [%s]", fromId, toId, label);
+ case INHERITANCE:
+ return String.format("%s --|> %s", fromId, toId);
+ }
+ throw _Exceptions.unmatchedCase(relationType);
+ }
+ }
+
+ @Value
+ public static class Object {
+
+ @Value
+ public static class Field {
+
+ public static Field forAss(final ObjectAssociation ass) {
+ return new Field(ass.getId(), _DiagramUtils.objectShortName(ass.getElementType()), ass.isOneToManyAssociation());
+ }
+
+ private final String name;
+ private final String elementTypeShortName;
+ private final boolean isPlural;
+
+ String render() {
+ return isPlural()
+ ? String.format("%s: [%s]", name, elementTypeShortName)
+ : String.format("%s: %s", name, elementTypeShortName);
+ }
+ }
+
+ private final String id;
+ private final String packageName;
+ private final String name;
+ private final Optional<String> stereotype;
+ private final List<ObjectGraph.Object.Field> fields = new ArrayList<>();
+
+ String render() {
+ val sb = new StringBuilder()
+ .append(String.format("object %s as %s",
+ doubleQuoted(name),
+ stereotype
+ .map(stp->String.format("%s <<%s>>", id, stp)).orElse(id)))
+ .append('\n');
+
+ fields.forEach(field->{
+ sb.append(id + " : " + field.render()).append('\n');
+ });
+ return sb.toString();
+ }
+ }
+
+ // -- IMPL
+
+ private final ObjectGraph.Context context = new ObjectGraph.Context();
+
+ public ObjectGraph.Object registerObject(final ObjectSpecification objSpec) {
+ return context.registerObject(objSpec);
+ }
+
+ public String render() {
+
+ val sb = new StringBuilder();
+
+ sb.append("left to right direction\n"); // arranges packages vertically
+
+ // group objects by package
+ context.logicalTypesByNamespace.forEach((namespace, logicalTypes)->{
+
+ // package start
+ sb.append("package ").append(namespace).append(" {\n");
+
+ logicalTypes.stream().map(context.objectByLogicalType::get)
+ .map(ObjectGraph.Object::render)
+ .forEach(s->sb.append(s).append('\n'));
+
+ // package end
+ sb.append("}\n");
+
+ });
+
+ context.createInheritanceRelations();
+
+ context.getRelations().stream()
+ .map(ObjectGraph.Relation::render)
+ .forEach(s->sb.append(s).append('\n'));
+
+ val plantuml = sb.toString();
+
+ //debug
+// System.err.println("--------PLANTUML------");
+// System.err.printf("%s%n", plantuml);
+// System.err.println("----------------------");
+
+ return plantuml;
+ }
+
+
+
+}
diff --git a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/_DiagramUtils.java b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/_DiagramUtils.java
index e8f75d5453..43a5b00adc 100644
--- a/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/_DiagramUtils.java
+++ b/extensions/core/docgen/src/main/java/org/apache/causeway/extensions/docgen/topics/domainobjects/_DiagramUtils.java
@@ -18,11 +18,10 @@
*/
package org.apache.causeway.extensions.docgen.topics.domainobjects;
-import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
-import org.apache.causeway.commons.collections.Can;
import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
-import org.apache.causeway.core.metamodel.spec.feature.MixedIn;
import lombok.val;
import lombok.experimental.UtilityClass;
@@ -37,27 +36,27 @@ class _DiagramUtils {
+ "--\n";
}
- String object(final String name, final List<String> fields) {
- val sb = new StringBuilder();
-
- sb.append("object " + name).append('\n');
-
- fields.forEach(field->{
- sb.append(name + " : " + field).append('\n');
- });
-
- return sb.toString();
+ String multilineLabel(final String...lines) {
+ return Stream.of(lines).collect(Collectors.joining("\\n"));
}
- String object(final ObjectSpecification objSpec) {
+ String doubleQuoted(final String string) {
+ return "\"" + string + "\"";
+ }
- val props = objSpec.streamProperties(MixedIn.EXCLUDED)
- .collect(Can.toCan());
- val fields = props.map(prop->prop.getId())
- .toList();
+ String objectId(final ObjectSpecification objSpec) {
+ return objSpec.getLogicalType().getLogicalTypeName();
+ }
- return object(objSpec.getLogicalType().getLogicalTypeSimpleName(), fields);
+ String objectName(final ObjectSpecification objSpec) {
+ return multilineLabel(
+ objectShortName(objSpec),
+ "<" + objSpec.getLogicalType().getNamespace() + ">");
}
+ String objectShortName(final ObjectSpecification objSpec) {
+ val simpleName = objSpec.getLogicalType().getLogicalTypeSimpleName();
+ return simpleName;
+ }
}