Allow RestconfStream to advertize multiple encodings 50/108850/2
authorRobert Varga <robert.varga@pantheon.tech>
Sat, 4 Nov 2023 00:03:23 +0000 (01:03 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Sat, 4 Nov 2023 00:08:02 +0000 (01:08 +0100)
Each stream can have multiple encodings -- which we currently do not
support, as we have a singular outputType.

Introduce EncodingName and the ability for RestconfStream to advertize
all encodings it supports. This paves the way for transport components
to select the encoding based on subscriber preferences.

JIRA: NETCONF-1102
Change-Id: I9dbe6f7a0805668e2bbaf18d369c709e71e341f0
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/AbstractNotificationStream.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/DataTreeChangeStream.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/streams/RestconfStream.java

index e8af6c23b3b79729789f467a26b4ff50c6abea21..b9e5eeeab32d8167d7813fa01de93171826e68ca 100644 (file)
@@ -7,6 +7,7 @@
  */
 package org.opendaylight.restconf.nb.rfc8040.streams;
 
+import com.google.common.collect.ImmutableMap;
 import java.time.Instant;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.mdsal.dom.api.DOMEvent;
@@ -23,13 +24,13 @@ import org.slf4j.LoggerFactory;
  */
 abstract class AbstractNotificationStream extends RestconfStream<DOMNotification> implements DOMNotificationListener {
     private static final Logger LOG = LoggerFactory.getLogger(AbstractNotificationStream.class);
+    private static final ImmutableMap<EncodingName, NotificationFormatterFactory> ENCODINGS = ImmutableMap.of(
+        EncodingName.RFC8040_JSON, JSONNotificationFormatter.FACTORY,
+        EncodingName.RFC8040_XML, XMLNotificationFormatter.FACTORY);
 
     AbstractNotificationStream(final ListenersBroker listenersBroker, final String name,
             final NotificationOutputType outputType) {
-        super(listenersBroker, name, outputType, switch (outputType) {
-            case JSON -> JSONNotificationFormatter.FACTORY;
-            case XML -> XMLNotificationFormatter.FACTORY;
-        });
+        super(listenersBroker, name, ENCODINGS, outputType);
     }
 
     @Override
index 2c8791a6eadd9f26387c54dd1b253ff2f10a9ac0..d6c19177a071614780bd227a5d06bf9b3d705f8d 100644 (file)
@@ -10,6 +10,7 @@ package org.opendaylight.restconf.nb.rfc8040.streams;
 import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.collect.ImmutableMap;
 import java.time.Instant;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -32,6 +33,9 @@ import org.slf4j.LoggerFactory;
 public class DataTreeChangeStream extends RestconfStream<List<DataTreeCandidate>>
         implements ClusteredDOMDataTreeChangeListener {
     private static final Logger LOG = LoggerFactory.getLogger(DataTreeChangeStream.class);
+    private static final ImmutableMap<EncodingName, DataTreeCandidateFormatterFactory> ENCODINGS = ImmutableMap.of(
+        EncodingName.RFC8040_JSON, JSONDataTreeCandidateFormatter.FACTORY,
+        EncodingName.RFC8040_XML, XMLDataTreeCandidateFormatter.FACTORY);
 
     private final DatabindProvider databindProvider;
     private final @NonNull LogicalDatastoreType datastore;
@@ -40,10 +44,7 @@ public class DataTreeChangeStream extends RestconfStream<List<DataTreeCandidate>
     DataTreeChangeStream(final ListenersBroker listenersBroker, final String name,
             final NotificationOutputType outputType, final DatabindProvider databindProvider,
             final LogicalDatastoreType datastore, final YangInstanceIdentifier path) {
-        super(listenersBroker, name, outputType, switch (outputType) {
-            case JSON -> JSONDataTreeCandidateFormatter.FACTORY;
-            case XML -> XMLDataTreeCandidateFormatter.FACTORY;
-        });
+        super(listenersBroker, name, ENCODINGS, outputType);
         this.databindProvider = requireNonNull(databindProvider);
         this.datastore = requireNonNull(datastore);
         this.path = requireNonNull(path);
index 4ed294cbb2f19b2e8bf3638af10458053992b24d..94a2ff5edde27e2a5246d4d99be7162b4e6e78e4 100644 (file)
@@ -11,14 +11,17 @@ import static java.util.Objects.requireNonNull;
 
 import com.google.common.base.MoreObjects;
 import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.collect.ImmutableMap;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.regex.Pattern;
 import javax.xml.xpath.XPathExpressionException;
 import org.checkerframework.checker.lock.qual.GuardedBy;
 import org.checkerframework.checker.lock.qual.Holding;
 import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.nb.rfc8040.ReceiveEventsParams;
+import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.restconf.monitoring.rev170126.restconf.state.streams.stream.Access;
 import org.opendaylight.yang.gen.v1.urn.sal.restconf.event.subscription.rev231103.NotificationOutputTypeGrouping.NotificationOutputType;
 import org.opendaylight.yangtools.concepts.Registration;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
@@ -27,11 +30,41 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Base superclass for all stream types.
+ * A RESTCONF notification event stream. Each stream produces a number of events encoded in at least one encoding. The
+ * set of supported encodings is available through {@link #encodings()}.
+ *
+ * @param <T> Type of processed events
  */
-abstract class RestconfStream<T> {
+public abstract class RestconfStream<T> {
+    /**
+     * An opinionated view on what values we can produce for {@link Access#getEncoding()}. The name can only be composed
+     * of one or more characters matching {@code [a-zA-Z]}.
+     *
+     * @param name Encoding name, as visible via the stream's {@code access} list
+     */
+    public record EncodingName(@NonNull String name) {
+        private static final Pattern PATTERN = Pattern.compile("[a-zA-Z]+");
+
+        /**
+         * Well-known JSON encoding defined by RFC8040's {@code ietf-restconf-monitoring.yang}.
+         */
+        public static final @NonNull EncodingName RFC8040_JSON = new EncodingName("json");
+        /**
+         * Well-known XML encoding defined by RFC8040's {@code ietf-restconf-monitoring.yang}.
+         */
+        public static final @NonNull EncodingName RFC8040_XML = new EncodingName("xml");
+
+        public EncodingName {
+            if (!PATTERN.matcher(name).matches()) {
+                throw new IllegalArgumentException("name must match " + PATTERN);
+            }
+        }
+    }
+
     private static final Logger LOG = LoggerFactory.getLogger(RestconfStream.class);
 
+    // ImmutableMap because it retains iteration order
+    private final @NonNull ImmutableMap<EncodingName, ? extends EventFormatterFactory<T>> encodings;
     private final @NonNull ListenersBroker listenersBroker;
     private final @NonNull String name;
 
@@ -45,12 +78,22 @@ abstract class RestconfStream<T> {
     private final NotificationOutputType outputType;
     private @NonNull EventFormatter<T> formatter;
 
-    RestconfStream(final ListenersBroker listenersBroker, final String name, final NotificationOutputType outputType,
-            final EventFormatterFactory<T> formatterFactory) {
+    protected RestconfStream(final ListenersBroker listenersBroker, final String name,
+            final ImmutableMap<EncodingName, ? extends EventFormatterFactory<T>> encodings,
+            final NotificationOutputType outputType) {
         this.listenersBroker = requireNonNull(listenersBroker);
         this.name = requireNonNull(name);
-        this.outputType = requireNonNull(outputType);
-        this.formatterFactory = requireNonNull(formatterFactory);
+        if (encodings.isEmpty()) {
+            throw new IllegalArgumentException("Stream '" + name + "' must support at least one encoding");
+        }
+        this.encodings = encodings;
+
+        final var encodingName = switch (outputType) {
+            case JSON -> EncodingName.RFC8040_JSON;
+            case XML -> EncodingName.RFC8040_XML;
+        };
+        this.outputType = outputType;
+        formatterFactory = formatterFactory(encodingName);
         formatter = formatterFactory.emptyFormatter();
     }
 
@@ -64,12 +107,30 @@ abstract class RestconfStream<T> {
     }
 
     /**
-     * Get output type.
+     * Get supported {@link EncodingName}s. The set is guaranteed to contain at least one element and does not contain
+     * {@code null}s.
      *
-     * @return Output type (JSON or XML).
+     * @return Supported encodings.
      */
-    final String getOutputType() {
-        return outputType.getName();
+    @SuppressWarnings("null")
+    final @NonNull Set<EncodingName> encodings() {
+        return encodings.keySet();
+    }
+
+    /**
+     * Return the {@link EventFormatterFactory} for an encoding.
+     *
+     * @param encoding An {@link EncodingName}
+     * @return The {@link EventFormatterFactory} for the selected encoding
+     * @throws NullPointerException if {@code encoding} is {@code null}
+     * @throws IllegalAccessError if {@code encoding} is not supported
+     */
+    final @NonNull EventFormatterFactory<T> formatterFactory(final EncodingName encoding) {
+        final var factory = encodings.get(requireNonNull(encoding));
+        if (factory == null) {
+            throw new IllegalArgumentException("Stream '" + name + "' does not support " + encoding);
+        }
+        return factory;
     }
 
     /**
@@ -216,6 +277,6 @@ abstract class RestconfStream<T> {
     }
 
     ToStringHelper addToStringAttributes(final ToStringHelper helper) {
-        return helper.add("name", name).add("output-type", getOutputType());
+        return helper.add("name", name).add("output-type", outputType.getName());
     }
 }