*/
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;
*/
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
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;
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;
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);
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;
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;
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();
}
}
/**
- * 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;
}
/**
}
ToStringHelper addToStringAttributes(final ToStringHelper helper) {
- return helper.add("name", name).add("output-type", getOutputType());
+ return helper.add("name", name).add("output-type", outputType.getName());
}
}