--- /dev/null
+/*
+ * Copyright (c) 2023 Orange and others. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.restconf.api.query;
+
+import java.net.URI;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * OpenDaylight extension parameter. When used as {@code odl-child-nodes-only=true}, it will instruct the listener
+ * streams to only emit child nodes.
+ */
+public final class ChildNodesOnlyParam implements RestconfQueryParam<ChildNodesOnlyParam> {
+ // API consistency: must not be confused with enum constants
+ @SuppressWarnings("checkstyle:ConstantName")
+ public static final String uriName = "odl-child-nodes-only";
+
+ private static final @NonNull URI CAPABILITY =
+ URI.create("urn:opendaylight:params:restconf:capability:child-nodes-only:1.0");
+ private static final @NonNull ChildNodesOnlyParam FALSE = new ChildNodesOnlyParam(false);
+ private static final @NonNull ChildNodesOnlyParam TRUE = new ChildNodesOnlyParam(true);
+
+ private final boolean value;
+
+ private ChildNodesOnlyParam(final boolean value) {
+ this.value = value;
+ }
+
+ public static @NonNull ChildNodesOnlyParam of(final boolean value) {
+ return value ? TRUE : FALSE;
+ }
+
+ public static @NonNull ChildNodesOnlyParam forUriValue(final String uriValue) {
+ return switch (uriValue) {
+ case "false" -> FALSE;
+ case "true" -> TRUE;
+ default -> throw new IllegalArgumentException("Value can be 'false' or 'true', not '" + uriValue + "'");
+ };
+ }
+
+ @Override
+ public Class<ChildNodesOnlyParam> javaClass() {
+ return ChildNodesOnlyParam.class;
+ }
+
+ @Override
+ public String paramName() {
+ return uriName;
+ }
+
+ @Override
+ public String paramValue() {
+ return String.valueOf(value);
+ }
+
+ public boolean value() {
+ return value;
+ }
+
+ public static @NonNull URI capabilityUri() {
+ return CAPABILITY;
+ }
+}
public sealed interface RestconfQueryParam<T extends RestconfQueryParam<T>> extends Immutable
permits ContentParam, DepthParam, FieldsParam, FilterParam, InsertParam, PointParam, WithDefaultsParam,
AbstractReplayParam,
- ChangedLeafNodesOnlyParam, LeafNodesOnlyParam, PrettyPrintParam, SkipNotificationDataParam {
+ // ODL extensions
+ ChangedLeafNodesOnlyParam, LeafNodesOnlyParam, PrettyPrintParam, SkipNotificationDataParam,
+ ChildNodesOnlyParam {
/**
* Return the Java representation class.
*
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.api.query.ChangedLeafNodesOnlyParam;
+import org.opendaylight.restconf.api.query.ChildNodesOnlyParam;
import org.opendaylight.restconf.api.query.FilterParam;
import org.opendaylight.restconf.api.query.LeafNodesOnlyParam;
import org.opendaylight.restconf.api.query.SkipNotificationDataParam;
private final StopTimeParam stopTime;
private final FilterParam filter;
private final ChangedLeafNodesOnlyParam changedLeafNodesOnly;
+ private final ChildNodesOnlyParam childNodesOnly;
private NotificationQueryParams(final StartTimeParam startTime, final StopTimeParam stopTime,
final FilterParam filter, final LeafNodesOnlyParam leafNodesOnly,
- final SkipNotificationDataParam skipNotificationData,
- final ChangedLeafNodesOnlyParam changedLeafNodesOnly) {
+ final SkipNotificationDataParam skipNotificationData, final ChangedLeafNodesOnlyParam changedLeafNodesOnly,
+ final ChildNodesOnlyParam childNodesOnly) {
this.startTime = startTime;
this.stopTime = stopTime;
this.filter = filter;
this.leafNodesOnly = leafNodesOnly;
this.skipNotificationData = skipNotificationData;
this.changedLeafNodesOnly = changedLeafNodesOnly;
+ this.childNodesOnly = childNodesOnly;
}
public static @NonNull NotificationQueryParams of(final StartTimeParam startTime, final StopTimeParam stopTime,
final FilterParam filter, final LeafNodesOnlyParam leafNodesOnly,
- final SkipNotificationDataParam skipNotificationData,
- final ChangedLeafNodesOnlyParam changedLeafNodesOnly) {
+ final SkipNotificationDataParam skipNotificationData, final ChangedLeafNodesOnlyParam changedLeafNodesOnly,
+ final ChildNodesOnlyParam childNodesOnly) {
if (stopTime != null && startTime == null) {
throw new IllegalArgumentException(StopTimeParam.uriName + " parameter has to be used with "
+ StartTimeParam.uriName + " parameter");
}
- if (changedLeafNodesOnly != null && leafNodesOnly != null) {
- throw new IllegalArgumentException(ChangedLeafNodesOnlyParam.uriName + " parameter cannot be used with "
- + LeafNodesOnlyParam.uriName + " parameter");
+ if (changedLeafNodesOnly != null) {
+ if (leafNodesOnly != null) {
+ throw new IllegalArgumentException(ChangedLeafNodesOnlyParam.uriName + " parameter cannot be used with "
+ + LeafNodesOnlyParam.uriName + " parameter");
+ }
+ if (childNodesOnly != null) {
+ throw new IllegalArgumentException(ChangedLeafNodesOnlyParam.uriName + " parameter cannot be used with "
+ + ChildNodesOnlyParam.uriName + " parameter");
+ }
}
return new NotificationQueryParams(startTime, stopTime, filter, leafNodesOnly, skipNotificationData,
- changedLeafNodesOnly);
+ changedLeafNodesOnly, childNodesOnly);
}
/**
return changedLeafNodesOnly;
}
+ /**
+ * Get odl-child-nodes-only query parameter.
+ *
+ * @return odl-child-nodes-only
+ */
+ public @Nullable ChildNodesOnlyParam childNodesOnly() {
+ return childNodesOnly;
+ }
+
@Override
public String toString() {
final var helper = MoreObjects.toStringHelper(this);
if (changedLeafNodesOnly != null) {
helper.add("changedLeafNodesOnly", changedLeafNodesOnly.value());
}
+ if (childNodesOnly != null) {
+ helper.add("childNodesOnly", childNodesOnly.value());
+ }
return helper.toString();
}
}
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
import org.opendaylight.restconf.api.query.ChangedLeafNodesOnlyParam;
+import org.opendaylight.restconf.api.query.ChildNodesOnlyParam;
import org.opendaylight.restconf.api.query.ContentParam;
import org.opendaylight.restconf.api.query.DepthParam;
import org.opendaylight.restconf.api.query.FieldsParam;
InsertParam.uriName, PointParam.uriName,
// Notifications
FilterParam.uriName, StartTimeParam.uriName, StopTimeParam.uriName,
- LeafNodesOnlyParam.uriName, SkipNotificationDataParam.uriName, ChangedLeafNodesOnlyParam.uriName);
+ LeafNodesOnlyParam.uriName, SkipNotificationDataParam.uriName, ChangedLeafNodesOnlyParam.uriName,
+ ChildNodesOnlyParam.uriName);
private QueryParams() {
// Utility class
LeafNodesOnlyParam leafNodesOnly = null;
SkipNotificationDataParam skipNotificationData = null;
ChangedLeafNodesOnlyParam changedLeafNodesOnly = null;
+ ChildNodesOnlyParam childNodesOnly = null;
for (Entry<String, List<String>> entry : uriInfo.getQueryParameters().entrySet()) {
final String paramName = entry.getKey();
changedLeafNodesOnly = optionalParam(ChangedLeafNodesOnlyParam::forUriValue, paramName,
paramValues);
break;
+ case ChildNodesOnlyParam.uriName:
+ childNodesOnly = optionalParam(ChildNodesOnlyParam::forUriValue, paramName, paramValues);
+ break;
default:
throw unhandledParam("notification", paramName);
}
try {
return NotificationQueryParams.of(startTime, stopTime, filter, leafNodesOnly, skipNotificationData,
- changedLeafNodesOnly);
+ changedLeafNodesOnly, childNodesOnly);
} catch (IllegalArgumentException e) {
throw new RestconfDocumentedException("Invalid query parameters: " + e.getMessage(), e);
}
import org.opendaylight.mdsal.dom.api.DOMTransactionChainListener;
import org.opendaylight.restconf.api.query.AbstractReplayParam;
import org.opendaylight.restconf.api.query.ChangedLeafNodesOnlyParam;
+import org.opendaylight.restconf.api.query.ChildNodesOnlyParam;
import org.opendaylight.restconf.api.query.DepthParam;
import org.opendaylight.restconf.api.query.FieldsParam;
import org.opendaylight.restconf.api.query.FilterParam;
.withChildValue(LeafNodesOnlyParam.capabilityUri().toString())
.withChildValue(ChangedLeafNodesOnlyParam.capabilityUri().toString())
.withChildValue(SkipNotificationDataParam.capabilityUri().toString())
+ .withChildValue(ChildNodesOnlyParam.capabilityUri().toString())
.build();
}
}
private boolean leafNodesOnly = false;
private boolean skipNotificationData = false;
private boolean changedLeafNodesOnly = false;
+ private boolean childNodesOnly = false;
private EventFormatter<T> formatter;
AbstractCommonSubscriber(final String streamName, final NotificationOutputType outputType,
final var changedLeafNodes = params.changedLeafNodesOnly();
changedLeafNodesOnly = changedLeafNodes != null && changedLeafNodes.value();
+ final var childNodes = params.childNodesOnly();
+ childNodesOnly = childNodes != null && childNodes.value();
+
final var filter = params.filter();
final String filterValue = filter == null ? null : filter.paramValue();
if (filterValue != null && !filterValue.isEmpty()) {
return skipNotificationData;
}
+ /**
+ * Check whether this query should only notify about child node changes.
+ *
+ * @return true if this query should only notify about child node changes
+ */
+ final boolean getChildNodesOnly() {
+ return childNodesOnly;
+ }
+
final EventFormatter<T> formatter() {
return formatter;
}
final Optional<String> maybeOutput;
try {
maybeOutput = formatter().eventData(effectiveModel(), notification, eventInstant, getLeafNodesOnly(),
- isSkipNotificationData(), getChangedLeafNodesOnly());
+ isSkipNotificationData(), getChangedLeafNodesOnly(), getChildNodesOnly());
} catch (Exception e) {
LOG.error("Failed to process notification {}", notification, e);
return;
}
public final boolean serialize(final DataTreeCandidate candidate, final boolean leafNodesOnly,
- final boolean skipData, final boolean changedLeafNodesOnly) throws T {
+ final boolean skipData, final boolean changedLeafNodesOnly, final boolean childNodesOnly) throws T {
if (leafNodesOnly || changedLeafNodesOnly) {
- final var path = new ArrayDeque<PathArgument>();
- path.addAll(candidate.getRootPath().getPathArguments());
- return serializeLeafNodesOnly(path, candidate.getRootNode(), skipData, changedLeafNodesOnly);
+ return serializeLeafNodesOnly(mutableRootPath(candidate), candidate.getRootNode(), skipData,
+ changedLeafNodesOnly);
+ }
+ if (childNodesOnly) {
+ serializeChildNodesOnly(mutableRootPath(candidate), candidate.getRootNode(), skipData);
+ return true;
}
serializeData(candidate.getRootPath().getPathArguments(), candidate.getRootNode(), skipData);
return true;
}
+ private static Deque<PathArgument> mutableRootPath(final DataTreeCandidate candidate) {
+ final var ret = new ArrayDeque<PathArgument>();
+ ret.addAll(candidate.getRootPath().getPathArguments());
+ return ret;
+ }
+
final boolean serializeLeafNodesOnly(final Deque<PathArgument> path, final DataTreeCandidateNode candidate,
final boolean skipData, final boolean changedLeafNodesOnly) throws T {
final var node = switch (candidate.modificationType()) {
return ret;
}
+ private void serializeChildNodesOnly(final Deque<PathArgument> path, final DataTreeCandidateNode current,
+ final boolean skipData) throws T {
+ switch (current.modificationType()) {
+ // just a subtree modification, recurse
+ case SUBTREE_MODIFIED -> {
+ for (var child : current.childNodes()) {
+ path.add(child.name());
+ serializeChildNodesOnly(path, child, skipData);
+ path.removeLast();
+ }
+ }
+ // other modification, serialize it
+ default -> serializeData(path, current, skipData);
+ }
+ }
+
private void serializeData(final Collection<PathArgument> dataPath, final DataTreeCandidateNode candidate,
final boolean skipData) throws T {
var stack = SchemaInferenceStack.of(context);
private final XPathExpression filter;
EventFormatter() {
- this.filter = null;
+ filter = null;
}
EventFormatter(final String xpathFilter) throws XPathExpressionException {
}
final Optional<String> eventData(final EffectiveModelContext schemaContext, final T input, final Instant now,
- final boolean leafNodesOnly, final boolean skipData,
- final boolean changedLeafNodesOnly)
- throws Exception {
+ final boolean leafNodesOnly, final boolean skipData, final boolean changedLeafNodesOnly,
+ final boolean childNodeOnly) throws Exception {
if (!filterMatches(schemaContext, input, now)) {
return Optional.empty();
}
return Optional.ofNullable(
- createText(schemaContext, input, now, leafNodesOnly, skipData, changedLeafNodesOnly));
+ createText(schemaContext, input, now, leafNodesOnly, skipData, changedLeafNodesOnly, childNodeOnly));
}
/**
* @param leafNodesOnly option to include only leaves in the result
* @param skipData option to skip data in the result, only paths would be included
* @param changedLeafNodesOnly option to include only changed leaves in the result
+ * @param childNodesOnly option to include only children in the result
* @return String representation of the formatted data
* @throws Exception if the underlying formatters fail to export the data to the requested format
*/
abstract String createText(EffectiveModelContext schemaContext, T input, Instant now, boolean leafNodesOnly,
- boolean skipData, boolean changedLeafNodesOnly) throws Exception;
+ boolean skipData, boolean changedLeafNodesOnly, boolean childNodeOnly) throws Exception;
private boolean filterMatches(final EffectiveModelContext schemaContext, final T input, final Instant now)
throws IOException {
@Override
String createText(final EffectiveModelContext schemaContext, final Collection<DataTreeCandidate> input,
- final Instant now, final boolean leafNodesOnly, final boolean skipData,
- final boolean changedLeafNodesOnly)
- throws IOException {
+ final Instant now, final boolean leafNodesOnly, final boolean skipData, final boolean changedLeafNodesOnly,
+ final boolean childNodesOnly) throws IOException {
final Writer writer = new StringWriter();
final JsonWriter jsonWriter = new JsonWriter(writer).beginObject();
final var serializer = new JsonDataTreeCandidateSerializer(schemaContext, codecSupplier, jsonWriter);
boolean nonEmpty = false;
for (var candidate : input) {
- nonEmpty |= serializer.serialize(candidate, leafNodesOnly, skipData, changedLeafNodesOnly);
+ nonEmpty |= serializer.serialize(candidate, leafNodesOnly, skipData, changedLeafNodesOnly, childNodesOnly);
}
// data-change-event
@Override
String createText(final EffectiveModelContext schemaContext, final DOMNotification input, final Instant now,
- final boolean leafNodesOnly, final boolean skipData, final boolean changedLeafNodesOnly)
- throws IOException {
+ final boolean leafNodesOnly, final boolean skipData, final boolean changedLeafNodesOnly,
+ final boolean childNodesOnly) throws IOException {
final Writer writer = new StringWriter();
final JsonWriter jsonWriter = new JsonWriter(writer).beginObject();
jsonWriter.name("ietf-restconf:notification").beginObject();
final Optional<String> maybeData;
try {
maybeData = formatter().eventData(databindProvider.currentContext().modelContext(), dataTreeCandidates, now,
- getLeafNodesOnly(), isSkipNotificationData(), getChangedLeafNodesOnly());
+ getLeafNodesOnly(), isSkipNotificationData(), getChangedLeafNodesOnly(), getChildNodesOnly());
} catch (final Exception e) {
LOG.error("Failed to process notification {}",
dataTreeCandidates.stream().map(Object::toString).collect(Collectors.joining(",")), e);
@Override
String createText(final EffectiveModelContext schemaContext, final Collection<DataTreeCandidate> input,
- final Instant now, final boolean leafNodesOnly, final boolean skipData,
- final boolean changedLeafNodesOnly) throws Exception {
+ final Instant now, final boolean leafNodesOnly, final boolean skipData, final boolean changedLeafNodesOnly,
+ final boolean childNodeOnly) throws Exception {
final var writer = new StringWriter();
boolean nonEmpty = false;
try {
final var serializer = new XmlDataTreeCandidateSerializer(schemaContext, xmlStreamWriter);
for (var candidate : input) {
- nonEmpty |= serializer.serialize(candidate, leafNodesOnly, skipData, changedLeafNodesOnly);
+ nonEmpty |= serializer.serialize(candidate, leafNodesOnly, skipData, changedLeafNodesOnly,
+ childNodeOnly);
}
// data-changed-notification
@Override
String createText(final EffectiveModelContext schemaContext, final DOMNotification input, final Instant now,
- final boolean leafNodesOnly, final boolean skipData, final boolean changedLeafNodesOnly)
- throws IOException {
+ final boolean leafNodesOnly, final boolean skipData, final boolean changedLeafNodesOnly,
+ final boolean childNodesOnly) throws IOException {
final var writer = new StringWriter();
try {
equalTo("urn:opendaylight:params:restconf:capability:pretty-print:1.0"),
equalTo("urn:opendaylight:params:restconf:capability:leaf-nodes-only:1.0"),
equalTo("urn:opendaylight:params:restconf:capability:changed-leaf-nodes-only:1.0"),
- equalTo("urn:opendaylight:params:restconf:capability:skip-notification-data:1.0")));
+ equalTo("urn:opendaylight:params:restconf:capability:skip-notification-data:1.0"),
+ equalTo("urn:opendaylight:params:restconf:capability:child-nodes-only:1.0")));
}
}
final NotificationListenerAdapter notifiAdapter = ListenersBroker.getInstance().registerNotificationListener(
schemaPathNotifi, "json-stream", NotificationOutputType.JSON);
return notifiAdapter.formatter()
- .eventData(SCHEMA_CONTEXT, notificationData, Instant.now(), false, false, false).orElseThrow();
+ .eventData(SCHEMA_CONTEXT, notificationData, Instant.now(), false, false, false, false).orElseThrow();
}
}
final boolean leafNodesOnly, final boolean skipNotificationData) {
super(path, streamName, outputType);
setQueryParams(NotificationQueryParams.of(StartTimeParam.forUriValue("1970-01-01T00:00:00Z"), null, null,
- LeafNodesOnlyParam.of(leafNodesOnly), SkipNotificationDataParam.of(skipNotificationData), null));
+ LeafNodesOnlyParam.of(leafNodesOnly), SkipNotificationDataParam.of(skipNotificationData), null, null));
}
ListenerAdapterTester(final YangInstanceIdentifier path, final String streamName,
final boolean changedLeafNodesOnly) {
super(path, streamName, outputType);
setQueryParams(NotificationQueryParams.of(StartTimeParam.forUriValue("1970-01-01T00:00:00Z"), null, null,
- null, null, ChangedLeafNodesOnlyParam.of(changedLeafNodesOnly)));
+ null, null, ChangedLeafNodesOnlyParam.of(changedLeafNodesOnly), null));
}
@Override
final NotificationListenerAdapter notifiAdapter = ListenersBroker.getInstance().registerNotificationListener(
schemaPathNotifi, "xml-stream", NotificationOutputTypeGrouping.NotificationOutputType.XML);
return notifiAdapter.formatter().eventData(SCHEMA_CONTEXT, notificationData, Instant.now(), false,
- false, false).orElseThrow();
+ false, false, false).orElseThrow();
}
}