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