From 7482d6950eb1fc554ff2c035dcf33c725a421003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Toma=C5=BE=20Su=C5=A1nik?= Date: Tue, 28 Jul 2026 18:38:04 +0200 Subject: [PATCH] Client: match subscriptions by context, not path only Add SubscriptionMatcher.pathAndContext() to select path-and-context matching without naming Filter classes, and use it in OpenCmwDataSource. Notifications are attributed to a subscription by the context keys whose Filter is registered in FilterRegistry; keys without one are ignored. Selecting context matching previously required passing Filter classes to the constructor, which also registers them globally. A client that registers its filters elsewhere had no way to enable it, so OpenCmwDataSource compared the URI path only and could not tell apart concurrent subscriptions that differ only by their query. --- .../io/opencmw/client/OpenCmwDataSource.java | 5 ++++- .../opencmw/filter/SubscriptionMatcher.java | 15 +++++++++++++++ .../filter/SubscriptionMatcherTest.java | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/client/src/main/java/io/opencmw/client/OpenCmwDataSource.java b/client/src/main/java/io/opencmw/client/OpenCmwDataSource.java index 592a4358..5558a811 100644 --- a/client/src/main/java/io/opencmw/client/OpenCmwDataSource.java +++ b/client/src/main/java/io/opencmw/client/OpenCmwDataSource.java @@ -96,7 +96,10 @@ public List getRegisteredDnsResolver() { private final ZMonitor socketMonitor; private final Map subscriptions = new HashMap<>(); // NOPMD: not accessed concurrently private final URI serverUri; - private final BiPredicate subscriptionMatcher = new SubscriptionMatcher(); // + // + // Context matching differentiates subscriptions to the same path by the context keys whose Filter + // implementations are registered in FilterRegistry; keys without one are ignored. + private final BiPredicate subscriptionMatcher = SubscriptionMatcher.pathAndContext(); private final long heartbeatInterval; private Future dnsWorkerResult; private long nextReconnectAttemptTimeStamp; diff --git a/core/src/main/java/io/opencmw/filter/SubscriptionMatcher.java b/core/src/main/java/io/opencmw/filter/SubscriptionMatcher.java index 742d1123..7ecdb55d 100644 --- a/core/src/main/java/io/opencmw/filter/SubscriptionMatcher.java +++ b/core/src/main/java/io/opencmw/filter/SubscriptionMatcher.java @@ -28,6 +28,21 @@ public SubscriptionMatcher(final Class... filterConfig) { FilterRegistry.registerNewFilter(filterConfig); } + private SubscriptionMatcher(final boolean pathOnly) { + isPathOnly = pathOnly; + } + + /** + * Matcher that compares path and context, discriminating on whichever filters are registered in + * the global {@link FilterRegistry} when a match is evaluated. Use this when the filters are + * registered elsewhere, rather than naming them here. + * + * @return context-aware matcher that registers no filters of its own + */ + public static SubscriptionMatcher pathAndContext() { + return new SubscriptionMatcher(false); + } + @Override public boolean test(final @NotNull URI notified, final @NotNull URI subscriber) { return isPathOnly ? testPathOnly(notified, subscriber) : testPathAndContext(notified, subscriber); diff --git a/core/src/test/java/io/opencmw/filter/SubscriptionMatcherTest.java b/core/src/test/java/io/opencmw/filter/SubscriptionMatcherTest.java index d574b43f..dce3d677 100644 --- a/core/src/test/java/io/opencmw/filter/SubscriptionMatcherTest.java +++ b/core/src/test/java/io/opencmw/filter/SubscriptionMatcherTest.java @@ -75,6 +75,25 @@ void testMatcherTimingContentContext() { test(matcher, false, "property?ctx=FAIR.SELECTOR.ALL", "property?ctx=FAIR.SELECTOR.C=2&contentType=text/html"); } + @Test + void testMatcherPathAndContextFactory() { + // filters registered elsewhere, none named at construction + FilterRegistry.registerNewFilter(TestFilter1.class); + + final BiPredicate matcher = SubscriptionMatcher.pathAndContext(); + + // subscriptions to the same path differing only by a registered context key must not cross-match + test(matcher, false, "property/A?testKey1=41", "property/A?testKey1=42"); + test(matcher, false, "property/A?testKey1=42", "property/A?testKey1=41"); + test(matcher, true, "property/A?testKey1=42", "property/A?testKey1=42"); + + // unregistered context keys keep their existing 'ignore' semantics on either side + test(matcher, true, "property/A?testKey1=42&unknownKey=x", "property/A?testKey1=42"); + test(matcher, true, "property/A?testKey1=42", "property/A?testKey1=42&unknownKey=x"); + test(matcher, true, "property/A?testKey1=42", "property/A*"); + test(matcher, false, "property/A?testKey1=42", "property/B?testKey1=42"); + } + private static void test(final BiPredicate matcher, final boolean expected, final String notify, final String subscription) throws AssertionFailedError { final boolean actual = matcher.test(URI.create(notify), URI.create(subscription)); if (actual != expected) {