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;
+    }
 
 }