diff --git a/bosk-core/src/main/java/works/bosk/drivers/ForwardingDriver.java b/bosk-core/src/main/java/works/bosk/drivers/ForwardingDriver.java
index a87f590b..da268248 100644
--- a/bosk-core/src/main/java/works/bosk/drivers/ForwardingDriver.java
+++ b/bosk-core/src/main/java/works/bosk/drivers/ForwardingDriver.java
@@ -2,8 +2,6 @@
import java.io.IOException;
import java.lang.reflect.Type;
-import java.util.ArrayList;
-import java.util.List;
import lombok.RequiredArgsConstructor;
import works.bosk.BoskDriver;
import works.bosk.Identifier;
@@ -11,63 +9,51 @@
import works.bosk.StateTreeNode;
import works.bosk.exceptions.InvalidTypeException;
+/**
+ * Implements all {@link BoskDriver} methods by simply calling the corresponding
+ * methods on another driver. Useful for overriding one or two methods while leaving
+ * the rest unchanged.
+ *
+ * Unlike {@link ReplicaSet}, this does not automatically fix up the references to
+ * point to the right bosk. The references must already be from the downstream bosk.
+ */
@RequiredArgsConstructor
public class ForwardingDriver implements BoskDriver {
- private final Iterable> downstream;
+ protected final BoskDriver downstream;
- /**
- * @return The result of calling initialRoot
on the first downstream driver
- * that doesn't throw {@link UnsupportedOperationException}. Other exceptions are propagated as-is,
- * and abort the initialization immediately.
- */
@Override
public R initialRoot(Type rootType) throws InvalidTypeException, IOException, InterruptedException {
- List exceptions = new ArrayList<>();
- for (BoskDriver d: downstream) {
- try {
- return d.initialRoot(rootType);
- } catch (UnsupportedOperationException e) {
- exceptions.add(e);
- }
- }
-
- // Oh dear.
- UnsupportedOperationException exception = new UnsupportedOperationException("Unable to forward initialRoot request");
- exceptions.forEach(exception::addSuppressed);
- throw exception;
+ return downstream.initialRoot(rootType);
}
@Override
public void submitReplacement(Reference target, T newValue) {
- downstream.forEach(d -> d.submitReplacement(target, newValue));
+ downstream.submitReplacement(target, newValue);
}
@Override
public void submitConditionalReplacement(Reference target, T newValue, Reference precondition, Identifier requiredValue) {
- downstream.forEach(d -> d.submitConditionalReplacement(target, newValue, precondition, requiredValue));
+ downstream.submitConditionalReplacement(target, newValue, precondition, requiredValue);
}
@Override
public void submitInitialization(Reference target, T newValue) {
- downstream.forEach(d -> d.submitInitialization(target, newValue));
+ downstream.submitInitialization(target, newValue);
}
@Override
public void submitDeletion(Reference target) {
- downstream.forEach(d -> d.submitDeletion(target));
+ downstream.submitDeletion(target);
}
@Override
public void submitConditionalDeletion(Reference target, Reference precondition, Identifier requiredValue) {
- downstream.forEach(d -> d.submitConditionalDeletion(target, precondition, requiredValue));
+ downstream.submitConditionalDeletion(target, precondition, requiredValue);
}
@Override
public void flush() throws InterruptedException, IOException {
- for (BoskDriver d: downstream) {
- // Note that exceptions from a downstream flush() will abort this loop
- d.flush();
- }
+ downstream.flush();
}
@Override
diff --git a/bosk-core/src/main/java/works/bosk/drivers/MirroringDriver.java b/bosk-core/src/main/java/works/bosk/drivers/MirroringDriver.java
deleted file mode 100644
index 705db69a..00000000
--- a/bosk-core/src/main/java/works/bosk/drivers/MirroringDriver.java
+++ /dev/null
@@ -1,89 +0,0 @@
-package works.bosk.drivers;
-
-import java.io.IOException;
-import java.lang.reflect.Type;
-import lombok.RequiredArgsConstructor;
-import works.bosk.Bosk;
-import works.bosk.BoskDriver;
-import works.bosk.DriverFactory;
-import works.bosk.Identifier;
-import works.bosk.Reference;
-import works.bosk.StateTreeNode;
-import works.bosk.exceptions.InvalidTypeException;
-
-import static java.util.Arrays.asList;
-import static lombok.AccessLevel.PRIVATE;
-
-/**
- * Sends events to another {@link Bosk} of the same type.
- */
-@RequiredArgsConstructor(access=PRIVATE)
-public class MirroringDriver implements BoskDriver {
- private final Bosk mirror;
-
- /**
- * Causes updates to be applied both to mirror
and to the downstream driver.
- */
- public static DriverFactory targeting(Bosk mirror) {
- return (boskInfo, downstream) -> new ForwardingDriver<>(asList(
- new MirroringDriver<>(mirror),
- downstream
- ));
- }
-
- /**
- * Causes updates to be applied only to other
.
- */
- public static MirroringDriver redirectingTo(Bosk other) {
- return new MirroringDriver<>(other);
- }
-
- @Override
- public R initialRoot(Type rootType) {
- throw new UnsupportedOperationException(MirroringDriver.class.getSimpleName() + " cannot supply an initial root");
- }
-
- @Override
- public void submitReplacement(Reference target, T newValue) {
- mirror.driver().submitReplacement(correspondingReference(target), newValue);
- }
-
- @Override
- public void submitConditionalReplacement(Reference target, T newValue, Reference precondition, Identifier requiredValue) {
- mirror.driver().submitConditionalReplacement(correspondingReference(target), newValue, correspondingReference(precondition), requiredValue);
- }
-
- @Override
- public void submitInitialization(Reference target, T newValue) {
- mirror.driver().submitInitialization(correspondingReference(target), newValue);
- }
-
- @Override
- public void submitDeletion(Reference target) {
- mirror.driver().submitDeletion(correspondingReference(target));
- }
-
- @Override
- public void submitConditionalDeletion(Reference target, Reference precondition, Identifier requiredValue) {
- mirror.driver().submitConditionalDeletion(correspondingReference(target), correspondingReference(precondition), requiredValue);
- }
-
- @Override
- public void flush() throws InterruptedException, IOException {
- mirror.driver().flush();
- }
-
- @SuppressWarnings("unchecked")
- private Reference correspondingReference(Reference original) {
- try {
- return (Reference) mirror.rootReference().then(Object.class, original.path());
- } catch (InvalidTypeException e) {
- throw new AssertionError("References are expected to be compatible: " + original, e);
- }
- }
-
- @Override
- public String toString() {
- return "Mirroring to " + mirror;
- }
-}
diff --git a/bosk-core/src/main/java/works/bosk/drivers/NoOpDriver.java b/bosk-core/src/main/java/works/bosk/drivers/NoOpDriver.java
new file mode 100644
index 00000000..5d413d84
--- /dev/null
+++ b/bosk-core/src/main/java/works/bosk/drivers/NoOpDriver.java
@@ -0,0 +1,28 @@
+package works.bosk.drivers;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import works.bosk.BoskDriver;
+import works.bosk.DriverFactory;
+import works.bosk.Identifier;
+import works.bosk.Reference;
+import works.bosk.StateTreeNode;
+import works.bosk.exceptions.InvalidTypeException;
+
+public class NoOpDriver implements BoskDriver {
+ public static DriverFactory factory() {
+ return (b,d) -> new NoOpDriver<>();
+ }
+
+ @Override
+ public R initialRoot(Type rootType) throws InvalidTypeException, IOException, InterruptedException {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override public void submitReplacement(Reference target, T newValue) { }
+ @Override public void submitConditionalReplacement(Reference target, T newValue, Reference precondition, Identifier requiredValue) { }
+ @Override public void submitInitialization(Reference target, T newValue) { }
+ @Override public void submitDeletion(Reference target) { }
+ @Override public void submitConditionalDeletion(Reference target, Reference precondition, Identifier requiredValue) { }
+ @Override public void flush() throws IOException, InterruptedException { }
+}
diff --git a/bosk-core/src/main/java/works/bosk/drivers/ReplicaSet.java b/bosk-core/src/main/java/works/bosk/drivers/ReplicaSet.java
index 99c1557b..42b1f0b4 100644
--- a/bosk-core/src/main/java/works/bosk/drivers/ReplicaSet.java
+++ b/bosk-core/src/main/java/works/bosk/drivers/ReplicaSet.java
@@ -6,6 +6,7 @@
import java.util.List;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
+import works.bosk.Bosk;
import works.bosk.BoskDriver;
import works.bosk.DriverFactory;
import works.bosk.Identifier;
@@ -21,11 +22,21 @@
* Note that this isn't used for true distributed replica sets with one bosk in each JVM process,
* but rather for constructing a local replica set within a single JVM process.
*
- * Evolution note: This kind of subsumes the functionality of both {@link ForwardingDriver}
- * and {@link MirroringDriver}. Seems like this class could either use or replace those.
- * Perhaps {@link ForwardingDriver} should do the {@link Replica#correspondingReference} logic that
- * {@link MirroringDriver} does, making the latter unnecessary; and then this class could
- * simply be a {@link ForwardingDriver} whose list of downstream drivers is mutable.
+ * The primary way to use this class is to instantiate a {@code new ReplicaSet()},
+ * then construct any number of bosks using {@link #driverFactory()}.
+ * All the resulting bosks will be in the replica set, and more can be added dynamically.
+ *
+ * There are also some factory methods that simplify some special use cases.
+ *
+ *
+ * -
+ * Use {@link #mirroringTo} to mirror changes from a primary bosk to some number
+ * of secondary ones.
+ *
+ * -
+ * Use {@link #redirectingTo} just to get a driver that can accept references to the wrong bosk.
+ *
+ *
*/
public class ReplicaSet {
final Queue> replicas = new ConcurrentLinkedQueue<>();
@@ -43,8 +54,52 @@ public DriverFactory driverFactory() {
};
}
+ /**
+ * Causes updates to be applied to {@code mirrors} and to the downstream driver.
+ *
+ * Assuming the returned factory is only used once, this has the effect of creating
+ * a fixed replica set to which new replicas can't be added dynamically;
+ * but the returned factory can be used multiple times.
+ */
+ @SafeVarargs
+ public static DriverFactory mirroringTo(Bosk... mirrors) {
+ var replicaSet = new ReplicaSet();
+ for (var m: mirrors) {
+ BoskDriver downstream = m.driver();
+ replicaSet.replicas.add(new Replica<>(
+ m.rootReference(),
+ new ForwardingDriver<>(downstream) {
+ @Override
+ public RR initialRoot(Type rootType) {
+ throw new UnsupportedOperationException("Don't use initialRoot from " + m);
+ }
+
+ @Override
+ public String toString() {
+ return downstream.toString() + " (minus initial state)";
+ }
+ }
+ ));
+ }
+ return replicaSet.driverFactory();
+ }
+
+ /**
+ * Causes updates to be applied only to other
.
+ */
+ public static BoskDriver redirectingTo(Bosk other) {
+ // A ReplicaSet with only the one replica
+ return new ReplicaSet()
+ .driverFactory().build(
+ other,
+ other.driver()
+ );
+ }
+
final class BroadcastShim implements BoskDriver {
/**
+ * TODO: should return the current state somehow. For now, I guess it's best to attach all the bosks before submitting any updates.
+ *
* @return The result of calling initialRoot
on the first downstream driver
* that doesn't throw {@link UnsupportedOperationException}. Other exceptions are propagated as-is,
* and abort the initialization immediately.
diff --git a/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java b/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java
index c3d1dc7b..cb310020 100644
--- a/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java
+++ b/bosk-core/src/test/java/works/bosk/BoskConstructorTest.java
@@ -10,10 +10,9 @@
import works.bosk.TypeValidationTest.MutableField;
import works.bosk.TypeValidationTest.SimpleTypes;
import works.bosk.drivers.ForwardingDriver;
+import works.bosk.drivers.NoOpDriver;
import works.bosk.exceptions.InvalidTypeException;
-import static java.util.Collections.emptyList;
-import static java.util.Collections.singleton;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -37,8 +36,8 @@ void basicProperties_correctValues() {
name,
rootType,
_ -> root,
- (b,d)-> {
- driver.set(new ForwardingDriver<>(singleton(d)));
+ (_, d)-> {
+ driver.set(new ForwardingDriver<>(d));
return driver.get();
});
@@ -153,7 +152,7 @@ private static void assertDefaultRootThrows(Class extends Throwable> expectedT
@NotNull
private static DriverFactory initialRootDriver(InitialRootFunction initialRootFunction) {
- return (b,d) -> new ForwardingDriver(emptyList()) {
+ return (_, _) -> new NoOpDriver<>() {
@Override
public StateTreeNode initialRoot(Type rootType) throws InvalidTypeException, IOException, InterruptedException {
return initialRootFunction.get();
diff --git a/bosk-core/src/test/java/works/bosk/DriverStackTest.java b/bosk-core/src/test/java/works/bosk/DriverStackTest.java
index c7ab51aa..22749237 100644
--- a/bosk-core/src/test/java/works/bosk/DriverStackTest.java
+++ b/bosk-core/src/test/java/works/bosk/DriverStackTest.java
@@ -2,14 +2,13 @@
import org.junit.jupiter.api.Test;
import works.bosk.drivers.ForwardingDriver;
+import works.bosk.drivers.NoOpDriver;
-import static java.util.Collections.emptySet;
-import static java.util.Collections.singletonList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
class DriverStackTest {
- final BoskDriver baseDriver = new ForwardingDriver<>(emptySet());
+ final BoskDriver baseDriver = new NoOpDriver<>();
@Test
void emptyStack_returnsDownstream() {
@@ -25,8 +24,8 @@ void stackedDrivers_correctOrder() {
);
TestDriver firstDriver = (TestDriver) stack.build(null, baseDriver);
- TestDriver secondDriver = (TestDriver) firstDriver.downstream;
- BoskDriver thirdDriver = secondDriver.downstream;
+ TestDriver secondDriver = (TestDriver) firstDriver.downstream();
+ BoskDriver thirdDriver = secondDriver.downstream();
assertEquals("first", firstDriver.name);
assertEquals("second", secondDriver.name);
@@ -35,12 +34,14 @@ void stackedDrivers_correctOrder() {
static class TestDriver extends ForwardingDriver {
final String name;
- final BoskDriver downstream;
public TestDriver(String name, BoskDriver downstream) {
- super(singletonList(downstream));
+ super(downstream);
this.name = name;
- this.downstream = downstream;
+ }
+
+ BoskDriver downstream() {
+ return downstream;
}
@Override
diff --git a/bosk-core/src/test/java/works/bosk/drivers/ForwardingDriverConformanceTest.java b/bosk-core/src/test/java/works/bosk/drivers/ForwardingDriverConformanceTest.java
index c025c049..1e9ada2f 100644
--- a/bosk-core/src/test/java/works/bosk/drivers/ForwardingDriverConformanceTest.java
+++ b/bosk-core/src/test/java/works/bosk/drivers/ForwardingDriverConformanceTest.java
@@ -2,13 +2,11 @@
import org.junit.jupiter.api.BeforeEach;
-import static java.util.Collections.singletonList;
-
public class ForwardingDriverConformanceTest extends DriverConformanceTest {
@BeforeEach
void setupDriverFactory() {
- driverFactory = (b,d)-> new ForwardingDriver<>(singletonList(d));
+ driverFactory = (_, d)-> new ForwardingDriver<>(d);
}
}
diff --git a/bosk-testing/src/main/java/works/bosk/drivers/AbstractDriverTest.java b/bosk-testing/src/main/java/works/bosk/drivers/AbstractDriverTest.java
index 2debec3e..a87ecc33 100644
--- a/bosk-testing/src/main/java/works/bosk/drivers/AbstractDriverTest.java
+++ b/bosk-testing/src/main/java/works/bosk/drivers/AbstractDriverTest.java
@@ -53,7 +53,7 @@ protected void setupBosksAndReferences(DriverFactory driverFactory)
// This is the bosk we're testing
bosk = new Bosk(boskName("Test", 1), TestEntity.class, AbstractDriverTest::initialRoot, DriverStack.of(
- MirroringDriver.targeting(canonicalBosk),
+ ReplicaSet.mirroringTo(canonicalBosk),
DriverStateVerifier.wrap(driverFactory, TestEntity.class, AbstractDriverTest::initialRoot)
));
driver = bosk.driver();
diff --git a/bosk-testing/src/main/java/works/bosk/drivers/DriverStateVerifier.java b/bosk-testing/src/main/java/works/bosk/drivers/DriverStateVerifier.java
index dca36e82..a53b7b56 100644
--- a/bosk-testing/src/main/java/works/bosk/drivers/DriverStateVerifier.java
+++ b/bosk-testing/src/main/java/works/bosk/drivers/DriverStateVerifier.java
@@ -58,7 +58,7 @@ public static DriverFactory wrap(DriverFactory verifier = new DriverStateVerifier<>(
stateTrackingBosk,
- MirroringDriver.redirectingTo(stateTrackingBosk)
+ ReplicaSet.redirectingTo(stateTrackingBosk)
);
return DriverStack.of(
DiagnosticScopeDriver.factory(dc -> dc.withAttribute(THREAD_NAME, currentThread().getName())),