diff --git a/praxiscore-api/src/main/java/org/praxislive/core/Connection.java b/praxiscore-api/src/main/java/org/praxislive/core/Connection.java index 51750e7c..6372bf8f 100644 --- a/praxiscore-api/src/main/java/org/praxislive/core/Connection.java +++ b/praxiscore-api/src/main/java/org/praxislive/core/Connection.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2023 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -21,99 +21,102 @@ */ package org.praxislive.core; -import org.praxislive.core.protocols.ContainerProtocol; +import java.util.Optional; import org.praxislive.core.types.PArray; import org.praxislive.core.types.PString; /** * A type representing a connection between two ports. */ -public final class Connection { - - private final PArray dataArray; +public final class Connection extends PArray.ArrayBasedValue { /** - * Create a connection reference. The child IDs must be valid according to - * {@link ComponentAddress#isValidID(java.lang.String)}. The port IDs must - * be valid according to {@link PortAddress#isValidID(java.lang.String)}. - * - * @param child1 ID of first child - * @param port1 ID of port on first child - * @param child2 ID of second child - * @param port2 ID of port on second child - * @throws IllegalArgumentException if the IDs are not valid + * Value type name. */ - public Connection(String child1, String port1, String child2, String port2) { - verifyChildID(child1); - verifyChildID(child2); - verifyPortID(port1); - verifyPortID(port2); - dataArray = PArray.of(PString.of(child1), PString.of(port1), - PString.of(child2), PString.of(port2)); + public static final String TYPE_NAME = "Connection"; + + private Connection(PArray data) { + super(data); + if (data.size() != 4) { + throw new IllegalArgumentException("Invalid connection data"); + } + verifyChildID(data.get(0).toString()); + verifyPortID(data.get(1).toString()); + verifyChildID(data.get(2).toString()); + verifyPortID(data.get(3).toString()); } /** - * Query the component ID of the first connected component. + * Query the component ID of the source component. * * @return ID of first child */ - public String child1() { - return dataArray.get(0).toString(); + public String sourceComponent() { + return dataArray().get(0).toString(); } /** - * Query the port ID of the connected port on the first component. + * Query the port ID of the source port. * * @return ID of port on first child */ - public String port1() { - return dataArray.get(1).toString(); + public String sourcePort() { + return dataArray().get(1).toString(); } /** - * Query the component ID of the second connected component. + * Query the component ID of the target component. * * @return ID of the second child */ - public String child2() { - return dataArray.get(2).toString(); + public String targetComponent() { + return dataArray().get(2).toString(); } /** - * Query the port ID of the connected port on the second component. + * Query the port ID of the target port. * * @return ID of port on second child */ - public String port2() { - return dataArray.get(3).toString(); + public String targetPort() { + return dataArray().get(3).toString(); } /** - * Access the Connection as the backing PArray data. The data consists of - * four values, {@code child1 port1 child2 port2}. - *

- * This is the same format included in the list returned from - * {@link ContainerProtocol#CONNECTIONS}. + * Create a connection reference. The child IDs must be valid according to + * {@link ComponentAddress#isValidID(java.lang.String)}. The port IDs must + * be valid according to {@link PortAddress#isValidID(java.lang.String)}. * - * @return backing data array + * @param child1 ID of first child + * @param port1 ID of port on first child + * @param child2 ID of second child + * @param port2 ID of port on second child + * @return new connection + * @throws IllegalArgumentException if the IDs are not valid */ - public PArray dataArray() { - return dataArray; - } - - @Override - public String toString() { - return dataArray.toString(); - } - - @Override - public int hashCode() { - return dataArray.hashCode(); + public static Connection of(String child1, String port1, String child2, String port2) { + return new Connection(PArray.of( + PString.of(child1), PString.of(port1), + PString.of(child2), PString.of(port2) + )); } - @Override - public boolean equals(Object obj) { - return obj == this || (obj instanceof Connection c && dataArray.equals(c.dataArray)); + /** + * Coerce the provided value into a Connection if possible. + * + * @param value value of unknown type + * @return connection or empty optional + */ + public static Optional from(Value value) { + if (value instanceof Connection connection) { + return Optional.of(connection); + } else { + try { + return PArray.from(value).map(Connection::new); + } catch (Exception ex) { + return Optional.empty(); + } + } } private static void verifyChildID(String childID) { diff --git a/praxiscore-api/src/main/java/org/praxislive/core/Value.java b/praxiscore-api/src/main/java/org/praxislive/core/Value.java index a469370e..c3c7a723 100644 --- a/praxiscore-api/src/main/java/org/praxislive/core/Value.java +++ b/praxiscore-api/src/main/java/org/praxislive/core/Value.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2022 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -320,6 +320,7 @@ public static List> listAll() { types.add(new Type<>(PortAddress.class, PortAddress.TYPE_NAME, PortAddress::from)); types.add(new Type<>(ComponentType.class, ComponentType.TYPE_NAME, ComponentType::from)); + types.add(new Type<>(Connection.class, Connection.TYPE_NAME, Connection::from)); Map, Type> typesByClass = new HashMap<>(); Map> typesByName = new HashMap<>(); diff --git a/praxiscore-api/src/main/java/org/praxislive/core/types/PArray.java b/praxiscore-api/src/main/java/org/praxislive/core/types/PArray.java index e6690fa4..2810ee98 100644 --- a/praxiscore-api/src/main/java/org/praxislive/core/types/PArray.java +++ b/praxiscore-api/src/main/java/org/praxislive/core/types/PArray.java @@ -1,7 +1,7 @@ /* * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. * - * Copyright 2023 Neil C Smith. + * Copyright 2024 Neil C Smith. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License version 3 only, as @@ -25,6 +25,7 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.stream.Collector; import java.util.stream.Stream; @@ -349,4 +350,78 @@ public static ArgumentInfo info() { ); } + /** + * An abstract superclass for values that are backed solely by a PArray. + * Subclassing this type can help with efficient serialization of the + * underlying representation. The concrete value type must be able to + * construct an equivalent value entirely from the PArray returned from + * {@link #dataArray()}. + *

+ * The toString, equals, equivalent and hashCode methods are all implemented + * based solely on the array data. + */ + public static abstract class ArrayBasedValue extends Value { + + private final PArray data; + + /** + * Construct a MapBasedValue using the provided data map. + * + * @param data data map + */ + protected ArrayBasedValue(PArray data) { + this.data = Objects.requireNonNull(data); + } + + /** + * Access the backing PArray data. + * + * @return backing array + */ + public final PArray dataArray() { + return data; + } + + @Override + public final String toString() { + return data.toString(); + } + + @Override + public final int hashCode() { + return data.hashCode(); + } + + @Override + public final boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final ArrayBasedValue other = (ArrayBasedValue) obj; + return Objects.equals(this.data, other.data); + } + + @Override + public final boolean equivalent(Value value) { + if (this == value) { + return true; + } + if (value instanceof ArrayBasedValue arrayBased) { + return data.equivalent(arrayBased.data); + } else { + return PMap.from(value) + .map(data::equivalent) + .orElse(false); + } + + } + + } + } diff --git a/praxiscore-base/src/main/java/org/praxislive/base/AbstractContainer.java b/praxiscore-base/src/main/java/org/praxislive/base/AbstractContainer.java index 9bfcc89e..f19d3b25 100644 --- a/praxiscore-base/src/main/java/org/praxislive/base/AbstractContainer.java +++ b/praxiscore-base/src/main/java/org/praxislive/base/AbstractContainer.java @@ -63,7 +63,7 @@ public abstract class AbstractContainer extends AbstractComponent implements Con private final static System.Logger LOG = System.getLogger(AbstractContainer.class.getName()); private final Map childMap; - private final Set connections; + private final Set connections; protected AbstractContainer() { childMap = new LinkedHashMap<>(); @@ -124,9 +124,7 @@ protected final void writeChildren(TreeWriter writer) { } protected final void writeConnections(TreeWriter writer) { - connections.forEach(c -> writer.writeConnection( - new Connection(c.get(0).toString(), c.get(1).toString(), - c.get(2).toString(), c.get(3).toString()))); + connections.forEach(writer::writeConnection); } protected void addChild(String id, Component child) throws VetoException { @@ -173,34 +171,26 @@ protected String getChildID(Component child) { protected void connect(String component1, String port1, String component2, String port2) throws PortConnectionException { - handleConnection(true, - PString.of(component1), - PString.of(port1), - PString.of(component2), - PString.of(port2)); + handleConnection(true, component1, port1, component2, port2); } protected void disconnect(String component1, String port1, String component2, String port2) { try { - handleConnection(false, - PString.of(component1), - PString.of(port1), - PString.of(component2), - PString.of(port2)); + handleConnection(false, component1, port1, component2, port2); } catch (PortConnectionException ex) { LOG.log(System.Logger.Level.ERROR, "", ex); } } - private void handleConnection(boolean connect, PString c1id, PString p1id, PString c2id, PString p2id) + private void handleConnection(boolean connect, String component1, String port1, String component2, String port2) throws PortConnectionException { try { - Component c1 = getChild(c1id.toString()); - final Port p1 = c1.getPort(p1id.toString()); - Component c2 = getChild(c2id.toString()); - final Port p2 = c2.getPort(p2id.toString()); + Component c1 = getChild(component1); + final Port p1 = c1.getPort(port1); + Component c2 = getChild(component2); + final Port p2 = c2.getPort(port2); - final PArray connection = PArray.of(c1id, p1id, c2id, p2id); + final Connection connection = Connection.of(component1, port1, component2, port2); if (connect) { p1.connect(p2); @@ -214,8 +204,8 @@ private void handleConnection(boolean connect, PString c1id, PString p1id, PStri } } catch (Exception ex) { LOG.log(System.Logger.Level.DEBUG, "Can't connect ports.", ex); - throw new PortConnectionException("Can't connect " + c1id + "!" + p1id - + " to " + c2id + "!" + p2id); + throw new PortConnectionException("Can't connect " + component1 + "!" + port1 + + " to " + component2 + "!" + port2); } } @@ -277,10 +267,10 @@ protected class ConnectControl implements Control { @Override public void call(Call call, PacketRouter router) throws Exception { handleConnection(true, - PString.from(call.args().get(0)).orElseThrow(), - PString.from(call.args().get(1)).orElseThrow(), - PString.from(call.args().get(2)).orElseThrow(), - PString.from(call.args().get(3)).orElseThrow()); + call.args().get(0).toString(), + call.args().get(1).toString(), + call.args().get(2).toString(), + call.args().get(3).toString()); router.route(call.reply()); } @@ -291,10 +281,10 @@ protected class DisconnectControl implements Control { @Override public void call(Call call, PacketRouter router) throws Exception { handleConnection(false, - PString.from(call.args().get(0)).orElseThrow(), - PString.from(call.args().get(1)).orElseThrow(), - PString.from(call.args().get(2)).orElseThrow(), - PString.from(call.args().get(3)).orElseThrow()); + call.args().get(0).toString(), + call.args().get(1).toString(), + call.args().get(2).toString(), + call.args().get(3).toString()); router.route(call.reply()); } @@ -314,9 +304,9 @@ private class ConnectionListener implements PortListener { Port p1; Port p2; - PArray connection; + Connection connection; - private ConnectionListener(Port p1, Port p2, PArray connection) { + private ConnectionListener(Port p1, Port p2, Connection connection) { this.p1 = p1; this.p2 = p2; this.connection = connection; diff --git a/praxiscore-base/src/test/java/org/praxislive/base/MapTreeWriterTest.java b/praxiscore-base/src/test/java/org/praxislive/base/MapTreeWriterTest.java index d578648d..acb5dff7 100644 --- a/praxiscore-base/src/test/java/org/praxislive/base/MapTreeWriterTest.java +++ b/praxiscore-base/src/test/java/org/praxislive/base/MapTreeWriterTest.java @@ -63,8 +63,8 @@ public void testWriter() { .writeInfo(ComponentProtocol.API_INFO) .writeProperty("p1", PNumber.of(42)); }) - .writeConnection(new Connection("child1", "out", "child2", "in")) - .writeConnection(new Connection("child2", "ready", "child1", "trigger")) + .writeConnection(Connection.of("child1", "out", "child2", "in")) + .writeConnection(Connection.of("child2", "ready", "child1", "trigger")) .build(); if (VERBOSE) { System.out.println("Writer output\n================="); @@ -94,8 +94,8 @@ private PMap buildComparison() { .build() ); builder.put("%connections", PArray.of( - new Connection("child1", "out", "child2", "in").dataArray(), - new Connection("child2", "ready", "child1", "trigger").dataArray() + Connection.of("child1", "out", "child2", "in").dataArray(), + Connection.of("child2", "ready", "child1", "trigger").dataArray() )); return builder.build(); } diff --git a/praxiscore-project/src/main/java/org/praxislive/project/GraphElement.java b/praxiscore-project/src/main/java/org/praxislive/project/GraphElement.java index 76026e86..561d7efc 100644 --- a/praxiscore-project/src/main/java/org/praxislive/project/GraphElement.java +++ b/praxiscore-project/src/main/java/org/praxislive/project/GraphElement.java @@ -420,68 +420,46 @@ public String toString() { public static final class Connection extends GraphElement { - private final String sourceComponent; - private final String sourcePort; - private final String targetComponent; - private final String targetPort; + private final org.praxislive.core.Connection value; private Connection(String sourceComponent, String sourcePort, String targetComponent, String targetPort) { - if (!ComponentAddress.isValidID(Objects.requireNonNull(sourceComponent))) { - throw new IllegalArgumentException(sourceComponent + " is not a valid component ID"); - } - if (!ComponentAddress.isValidID(Objects.requireNonNull(targetComponent))) { - throw new IllegalArgumentException(targetComponent + " is not a valid component ID"); - } - if (!PortAddress.isValidID(Objects.requireNonNull(sourcePort))) { - throw new IllegalArgumentException(sourcePort + " is not a valid port ID"); - } - if (!PortAddress.isValidID(Objects.requireNonNull(targetPort))) { - throw new IllegalArgumentException(targetPort + " is not a valid port ID"); - } - - this.sourceComponent = sourceComponent; - this.sourcePort = sourcePort; - this.targetComponent = targetComponent; - this.targetPort = targetPort; + value = org.praxislive.core.Connection.of(sourceComponent, sourcePort, targetComponent, targetPort); } public String sourceComponent() { - return sourceComponent; + return value.sourceComponent(); } public String sourcePort() { - return sourcePort; + return value.sourcePort(); } public String targetComponent() { - return targetComponent; + return value.targetComponent(); } public String targetPort() { - return targetPort; + return value.targetPort(); } @Override public int hashCode() { - return Objects.hash(sourceComponent, sourcePort, targetComponent, targetPort); + return Objects.hash(value); } @Override public boolean equals(Object obj) { return obj == this - || (obj instanceof Connection c - && Objects.equals(this.sourceComponent, c.sourceComponent) - && Objects.equals(this.sourcePort, c.sourcePort) - && Objects.equals(this.targetComponent, c.targetComponent) - && Objects.equals(this.targetPort, c.targetPort)); + || obj instanceof Connection c + && Objects.equals(this.value, c.value); } @Override public String toString() { - return "Connection{" + "sourceComponent=" + sourceComponent - + ", sourcePort=" + sourcePort - + ", targetComponent=" + targetComponent - + ", targetPort=" + targetPort + "}"; + return "Connection{" + "sourceComponent=" + sourceComponent() + + ", sourcePort=" + sourcePort() + + ", targetComponent=" + targetComponent() + + ", targetPort=" + targetPort() + "}"; } } diff --git a/praxiscore-project/src/test/java/org/praxislive/project/GraphModelTest.java b/praxiscore-project/src/test/java/org/praxislive/project/GraphModelTest.java index efc9d28a..431f5790 100644 --- a/praxiscore-project/src/test/java/org/praxislive/project/GraphModelTest.java +++ b/praxiscore-project/src/test/java/org/praxislive/project/GraphModelTest.java @@ -517,9 +517,9 @@ private static PMap serializedGraph() { .build() ); builder.put("%connections", PArray.of( - new Connection("child1", "out", "child2", "in").dataArray(), - new Connection("child1", "out", "container", "in").dataArray(), - new Connection("container", "ready", "child1", "trigger").dataArray() + Connection.of("child1", "out", "child2", "in"), + Connection.of("child1", "out", "container", "in"), + Connection.of("container", "ready", "child1", "trigger") )); return builder.build(); }