import org.opendaylight.restconf.server.api.ResourceBody;
import org.opendaylight.restconf.server.spi.ApiPathCanonizer;
import org.opendaylight.restconf.server.spi.ApiPathNormalizer;
+import org.opendaylight.restconf.server.spi.DefaultResourceContext;
import org.opendaylight.restconf.server.spi.OperationInput;
import org.opendaylight.restconf.server.spi.RpcImplementation;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.netconf.with.defaults.rev110601.WithDefaultsMode;
final PatchContext patch;
try {
- patch = body.toPatchContext(path);
+ patch = body.toPatchContext(new DefaultResourceContext(path));
} catch (IOException e) {
LOG.debug("Error parsing YANG Patch input", e);
return RestconfFuture.failed(new RestconfDocumentedException("Error parsing input: " + e.getMessage(),
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.codec.gson.JSONCodecFactory;
import org.opendaylight.yangtools.yang.data.codec.gson.JsonParserStream;
import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNormalizedNodeStreamWriter;
import org.opendaylight.yangtools.yang.data.impl.schema.NormalizationResultHolder;
}
@Override
- PatchContext toPatchContext(final DatabindPath.Data path, final InputStream inputStream) throws IOException {
+ PatchContext toPatchContext(final ResourceContext resource, final InputStream inputStream) throws IOException {
try (var jsonReader = new JsonReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
final var patchId = new AtomicReference<String>();
- final var resultList = read(jsonReader, path, patchId);
+ final var resultList = read(jsonReader, resource, patchId);
// Note: patchId side-effect of above
return new PatchContext(patchId.get(), resultList);
}
}
- private static ImmutableList<PatchEntity> read(final JsonReader in, final DatabindPath.Data path,
+ private static ImmutableList<PatchEntity> read(final JsonReader in, final @NonNull ResourceContext resource,
final AtomicReference<String> patchId) throws IOException {
final var edits = ImmutableList.<PatchEntity>builder();
final var edit = new PatchEdit();
case END_DOCUMENT:
break;
case NAME:
- parseByName(in.nextName(), edit, in, path, edits, patchId);
+ parseByName(in.nextName(), edit, in, resource, edits, patchId);
break;
case END_OBJECT:
in.endObject();
// Switch value of parsed JsonToken.NAME and read edit definition or patch id
private static void parseByName(final @NonNull String name, final @NonNull PatchEdit edit,
- final @NonNull JsonReader in, final DatabindPath.@NonNull Data path,
+ final @NonNull JsonReader in, final @NonNull ResourceContext resource,
final @NonNull Builder<PatchEntity> resultCollection, final @NonNull AtomicReference<String> patchId)
throws IOException {
switch (name) {
in.beginArray();
while (in.hasNext()) {
- readEditDefinition(edit, in, path);
+ readEditDefinition(edit, in, resource);
resultCollection.add(prepareEditOperation(edit));
edit.clear();
}
in.endArray();
} else {
- readEditDefinition(edit, in, path);
+ readEditDefinition(edit, in, resource);
resultCollection.add(prepareEditOperation(edit));
edit.clear();
}
// Read one patch edit object from JSON input
private static void readEditDefinition(final @NonNull PatchEdit edit, final @NonNull JsonReader in,
- final DatabindPath.@NonNull Data path) throws IOException {
+ final @NonNull ResourceContext resource) throws IOException {
String deferredValue = null;
in.beginObject();
+ final var codecs = resource.path.databind().jsonCodecs();
+
while (in.hasNext()) {
final String editDefinition = in.nextName();
switch (editDefinition) {
break;
case "target":
// target can be specified completely in request URI
- edit.setTarget(parsePatchTarget(path, in.nextString()));
- final var stack = path.databind().schemaTree().enterPath(edit.getTarget()).orElseThrow().stack();
+ final var target = parsePatchTarget(resource, in.nextString());
+ edit.setTarget(target.instance());
+ final var stack = target.inference().toSchemaInferenceStack();
if (!stack.isEmpty()) {
stack.exit();
}
deferredValue = readValueNode(in);
} else {
// We have a target schema node, reuse this reader without buffering the value.
- edit.setData(readEditData(in, edit.getTargetSchemaNode(), path.databind()));
+ edit.setData(readEditData(in, edit.getTargetSchemaNode(), codecs));
}
break;
default:
if (deferredValue != null) {
// read saved data to normalized node when target schema is already known
edit.setData(readEditData(new JsonReader(new StringReader(deferredValue)), edit.getTargetSchemaNode(),
- path.databind()));
+ codecs));
}
}
* @return NormalizedNode representing data
*/
private static NormalizedNode readEditData(final @NonNull JsonReader in, final @NonNull Inference targetSchemaNode,
- final @NonNull DatabindContext databind) {
+ final @NonNull JSONCodecFactory codecs) {
final var resultHolder = new NormalizationResultHolder();
final var writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
- JsonParserStream.create(writer, databind.jsonCodecs(), targetSchemaNode).parse(in);
+ JsonParserStream.create(writer, codecs, targetSchemaNode).parse(in);
return resultHolder.getResult().data();
}
*/
package org.opendaylight.restconf.server.api;
+import static java.util.Objects.requireNonNull;
+
import java.io.IOException;
import java.io.InputStream;
import java.text.ParseException;
import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
import org.opendaylight.restconf.api.ApiPath;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.common.patch.PatchContext;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer;
+import org.opendaylight.restconf.server.api.DatabindPath.Data;
import org.opendaylight.yang.gen.v1.urn.ietf.params.xml.ns.yang.ietf.yang.patch.rev170222.yang.patch.yang.patch.Edit.Operation;
+import org.opendaylight.yangtools.concepts.Immutable;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
-import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
/**
* A YANG Patch body.
*/
public abstract sealed class PatchBody extends AbstractBody permits JsonPatchBody, XmlPatchBody {
+ /**
+ * Resource context needed to completely resolve a {@link PatchBody}.
+ */
+ @NonNullByDefault
+ public abstract static class ResourceContext implements Immutable {
+ protected final Data path;
+
+ protected ResourceContext(final Data path) {
+ this.path = requireNonNull(path);
+ }
+
+ /**
+ * Return a {@link ResourceContext} for a sub-resource identified by an {@link ApiPath}.
+ *
+ * @param apiPath sub-resource
+ * @return A {@link ResourceContext}
+ * @throws RestconfDocumentedException if the sub-resource cannot be resolved
+ */
+ protected abstract ResourceContext resolveRelative(ApiPath apiPath);
+ }
+
PatchBody(final InputStream inputStream) {
super(inputStream);
}
- public final @NonNull PatchContext toPatchContext(final DatabindPath.@NonNull Data path) throws IOException {
+ public final @NonNull PatchContext toPatchContext(final @NonNull ResourceContext resource) throws IOException {
try (var is = acquireStream()) {
- return toPatchContext(path, is);
+ return toPatchContext(resource, is);
}
}
- abstract @NonNull PatchContext toPatchContext(DatabindPath.@NonNull Data path, @NonNull InputStream inputStream)
+ abstract @NonNull PatchContext toPatchContext(@NonNull ResourceContext resource, @NonNull InputStream inputStream)
throws IOException;
- static final YangInstanceIdentifier parsePatchTarget(final DatabindPath.@NonNull Data path, final String target) {
+ static final Data parsePatchTarget(final @NonNull ResourceContext resource, final String target) {
// As per: https://www.rfc-editor.org/rfc/rfc8072#page-18:
//
// "Identifies the target data node for the edit
ErrorType.RPC, ErrorTag.MALFORMED_MESSAGE, e);
}
- final YangInstanceIdentifier result;
+ final Data result;
try {
- result = ApiPathNormalizer.normalizeSubResource(path, targetPath).instance();
+ result = resource.resolveRelative(targetPath).path;
} catch (RestconfDocumentedException e) {
throw new RestconfDocumentedException("Invalid edit target '" + targetPath + "'",
ErrorType.RPC, ErrorTag.MALFORMED_MESSAGE, e);
}
- if (result.isEmpty()) {
+ if (result.instance().isEmpty()) {
throw new RestconfDocumentedException("Target node resource must not be a datastore resource",
ErrorType.RPC, ErrorTag.MALFORMED_MESSAGE);
}
}
@Override
- PatchContext toPatchContext(final DatabindPath.Data path, final InputStream inputStream) throws IOException {
+ PatchContext toPatchContext(final ResourceContext resource, final InputStream inputStream) throws IOException {
try {
- return parse(path, UntrustedXML.newDocumentBuilder().parse(inputStream));
+ return parse(resource, UntrustedXML.newDocumentBuilder().parse(inputStream));
} catch (XMLStreamException | SAXException | URISyntaxException e) {
LOG.debug("Failed to parse YANG Patch XML", e);
throw new RestconfDocumentedException("Error parsing YANG Patch XML: " + e.getMessage(), ErrorType.PROTOCOL,
}
}
- private static @NonNull PatchContext parse(final DatabindPath.@NonNull Data path, final Document doc)
+ private static @NonNull PatchContext parse(final ResourceContext resource, final Document doc)
throws XMLStreamException, IOException, SAXException, URISyntaxException {
final var entities = ImmutableList.<PatchEntity>builder();
final var patchId = doc.getElementsByTagName("patch-id").item(0).getFirstChild().getNodeValue();
final var editNodes = doc.getElementsByTagName("edit");
- final var databind = path.databind();
for (int i = 0; i < editNodes.getLength(); i++) {
final Element element = (Element) editNodes.item(i);
final Element firstValueElement = values != null ? values.get(0) : null;
// find complete path to target, it can be also empty (only slash)
- final var targetII = parsePatchTarget(path, target);
- // move schema node
- final var lookup = databind.schemaTree().enterPath(targetII).orElseThrow();
-
- final var stack = lookup.stack();
- final var inference = stack.toInference();
+ final var targetData = parsePatchTarget(resource, target);
+ final var inference = targetData.inference();
+ final var stack = inference.toSchemaInferenceStack();
if (!stack.isEmpty()) {
stack.exit();
}
+ final var targetPath = targetData.instance();
+
if (requiresValue(oper)) {
final var resultHolder = new NormalizationResultHolder();
final var writer = ImmutableNormalizedNodeStreamWriter.from(resultHolder);
- final var xmlParser = XmlParserStream.create(writer, databind.xmlCodecs(), inference);
+ final var xmlParser = XmlParserStream.create(writer, resource.path.databind().xmlCodecs(), inference);
xmlParser.traverse(new DOMSource(firstValueElement));
final var result = resultHolder.getResult().data();
// for lists allow to manipulate with list items through their parent
- if (targetII.getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
- entities.add(new PatchEntity(editId, oper, targetII.getParent(), result));
+ if (targetPath.getLastPathArgument() instanceof NodeIdentifierWithPredicates) {
+ entities.add(new PatchEntity(editId, oper, targetPath.getParent(), result));
} else {
- entities.add(new PatchEntity(editId, oper, targetII, result));
+ entities.add(new PatchEntity(editId, oper, targetPath, result));
}
} else {
- entities.add(new PatchEntity(editId, oper, targetII));
+ entities.add(new PatchEntity(editId, oper, targetPath));
}
}
namespace, firstStep, it);
}
- private @NonNull DatabindPath normalizeSteps(final SchemaInferenceStack stack,
- final @NonNull DataSchemaContext rootNode, final @NonNull List<PathArgument> pathPrefix,
- final @NonNull QNameModule firstNamespace, final @NonNull Step firstStep,
- final Iterator<@NonNull Step> it) {
+ @NonNull DatabindPath normalizeSteps(final SchemaInferenceStack stack, final @NonNull DataSchemaContext rootNode,
+ final @NonNull List<PathArgument> pathPrefix, final @NonNull QNameModule firstNamespace,
+ final @NonNull Step firstStep, final Iterator<@NonNull Step> it) {
var parentNode = rootNode;
var namespace = firstNamespace;
var step = firstStep;
ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
}
- public static @NonNull Data normalizeSubResource(final Data resource, final ApiPath subResource) {
- // If subResource is empty just return the resource
- final var steps = subResource.steps();
- if (steps.isEmpty()) {
- return requireNonNull(resource);
- }
-
- final var normalizer = new ApiPathNormalizer(resource.databind());
- final var urlPath = resource.instance();
- if (urlPath.isEmpty()) {
- // URL indicates the datastore resource, let's just normalize targetPath
- return normalizer.normalizeDataPath(subResource);
- }
-
- // Defer to normalizePath(), faking things a bit. Then check the result.
- final var it = steps.iterator();
- final var path = normalizer.normalizeSteps(resource.inference().toSchemaInferenceStack(), resource.schema(),
- urlPath.getPathArguments(), urlPath.getLastPathArgument().getNodeType().getModule(), it.next(), it);
- if (path instanceof Data dataPath) {
- return dataPath;
- }
- throw new RestconfDocumentedException("Sub-resource '" + subResource + "' resolves to non-data " + path,
- ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
- }
-
@Override
public PathArgument normalizePoint(final ApiPath value) {
final var path = normalizePath(value);
--- /dev/null
+/*
+ * Copyright (c) 2024 PANTHEON.tech, s.r.o. 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.server.spi;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.restconf.api.ApiPath;
+import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
+import org.opendaylight.restconf.server.api.DatabindPath.Data;
+import org.opendaylight.restconf.server.api.PatchBody.ResourceContext;
+import org.opendaylight.yangtools.yang.common.ErrorTag;
+import org.opendaylight.yangtools.yang.common.ErrorType;
+
+/**
+ * Default implementation of a {@link ResourceContext}.
+ */
+@NonNullByDefault
+public final class DefaultResourceContext extends ResourceContext {
+ public DefaultResourceContext(final Data path) {
+ super(path);
+ }
+
+ @Override
+ protected ResourceContext resolveRelative(final ApiPath apiPath) {
+ // If subResource is empty just return this resource
+ final var steps = apiPath.steps();
+ if (steps.isEmpty()) {
+ return this;
+ }
+
+ final var normalizer = new ApiPathNormalizer(path.databind());
+ final var urlPath = path.instance();
+ if (urlPath.isEmpty()) {
+ // URL indicates the datastore resource, let's just normalize targetPath
+ return new DefaultResourceContext(normalizer.normalizeDataPath(apiPath));
+ }
+
+ // Defer to normalizeSteps(), faking things a bit. Then check the result.
+ final var it = steps.iterator();
+ final var resolved = normalizer.normalizeSteps(path.inference().toSchemaInferenceStack(), path.schema(),
+ urlPath.getPathArguments(), urlPath.getLastPathArgument().getNodeType().getModule(), it.next(), it);
+ if (resolved instanceof Data dataPath) {
+ return new DefaultResourceContext(dataPath);
+ }
+ throw new RestconfDocumentedException("Sub-resource '" + apiPath + "' resolves to non-data " + resolved,
+ ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
+ }
+}
import org.opendaylight.restconf.nb.rfc8040.AbstractInstanceIdentifierTest;
import org.opendaylight.restconf.nb.rfc8040.rests.transactions.MdsalRestconfStrategy;
import org.opendaylight.restconf.server.api.PatchBody;
+import org.opendaylight.restconf.server.spi.DefaultResourceContext;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
@RunWith(MockitoJUnitRunner.Silent.class)
final var stratAndPath = strategy.resolveStrategyPath(apiPath);
try (var body = bodyConstructor.apply(stringInputStream(patchBody))) {
- return body.toPatchContext(stratAndPath.path());
+ return body.toPatchContext(new DefaultResourceContext(stratAndPath.path()));
}
}
}