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 3c982cef83 CAUSEWAY-3404: adds GraphUtils (basic graph algorithms)
3c982cef83 is described below

commit 3c982cef831b5b0a63c02c8233bb0b8cc7f0e075
Author: Andi Huber <[email protected]>
AuthorDate: Fri Jan 19 17:41:41 2024 +0100

    CAUSEWAY-3404: adds GraphUtils (basic graph algorithms)
---
 .../services/metamodel/MetaModelService.java       |   4 +-
 .../services/metamodel/objgraph/ObjectGraph.java   |  85 +++++++++-
 .../objgraph/_ObjectGraphObjectModifier.java       |  43 ++---
 .../objgraph/_ObjectGraphRelationMerger.java       |  13 +-
 commons/src/main/java/module-info.java             |   2 +-
 .../apache/causeway/commons/graph/GraphUtils.java  | 182 +++++++++++++++++++++
 .../collections/_PrimitiveCollections.java         | 135 +++++++++++++++
 .../collections/_PrimitiveCollectionsTest.java     | 105 ++++++++++++
 .../domainobjects/EntityDiagramPageAbstract.java   |   6 +
 .../tooling/cli/projdoc/ProjectDocModel.java       |  22 +--
 .../d3js/ObjectGraphRendererEdgeListing.java       |  68 ++++++++
 11 files changed, 617 insertions(+), 48 deletions(-)

diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/MetaModelService.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/MetaModelService.java
index 01e1f56cfd..808a764f0b 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/MetaModelService.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/MetaModelService.java
@@ -23,8 +23,6 @@ import java.util.function.BiPredicate;
 
 import javax.inject.Named;
 
-import org.apache.causeway.commons.collections.Can;
-
 import org.springframework.lang.Nullable;
 
 import org.apache.causeway.applib.annotation.Action;
@@ -35,6 +33,7 @@ import org.apache.causeway.applib.id.LogicalType;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import 
org.apache.causeway.applib.services.commanddto.processor.CommandDtoProcessor;
 import org.apache.causeway.applib.services.metamodel.objgraph.ObjectGraph;
+import org.apache.causeway.commons.collections.Can;
 import org.apache.causeway.schema.metamodel.v2.MetamodelDto;
 
 import lombok.NonNull;
@@ -69,7 +68,6 @@ public interface MetaModelService {
      * </p>
      *
      * @param logicalType
-     * @return
      */
     Can<LogicalType> logicalTypeAndAliasesFor(final LogicalType logicalType);
 
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/ObjectGraph.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/ObjectGraph.java
index be43ee484e..4fe2b1c422 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/ObjectGraph.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/ObjectGraph.java
@@ -25,9 +25,16 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Optional;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
 
 import org.springframework.lang.Nullable;
 
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.collections.ImmutableEnumSet;
+import org.apache.causeway.commons.graph.GraphUtils;
+import org.apache.causeway.commons.graph.GraphUtils.GraphKernel;
+import 
org.apache.causeway.commons.graph.GraphUtils.GraphKernel.GraphCharacteristic;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.base._Strings.StringOperator;
 import org.apache.causeway.commons.internal.collections._Multimaps;
@@ -57,6 +64,16 @@ public class ObjectGraph {
         private final @With @NonNull Optional<String> stereotype;
         private final @With @NonNull Optional<String> description;
         private final @With List<ObjectGraph.Field> fields;
+        /** @return {@code packageName + "." + name} */
+        public String fqName() {
+            return packageName + "." + name;
+        }
+        public Object copy() {
+            return withFields(
+                    fields.stream()
+                        .map(Field::copy)
+                        .collect(Collectors.toCollection(ArrayList::new)));
+        }
     }
 
     @lombok.Value @Accessors(fluent=true)
@@ -65,6 +82,9 @@ public class ObjectGraph {
         private final @With @NonNull String elementTypeShortName;
         private final @With boolean isPlural;
         private final @With @NonNull Optional<String> description;
+        public Field copy() {
+            return withName(name);
+        }
     }
 
     @lombok.Value @Accessors(fluent=true)
@@ -86,6 +106,10 @@ public class ObjectGraph {
         public String descriptionFormatted() {
             return multiplicityNotation().apply(description);
         }
+        public Relation copy(final Map<String, ObjectGraph.Object> objectById) 
{
+            return withFrom(objectById.get(fromId()))
+                    .withTo(objectById.get(toId()));
+        }
     }
 
     public enum RelationType {
@@ -107,6 +131,10 @@ public class ObjectGraph {
     }
 
     public static interface Transformer {
+        /**
+         * If called from {@link ObjectGraph#transform(Transformer)},
+         * given {@link ObjectGraph objGraph} is a defensive copy, that can 
safely be mutated.
+         */
         ObjectGraph transform(ObjectGraph objGraph);
     }
 
@@ -117,6 +145,9 @@ public class ObjectGraph {
     public static class Transformers {
         @Getter(lazy = true)
         private final Transformer relationMerger = new 
_ObjectGraphRelationMerger();
+        public Transformer objectModifier(final @NonNull 
UnaryOperator<ObjectGraph.Object> modifier) {
+            return new _ObjectGraphObjectModifier(modifier);
+        }
     }
 
     public static interface Renderer {
@@ -130,9 +161,15 @@ public class ObjectGraph {
         return factory.create();
     }
 
+    /**
+     * Passes a (deep clone) copy of this {@link ObjectGraph} to given {@link 
Transformer}
+     * and returns a transformed {@link ObjectGraph}.
+     * <p>
+     * Hence transformers are not required to create defensive copies.
+     */
     public ObjectGraph transform(final @Nullable ObjectGraph.Transformer 
transfomer) {
         return transfomer!=null
-                ? transfomer.transform(this)
+                ? transfomer.transform(this.copy())
                 : this;
     }
 
@@ -162,6 +199,35 @@ public class ObjectGraph {
                 os.write(dsl.getBytes(StandardCharsets.UTF_8)));
     }
 
+    /**
+     * Returns a (deep clone) copy of this {@link ObjectGraph}.
+     */
+    public ObjectGraph copy() {
+        var copy = new ObjectGraph();
+        this.objects().stream()
+            .map(ObjectGraph.Object::copy)
+            .forEach(copy.objects()::add);
+        var copyiedObjectById = copy.objectById();
+        this.relations().stream()
+            .map(rel->rel.copy(copyiedObjectById))
+            .forEach(copy.relations()::add);
+        return copy;
+    }
+
+    /**
+     * Returns a {@link GraphKernel} of given characteristics.
+     */
+    public GraphKernel kernel(final @NonNull 
ImmutableEnumSet<GraphCharacteristic> characteristics) {
+        var kernel = new GraphUtils.GraphKernel(
+                objects().size(), characteristics);
+        relations().forEach(rel->{
+            kernel.addEdge(
+                    objects().indexOf(rel.from()),
+                    objects().indexOf(rel.to()));
+        });
+        return kernel;
+    }
+
     /**
      * Returns objects grouped by package (as list-multimap).
      */
@@ -182,4 +248,21 @@ public class ObjectGraph {
         return objectById;
     }
 
+    /**
+     * Returns a sub-graph comprised only of object nodes as picked per zero 
based indexes {@code int[]}.
+     */
+    public ObjectGraph subGraph(final int[] objectIndexes) {
+        var subGraph = this.transform(g->{
+            var subSet = 
Can.ofCollection(g.objects()).pickByIndex(objectIndexes);
+            g.objects().clear();
+            subSet.forEach(g.objects()::add);
+            var objectIds = g.objectById().keySet();
+            g.relations().removeIf(rel->
+                !(objectIds.contains(rel.fromId())
+                    || objectIds.contains(rel.fromId())));
+            return g;
+        });
+        return subGraph;
+    }
+
 }
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/graph/_Graph.java 
b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/_ObjectGraphObjectModifier.java
similarity index 54%
rename from 
commons/src/main/java/org/apache/causeway/commons/internal/graph/_Graph.java
rename to 
api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/_ObjectGraphObjectModifier.java
index cb7690a46b..e01375a9b5 100644
--- 
a/commons/src/main/java/org/apache/causeway/commons/internal/graph/_Graph.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/_ObjectGraphObjectModifier.java
@@ -16,38 +16,27 @@
  *  specific language governing permissions and limitations
  *  under the License.
  */
-package org.apache.causeway.commons.internal.graph;
+package org.apache.causeway.applib.services.metamodel.objgraph;
 
-import java.util.function.BiPredicate;
-import java.util.stream.Stream;
-
-import org.apache.causeway.commons.collections.Can;
+import java.util.Objects;
+import java.util.function.UnaryOperator;
+import java.util.stream.Collectors;
 
 import lombok.RequiredArgsConstructor;
 
-/**
- * <h1>- internal use only -</h1>
- * <p>
- * Adjacency Matrix
- * </p>
- * <p>
- * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this 
package! <br/>
- * These may be changed or removed without notice!
- * </p>
- *
- * @since 2.0
- */
-@RequiredArgsConstructor(staticName = "of")
-public class _Graph<T> {
+@RequiredArgsConstructor
+class _ObjectGraphObjectModifier implements ObjectGraph.Transformer {
 
-    private final Can<T> nodes;
-    private final BiPredicate<T, T> relationPredicate;
+    final UnaryOperator<ObjectGraph.Object> modifier;
 
-    public Stream<T> streamNeighbors(T a) {
-        return nodes.stream()
-        .filter(b->!a.equals(b))
-        .filter(b->relationPredicate.test(a, b));
+    @Override
+    public ObjectGraph transform(final ObjectGraph g) {
+        var modified = g.objects().stream()
+                .map(obj->Objects.requireNonNull(modifier.apply(obj),
+                        ()->"modifier returned null on non-null 
ObjectGraph.Object"))
+                .collect(Collectors.toList());
+        g.objects().clear();
+        g.objects().addAll(modified);
+        return g;
     }
-
-
 }
diff --git 
a/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/_ObjectGraphRelationMerger.java
 
b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/_ObjectGraphRelationMerger.java
index a947cd54f6..fc5164e6e4 100644
--- 
a/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/_ObjectGraphRelationMerger.java
+++ 
b/api/applib/src/main/java/org/apache/causeway/applib/services/metamodel/objgraph/_ObjectGraphRelationMerger.java
@@ -62,17 +62,20 @@ class _ObjectGraphRelationMerger implements 
ObjectGraph.Transformer {
         relations.removeIf(ObjectGraph.Relation::isAssociation);
 
         shared.forEach((key, list) -> {
-            if(list.size()<2) return;
-
+            if(list.isEmpty()) return;
+            var firstRel = list.get(0);
+            if(list.size()==1) {
+                relations.add(firstRel);
+                return;
+            }
             var mergedDescriptions = list.stream()
                     .map(rel->rel.descriptionFormatted())
                     .collect(Collectors.joining(","));
 
-            var a = list.get(0);
             var merged = new ObjectGraph.Relation(
                     ObjectGraph.RelationType.MERGED_ASSOCIATIONS,
-                    objectById.get(a.fromId()),
-                    objectById.get(a.toId()),
+                    objectById.get(firstRel.fromId()),
+                    objectById.get(firstRel.toId()),
                     mergedDescriptions, // already formatted honoring 
multiplicity notation
                     "", "");
             relations.add(merged);
diff --git a/commons/src/main/java/module-info.java 
b/commons/src/main/java/module-info.java
index 23ae09197f..87cba692e2 100644
--- a/commons/src/main/java/module-info.java
+++ b/commons/src/main/java/module-info.java
@@ -19,6 +19,7 @@
 module org.apache.causeway.commons {
     exports org.apache.causeway.commons.binding;
     exports org.apache.causeway.commons.collections;
+    exports org.apache.causeway.commons.graph;
     exports org.apache.causeway.commons.semantics;
     exports org.apache.causeway.commons.concurrent;
     exports org.apache.causeway.commons.functional;
@@ -43,7 +44,6 @@ module org.apache.causeway.commons {
     exports org.apache.causeway.commons.internal.exceptions;
     exports org.apache.causeway.commons.internal.factory;
     exports org.apache.causeway.commons.internal.functions;
-    exports org.apache.causeway.commons.internal.graph;
     exports org.apache.causeway.commons.internal.hardening;
     exports org.apache.causeway.commons.internal.hash;
     exports org.apache.causeway.commons.internal.html;
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/graph/GraphUtils.java 
b/commons/src/main/java/org/apache/causeway/commons/graph/GraphUtils.java
new file mode 100644
index 0000000000..ea22753173
--- /dev/null
+++ b/commons/src/main/java/org/apache/causeway/commons/graph/GraphUtils.java
@@ -0,0 +1,182 @@
+/*
+ *  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.commons.graph;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.BiPredicate;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.collections.ImmutableEnumSet;
+import 
org.apache.causeway.commons.graph.GraphUtils.GraphKernel.GraphCharacteristic;
+import org.apache.causeway.commons.internal.assertions._Assert;
+import 
org.apache.causeway.commons.internal.collections._PrimitiveCollections.IntList;
+
+import lombok.Getter;
+import lombok.NonNull;
+import lombok.experimental.Accessors;
+import lombok.experimental.UtilityClass;
+
+/**
+ * Early draft.
+ */
+@UtilityClass
+public class GraphUtils {
+
+    // -- FACTORIES
+
+    public <T> GraphKernel kernelForAdjacency(final Can<T> nodes, final 
BiPredicate<T, T> adjaciency) {
+        var kernel = new GraphKernel(nodes.size(), 
ImmutableEnumSet.noneOf(GraphCharacteristic.class));
+        final int m = nodes.size()-1;
+        final int n = nodes.size();
+        for (int i = 0; i < m; i++) {
+            var a = nodes.getElseFail(i);
+            for (int j = i; j < n; j++) {
+                var b = nodes.getElseFail(j);
+                if(adjaciency.test(a, b)) {
+                    kernel.addEdge(i, j);
+                }
+                if(adjaciency.test(b, a)) {
+                    kernel.addEdge(j, i);
+                }
+            }
+        }
+        return kernel;
+    }
+
+    // -- GRAPH KERNEL
+
+    /** no multi edge support */
+    public final static class GraphKernel {
+
+        public enum GraphCharacteristic {
+            UNDIRECTED
+            //NO_LOOPS,
+            //NO_MULTI_EDGES,
+            //WEIGHTED,
+            ;
+        }
+
+        private final ImmutableEnumSet<GraphCharacteristic> characteristics;
+        @Getter @Accessors(fluent=true)
+        private final int nodeCount;
+        private final List<IntList> adjacencyList;
+        public GraphKernel(
+                final int nodeCount,
+                final @NonNull ImmutableEnumSet<GraphCharacteristic> 
characteristics) {
+            this.characteristics = characteristics;
+            this.nodeCount = nodeCount;
+            this.adjacencyList = new ArrayList<>(nodeCount);
+            for (int i = 0; i < nodeCount; i++) {
+                adjacencyList.add(new IntList());
+            }
+        }
+        public boolean isUndirected() {
+            return characteristics.contains(GraphCharacteristic.UNDIRECTED);
+        }
+        public void addEdge(final int u, final int v) {
+            boundsCheck(u);
+            boundsCheck(v);
+            adjacencyList.get(u).addUnique(v);
+            if(isUndirected()) {
+                adjacencyList.get(v).addUnique(u);
+            }
+        }
+        public IntStream streamNeighbors(final int nodeIndex) {
+            boundsCheck(nodeIndex);
+            return adjacencyList.get(nodeIndex).stream();
+        }
+        public GraphKernel copy() {
+            var copy = new GraphKernel(nodeCount, characteristics);
+            for (int u = 0; u < nodeCount; u++) {
+                for (int v : adjacencyList.get(u)) {
+                    copy.addEdge(u, v);
+                }
+            }
+            return copy;
+        }
+        public GraphKernel toUndirected() {
+            var copy = new GraphKernel(nodeCount, 
characteristics.add(GraphCharacteristic.UNDIRECTED));
+            for (int u = 0; u < nodeCount; u++) {
+                for (int v : adjacencyList.get(u)) {
+                    copy.addEdge(u, v);
+                    copy.addEdge(v, u);
+                }
+            }
+            return copy;
+        }
+        /**
+         * Returns a list of {@code int[]},
+         * where each list entry contains the zero based indexes of weakly 
connected graph nodes.
+         */
+        public List<int[]> findWeaklyConnectedNodes() {
+            var undirectedGraph = this.isUndirected()
+                    ? this
+                    : this.toUndirected();
+            var adjacencyList = new 
WeaklyConnectedNodesFinder().find(undirectedGraph);
+            return adjacencyList.stream()
+                .filter(IntList::isNotEmpty)
+                .map(IntList::toArray)
+                .collect(Collectors.toList());
+        }
+
+        // -- HELPER
+
+        private void boundsCheck(final int nodeIndex) {
+            if(nodeIndex<0
+                    || nodeIndex>=nodeCount) {
+                throw new IndexOutOfBoundsException(nodeIndex);
+            }
+        }
+
+        private class WeaklyConnectedNodesFinder {
+            List<IntList> find(final GraphKernel undirectedGraph) {
+                _Assert.assertTrue(undirectedGraph.isUndirected());
+                var connectedComponents = new ArrayList<IntList>();
+                final boolean[] isVisited = new 
boolean[undirectedGraph.nodeCount()];
+                for (int i = 0; i < undirectedGraph.nodeCount(); i++) {
+                    if (!isVisited[i]) {
+                        var component = new IntList();
+                        findConnectedComponent(i, isVisited, component, 
undirectedGraph);
+                        connectedComponents.add(component);
+                    }
+                }
+                return connectedComponents;
+            }
+            // finds a connected component starting from source using DFS
+            private void findConnectedComponent(
+                    final int src,
+                    final boolean[] isVisited,
+                    final IntList component,
+                    final GraphKernel undirectedGraph) {
+                isVisited[src] = true;
+                component.add(src);
+                for (int v : undirectedGraph.adjacencyList.get(src)) {
+                    if (!isVisited[v]) {
+                        findConnectedComponent(v, isVisited, component, 
undirectedGraph);
+                    }
+                }
+            }
+        }
+
+    }
+
+}
diff --git 
a/commons/src/main/java/org/apache/causeway/commons/internal/collections/_PrimitiveCollections.java
 
b/commons/src/main/java/org/apache/causeway/commons/internal/collections/_PrimitiveCollections.java
new file mode 100644
index 0000000000..c39a4a22be
--- /dev/null
+++ 
b/commons/src/main/java/org/apache/causeway/commons/internal/collections/_PrimitiveCollections.java
@@ -0,0 +1,135 @@
+/*
+ *  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.commons.internal.collections;
+
+import java.util.Iterator;
+import java.util.OptionalInt;
+import java.util.PrimitiveIterator;
+import java.util.stream.IntStream;
+
+import lombok.experimental.UtilityClass;
+
+/**
+ * <h1>- internal use only -</h1>
+ * Primitive int list implementation.
+ * <p>
+ * <b>WARNING</b>: Do <b>NOT</b> use any of the classes provided by this 
package! <br/>
+ * These may be changed or removed without notice!
+ *
+ * @since 2.0
+ */
+@UtilityClass
+public class _PrimitiveCollections {
+
+    /**
+     * Primitive int list implementation. Can also operate as a set,
+     * if {@link IntList#addUnique(int)} is used over {@link IntList#add(int)}.
+     * @implNote not thread-safe
+     */
+    public static class IntList implements Iterable<Integer> {
+
+        private static final int DEFAULT_INITIAL_CAPACITY = 8;
+
+        private int[] buf;
+        private int size = 0;
+
+        public int size() {
+            return size;
+        }
+
+        public boolean isEmpty() {
+            return size==0;
+        }
+        public boolean isNotEmpty() {
+            return size!=0;
+        }
+
+        public IntList addUnique(final int v) {
+            if(!contains(v)) return add(v);
+            return this;
+        }
+
+        public IntList add(final int v) {
+            if(buf==null) {
+                this.buf = new int[DEFAULT_INITIAL_CAPACITY];
+            } else if(size==buf.length) {
+                var old = buf;
+                this.buf = new int[buf.length * 2];
+                System.arraycopy(old, 0, buf, 0, size);
+            }
+            buf[size++] = v;
+            return this;
+        }
+
+        public int get(final int index) {
+            if(index<0
+                    || index>=size) {
+                throw new IndexOutOfBoundsException(index);
+            }
+            return buf[index];
+        }
+
+        public OptionalInt indexOf(final int v) {
+            if(isEmpty()) return OptionalInt.empty();
+            for (int i = 0; i < size; i++) {
+                if(buf[i]==v) return OptionalInt.of(i);
+            }
+            return OptionalInt.empty();
+        }
+
+        public boolean contains(final int v) {
+            if(isEmpty()) return false;
+            for (int i = 0; i < size; i++) {
+                if(buf[i]==v) return true;
+            }
+            return false;
+        }
+
+        public IntStream stream() {
+            return IntStream.of(toArray());
+        }
+
+        /**
+         * @return a new array containing all the int(s) of this list
+         */
+        public int[] toArray() {
+            var result = new int[size];
+            if(!isEmpty()) {
+                System.arraycopy(buf, 0, result, 0, size);
+            }
+            return result;
+        }
+
+        @Override
+        public Iterator<Integer> iterator() {
+            var defensiveCopy = toArray();
+            return new PrimitiveIterator.OfInt() {
+                int index = 0;
+                @Override public boolean hasNext() {
+                    return index<defensiveCopy.length;
+                }
+                @Override public int nextInt() {
+                    return defensiveCopy[index++];
+                }
+            };
+        }
+
+    }
+
+}
diff --git 
a/commons/src/test/java/org/apache/causeway/commons/internal/collections/_PrimitiveCollectionsTest.java
 
b/commons/src/test/java/org/apache/causeway/commons/internal/collections/_PrimitiveCollectionsTest.java
new file mode 100644
index 0000000000..8d0e26dbfa
--- /dev/null
+++ 
b/commons/src/test/java/org/apache/causeway/commons/internal/collections/_PrimitiveCollectionsTest.java
@@ -0,0 +1,105 @@
+/*
+ *  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.commons.internal.collections;
+
+import java.util.stream.IntStream;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import 
org.apache.causeway.commons.internal.collections._PrimitiveCollections.IntList;
+
+class _PrimitiveCollectionsTest {
+
+    @Test
+    void emptyList() {
+
+        var intList = new IntList();
+        assertEquals(0, intList.size());
+        assertTrue(intList.isEmpty());
+        assertNotNull(intList.toArray());
+        assertEquals(0, intList.toArray().length);
+        assertThrows(IndexOutOfBoundsException.class, ()->intList.get(0));
+        assertEquals(0L, intList.stream().count());
+    }
+
+    @Test
+    void oneElementList() {
+        var intList = new IntList();
+        intList.add(5);
+
+        assertEquals(1, intList.size());
+        assertFalse(intList.isEmpty());
+        assertNotNull(intList.toArray());
+        assertEquals(1, intList.toArray().length);
+        assertEquals(5, intList.get(0));
+        assertThrows(IndexOutOfBoundsException.class, ()->intList.get(1));
+        assertEquals(1L, intList.stream().count());
+        assertEquals(5, intList.stream().sum());
+    }
+
+    @Test
+    void manyElementList() {
+        var intList = new IntList();
+        IntStream.range(0, 100)
+            .forEach(i->intList.add(i + 100));
+
+        assertEquals(100, intList.size());
+        assertFalse(intList.isEmpty());
+        assertNotNull(intList.toArray());
+        assertEquals(100, intList.toArray().length);
+        assertEquals(100, intList.get(0));
+        assertEquals(199, intList.get(99));
+        assertEquals(99, intList.indexOf(199).orElse(-1));
+        assertThrows(IndexOutOfBoundsException.class, ()->intList.get(100));
+        assertEquals(100L, intList.stream().count());
+        assertEquals(14950, intList.stream().sum());
+    }
+
+    @Test
+    void setSemantics() {
+        var intList = new IntList();
+        intList.addUnique(5);
+        intList.addUnique(4);
+        intList.addUnique(7);
+        intList.addUnique(4);
+
+        assertEquals(3, intList.size());
+        assertFalse(intList.isEmpty());
+        assertNotNull(intList.toArray());
+        assertEquals(3, intList.toArray().length);
+        assertEquals(5, intList.get(0));
+        assertThrows(IndexOutOfBoundsException.class, ()->intList.get(3));
+        assertEquals(3L, intList.stream().count());
+        assertEquals(5+4+7, intList.stream().sum());
+
+        assertTrue(intList.contains(4));
+        assertTrue(intList.contains(7));
+        assertFalse(intList.contains(2));
+
+        assertEquals(2, intList.indexOf(7).orElse(-1));
+    }
+
+
+}
diff --git 
a/extensions/core/docgen/help/src/main/java/org/apache/causeway/extensions/docgen/help/topics/domainobjects/EntityDiagramPageAbstract.java
 
b/extensions/core/docgen/help/src/main/java/org/apache/causeway/extensions/docgen/help/topics/domainobjects/EntityDiagramPageAbstract.java
index 0a361e7d51..d6bea8ba9f 100644
--- 
a/extensions/core/docgen/help/src/main/java/org/apache/causeway/extensions/docgen/help/topics/domainobjects/EntityDiagramPageAbstract.java
+++ 
b/extensions/core/docgen/help/src/main/java/org/apache/causeway/extensions/docgen/help/topics/domainobjects/EntityDiagramPageAbstract.java
@@ -35,6 +35,7 @@ import 
org.apache.causeway.valuetypes.asciidoc.builder.AsciiDocBuilder;
 import org.apache.causeway.valuetypes.asciidoc.builder.AsciiDocFactory;
 import 
org.apache.causeway.valuetypes.asciidoc.builder.objgraph.d3js.ObjectGraphRendererD3js;
 import 
org.apache.causeway.valuetypes.asciidoc.builder.objgraph.d3js.ObjectGraphRendererD3js.GraphRenderOptions;
+import 
org.apache.causeway.valuetypes.asciidoc.builder.objgraph.d3js.ObjectGraphRendererEdgeListing;
 import 
org.apache.causeway.valuetypes.asciidoc.builder.objgraph.plantuml.ObjectGraphRendererPlantuml;
 
 import lombok.RequiredArgsConstructor;
@@ -89,6 +90,11 @@ public abstract class EntityDiagramPageAbstract implements 
HelpPage {
                 new 
ObjectGraphRendererD3js(GraphRenderOptions.builder().build()));
         return new AsciiDocBuilder()
                 .append(doc->AsciiDocFactory.htmlPassthroughBlock(doc, 
d3jsSourceAsHtml))
+                .append(doc->{
+                    var source = objectGraph.render(new 
ObjectGraphRendererEdgeListing());
+                    var sourceBlock = AsciiDocFactory.sourceBlock(doc, "txt", 
source);
+                    sourceBlock.setTitle("Edge Listing");
+                })
                 .buildAsString();
     }
 
diff --git 
a/tooling/cli/src/main/java/org/apache/causeway/tooling/cli/projdoc/ProjectDocModel.java
 
b/tooling/cli/src/main/java/org/apache/causeway/tooling/cli/projdoc/ProjectDocModel.java
index 04edb71a3a..8e3da39f48 100644
--- 
a/tooling/cli/src/main/java/org/apache/causeway/tooling/cli/projdoc/ProjectDocModel.java
+++ 
b/tooling/cli/src/main/java/org/apache/causeway/tooling/cli/projdoc/ProjectDocModel.java
@@ -43,9 +43,10 @@ import org.asciidoctor.ast.Document;
 import org.springframework.lang.Nullable;
 
 import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.commons.functional.IndexedConsumer;
+import org.apache.causeway.commons.graph.GraphUtils;
 import org.apache.causeway.commons.internal.base._Strings;
 import org.apache.causeway.commons.internal.exceptions._Exceptions;
-import org.apache.causeway.commons.internal.graph._Graph;
 import org.apache.causeway.commons.io.FileUtils;
 import org.apache.causeway.tooling.c4.C4;
 import org.apache.causeway.tooling.cli.CliConfig;
@@ -232,7 +233,6 @@ public class ProjectDocModel {
             projectNodes.add(module);
         }
 
-        //XXX lombok issues, not using val here
         public String toPlantUml(final String softwareSystemName) {
 
             val softwareSystem = c4.softwareSystem(softwareSystemName, null);
@@ -246,16 +246,16 @@ public class ProjectDocModel {
                 return ProjectAndContainerTuple.of(projectNode, container);
             });
 
+            var adjMatrix = GraphUtils.kernelForAdjacency(tuples,
+                    (a, 
b)->a.projectNode.getChildren().contains(b.projectNode));
 
-            final _Graph<ProjectAndContainerTuple> adjMatrix =
-                    _Graph.of(tuples, (a, 
b)->a.projectNode.getChildren().contains(b.projectNode));
-
-            tuples.forEach(tuple->{
-                adjMatrix.streamNeighbors(tuple)
-                .forEach(dependentTuple->{
-                    tuple.container.uses(dependentTuple.container, "");
-                });
-            });
+            tuples.forEach(IndexedConsumer.zeroBased((i, tuple)->{
+                adjMatrix.streamNeighbors(i)
+                    .mapToObj(tuples::getElseFail)
+                    .forEach(dependentTuple->{
+                        tuple.container.uses(dependentTuple.container, "");
+                    });
+            }));
 
             val containerView = c4.getViewSet()
                     .createContainerView(softwareSystem, 
c4.getWorkspaceName(), "Artifact Hierarchy (Maven)");
diff --git 
a/valuetypes/asciidoc/builder/src/main/java/org/apache/causeway/valuetypes/asciidoc/builder/objgraph/d3js/ObjectGraphRendererEdgeListing.java
 
b/valuetypes/asciidoc/builder/src/main/java/org/apache/causeway/valuetypes/asciidoc/builder/objgraph/d3js/ObjectGraphRendererEdgeListing.java
new file mode 100644
index 0000000000..5755940013
--- /dev/null
+++ 
b/valuetypes/asciidoc/builder/src/main/java/org/apache/causeway/valuetypes/asciidoc/builder/objgraph/d3js/ObjectGraphRendererEdgeListing.java
@@ -0,0 +1,68 @@
+/*
+ *  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.valuetypes.asciidoc.builder.objgraph.d3js;
+
+import java.util.stream.Collectors;
+
+import org.apache.causeway.applib.services.metamodel.objgraph.ObjectGraph;
+import org.apache.causeway.commons.collections.ImmutableEnumSet;
+import org.apache.causeway.commons.functional.IndexedConsumer;
+import org.apache.causeway.commons.graph.GraphUtils.GraphKernel;
+
+import lombok.RequiredArgsConstructor;
+
+@RequiredArgsConstructor
+public class ObjectGraphRendererEdgeListing implements ObjectGraph.Renderer {
+
+    @Override
+    public void render(final StringBuilder sb, final ObjectGraph objGraph) {
+
+        /*
+         * List of {@code int[]},
+         * where each array contains to the zero based indexes of weakly 
connected graph nodes (objects).
+         */
+        var listOfWeaklyConnectedNodes = 
objGraph.kernel(ImmutableEnumSet.of(GraphKernel.GraphCharacteristic.UNDIRECTED))
+            .findWeaklyConnectedNodes();
+
+        var listOfWeaklyConnectedEdges = listOfWeaklyConnectedNodes.stream()
+            
.map(connectedObjectIndexes->renderConnectedSubGraph(objGraph.subGraph(connectedObjectIndexes)))
+            .collect(Collectors.joining(",\n"));
+        sb.append(listOfWeaklyConnectedEdges);
+    }
+
+    private String renderConnectedSubGraph(final ObjectGraph objGraph) {
+        final int maxRelIndex = objGraph.relations().size() - 1;
+
+        var sb = new StringBuilder();
+        sb.append("{");
+
+        objGraph.relations().forEach(IndexedConsumer.zeroBased((i, rel)->{
+            var fromId = rel.from().id();
+            var toId = rel.to().id();
+            sb.append(String.format("%s->%s", fromId, toId));
+            if(i<maxRelIndex) sb.append(",");
+            if(i%8==7) sb.append("\n");
+        }));
+
+        sb.append("}").append("\n");
+
+        return sb.toString();
+    }
+
+}
\ No newline at end of file


Reply via email to