import static com.google.common.base.Verify.verifyNotNull;
import static java.util.Objects.requireNonNull;
+import com.google.common.base.VerifyException;
import com.google.common.collect.ImmutableMap;
-import java.text.ParseException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.jdt.annotation.NonNull;
import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
import org.opendaylight.restconf.nb.rfc8040.Insert.PointNormalizer;
import org.opendaylight.restconf.server.api.DatabindContext;
-import org.opendaylight.restconf.server.spi.ApiPathNormalizer.OperationPath.Rpc;
+import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Action;
+import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Data;
+import org.opendaylight.restconf.server.spi.ApiPathNormalizer.Path.Rpc;
import org.opendaylight.yangtools.yang.common.ErrorTag;
import org.opendaylight.yangtools.yang.common.ErrorType;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeWithValue;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
-import org.opendaylight.yangtools.yang.data.impl.codec.TypeDefinitionAwareCodec;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContext;
import org.opendaylight.yangtools.yang.data.util.DataSchemaContext.PathMixin;
import org.opendaylight.yangtools.yang.model.api.ActionNodeContainer;
import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.api.EffectiveStatementInference;
import org.opendaylight.yangtools.yang.model.api.LeafListSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.LeafSchemaNode;
import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
-import org.opendaylight.yangtools.yang.model.api.TypeDefinition;
+import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
import org.opendaylight.yangtools.yang.model.api.stmt.ActionEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.api.stmt.IdentityEffectiveStatement;
import org.opendaylight.yangtools.yang.model.api.stmt.InputEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.api.stmt.OutputEffectiveStatement;
import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.api.type.IdentityrefTypeDefinition;
-import org.opendaylight.yangtools.yang.model.api.type.InstanceIdentifierTypeDefinition;
-import org.opendaylight.yangtools.yang.model.api.type.LeafrefTypeDefinition;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack;
import org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.Inference;
/**
- * Deserializer for {@link String} to {@link YangInstanceIdentifier} for restconf.
+ * Utility for normalizing {@link ApiPath}s. An {@link ApiPath} can represent a number of different constructs, as
+ * denoted to in the {@link Path} interface hierarchy.
+ *
+ * <p>
+ * This process is governed by
+ * <a href="https://www.rfc-editor.org/rfc/rfc8040#section-3.5.3">RFC8040, section 3.5.3</a>. The URI provides the
+ * equivalent of NETCONF XML filter encoding, with data values being escaped RFC7891 strings.
*/
public final class ApiPathNormalizer implements PointNormalizer {
+ /**
+ * A normalized {@link ApiPath}. This can be either
+ * <ul>
+ * <li>a {@link Data} pointing to a datastore resource, or</li>
+ * <li>an {@link Rpc} pointing to a YANG {@code rpc} statement, or</li>
+ * <li>an {@link Action} pointing to an instantiation of a YANG {@code action} statement</li>
+ * </ul>
+ */
@NonNullByDefault
public sealed interface Path {
-
+ /**
+ * Returns the {@link EffectiveStatementInference} made by this path.
+ *
+ * @return the {@link EffectiveStatementInference} made by this path
+ */
Inference inference();
- }
-
- @NonNullByDefault
- public sealed interface InstanceReference extends Path {
-
- YangInstanceIdentifier instance();
- }
-
- @NonNullByDefault
- public record DataPath(Inference inference, YangInstanceIdentifier instance, DataSchemaContext schema)
- implements InstanceReference {
- public DataPath {
- requireNonNull(inference);
- requireNonNull(instance);
- requireNonNull(schema);
- }
- }
-
- @NonNullByDefault
- public sealed interface OperationPath extends Path {
-
- InputEffectiveStatement inputStatement();
+ /**
+ * A {@link Path} denoting an invocation of a YANG {@code action}.
+ *
+ * @param inference the {@link EffectiveStatementInference} made by this path
+ * @param instance the {@link YangInstanceIdentifier} of the instance being referenced, guaranteed to be
+ * non-empty
+ * @param action the {@code action}
+ */
record Action(Inference inference, YangInstanceIdentifier instance, ActionEffectiveStatement action)
implements OperationPath, InstanceReference {
public Action {
requireNonNull(inference);
requireNonNull(action);
- requireNonNull(instance);
+ if (instance.isEmpty()) {
+ throw new IllegalArgumentException("action must be instantiated on a data resource");
+ }
}
@Override
public InputEffectiveStatement inputStatement() {
return action.input();
}
+
+ @Override
+ public OutputEffectiveStatement outputStatement() {
+ return action.output();
+ }
+ }
+
+ /**
+ * A {@link Path} denoting a datastore instance.
+ *
+ * @param inference the {@link EffectiveStatementInference} made by this path
+ * @param instance the {@link YangInstanceIdentifier} of the instance being referenced,
+ * {@link YangInstanceIdentifier#empty()} denotes the datastore
+ * @param schema the {@link DataSchemaContext} of the datastore instance
+ */
+ // FIXME: split into 'Datastore' and 'Data' with non-empty instance, so we can bind to correct
+ // instance-identifier semantics, which does not allow YangInstanceIdentifier.empty()
+ record Data(Inference inference, YangInstanceIdentifier instance, DataSchemaContext schema)
+ implements InstanceReference {
+ public Data {
+ requireNonNull(inference);
+ requireNonNull(instance);
+ requireNonNull(schema);
+ }
}
+ /**
+ * A {@link Path} denoting an invocation of a YANG {@code rpc}.
+ *
+ * @param inference the {@link EffectiveStatementInference} made by this path
+ * @param rpc the {@code rpc}
+ */
record Rpc(Inference inference, RpcEffectiveStatement rpc) implements OperationPath {
public Rpc {
requireNonNull(inference);
public InputEffectiveStatement inputStatement() {
return rpc.input();
}
+
+ @Override
+ public OutputEffectiveStatement outputStatement() {
+ return rpc.output();
+ }
}
}
- private final @NonNull EffectiveModelContext modelContext;
+ /**
+ * An intermediate trait of {@link Path}s which are referencing a YANG data resource. This can be either
+ * a {@link Data}, or an {@link Action}}.
+ */
+ @NonNullByDefault
+ public sealed interface InstanceReference extends Path {
+ /**
+ * Returns the {@link YangInstanceIdentifier} of the instance being referenced.
+ *
+ * @return the {@link YangInstanceIdentifier} of the instance being referenced,
+ * {@link YangInstanceIdentifier#empty()} denotes the datastora
+ */
+ YangInstanceIdentifier instance();
+ }
+
+ /**
+ * An intermediate trait of {@link Path}s which are referencing a YANG operation. This can be either
+ * an {@link Action} on an {@link Rpc}.
+ */
+ @NonNullByDefault
+ public sealed interface OperationPath extends Path {
+ /**
+ * Returns the {@code input} statement of this operation.
+ *
+ * @return the {@code input} statement of this operation
+ */
+ InputEffectiveStatement inputStatement();
+
+ /**
+ * Returns the {@code output} statement of this operation.
+ *
+ * @return the {@code output} statement of this operation
+ */
+ OutputEffectiveStatement outputStatement();
+ }
+
private final @NonNull DatabindContext databind;
public ApiPathNormalizer(final DatabindContext databind) {
this.databind = requireNonNull(databind);
- modelContext = databind.modelContext();
}
public @NonNull Path normalizePath(final ApiPath apiPath) {
final var it = apiPath.steps().iterator();
if (!it.hasNext()) {
- return new DataPath(Inference.ofDataTreePath(modelContext), YangInstanceIdentifier.of(),
+ return new Data(Inference.ofDataTreePath(databind.modelContext()), YangInstanceIdentifier.of(),
databind.schemaTree().getRoot());
}
var qname = step.identifier().bindTo(namespace);
// We go through more modern APIs here to get this special out of the way quickly
+ final var modelContext = databind.modelContext();
final var optRpc = modelContext.findModuleStatement(namespace).orElseThrow()
.findSchemaTreeNode(RpcEffectiveStatement.class, qname);
if (optRpc.isPresent()) {
final var stack = SchemaInferenceStack.of(modelContext);
final var stmt = stack.enterSchemaTree(rpc.argument());
verify(rpc.equals(stmt), "Expecting %s, inferred %s", rpc, stmt);
- return new OperationPath.Rpc(stack.toInference(), rpc);
+ return new Rpc(stack.toInference(), rpc);
}
final var stack = SchemaInferenceStack.of(modelContext);
final var actionStmt = action.asEffectiveStatement();
verify(actionStmt.equals(stmt), "Expecting %s, inferred %s", actionStmt, stmt);
- return new OperationPath.Action(stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
+ return new Action(stack.toInference(), YangInstanceIdentifier.of(path), actionStmt);
}
}
if (step instanceof ListInstance listStep) {
final var values = listStep.keyValues();
final var schema = childNode.dataSchemaNode();
- pathArg = schema instanceof ListSchemaNode listSchema
- ? prepareNodeWithPredicates(stack, qname, listSchema, values)
- : prepareNodeWithValue(stack, qname, schema, values);
+ if (schema instanceof ListSchemaNode listSchema) {
+ pathArg = prepareNodeWithPredicates(stack, qname, listSchema, values);
+ } else if (schema instanceof LeafListSchemaNode leafListSchema) {
+ if (values.size() != 1) {
+ throw new RestconfDocumentedException("Entry '" + qname + "' requires one value predicate.",
+ ErrorType.PROTOCOL, ErrorTag.BAD_ATTRIBUTE);
+ }
+ pathArg = new NodeWithValue<>(qname, parserJsonValue(stack, leafListSchema, values.get(0)));
+ } else {
+ throw new RestconfDocumentedException(
+ "Entry '" + qname + "' does not take a key or value predicate.",
+ ErrorType.PROTOCOL, ErrorTag.MISSING_ATTRIBUTE);
+ }
} else {
if (childNode.dataSchemaNode() instanceof ListSchemaNode list && !list.getKeyDefinition().isEmpty()) {
throw new RestconfDocumentedException(
path.add(pathArg);
if (!it.hasNext()) {
- return new DataPath(stack.toInference(), YangInstanceIdentifier.of(path), childNode);
+ return new Data(stack.toInference(), YangInstanceIdentifier.of(path), childNode);
}
parentNode = childNode;
}
}
- public @NonNull DataPath normalizeDataPath(final ApiPath apiPath) {
+ public @NonNull Data normalizeDataPath(final ApiPath apiPath) {
final var path = normalizePath(apiPath);
- if (path instanceof DataPath dataPath) {
+ if (path instanceof Data dataPath) {
return dataPath;
}
throw new RestconfDocumentedException("Point '" + apiPath + "' resolves to non-data " + path,
@Override
public PathArgument normalizePoint(final ApiPath value) {
final var path = normalizePath(value);
- if (path instanceof DataPath dataPath) {
+ if (path instanceof Data dataPath) {
final var lastArg = dataPath.instance().getLastPathArgument();
if (lastArg != null) {
return lastArg;
throw new IllegalArgumentException("Point '" + value + "' resolves to non-data " + path);
}
- public @NonNull Rpc normalizeRpcPath(final ApiPath apiPath) {
+ public Path.@NonNull Rpc normalizeRpcPath(final ApiPath apiPath) {
final var steps = apiPath.steps();
return switch (steps.size()) {
case 0 -> throw new RestconfDocumentedException("RPC name must be present", ErrorType.PROTOCOL,
};
}
- public @NonNull Rpc normalizeRpcPath(final ApiPath.Step step) {
+ public Path.@NonNull Rpc normalizeRpcPath(final ApiPath.Step step) {
final var firstModule = step.module();
if (firstModule == null) {
throw new RestconfDocumentedException(
final var namespace = resolveNamespace(firstModule);
final var qname = step.identifier().bindTo(namespace);
- final var stack = SchemaInferenceStack.of(modelContext);
+ final var stack = SchemaInferenceStack.of(databind.modelContext());
final SchemaTreeEffectiveStatement<?> stmt;
try {
stmt = stack.enterSchemaTree(qname);
}
public @NonNull InstanceReference normalizeDataOrActionPath(final ApiPath apiPath) {
-
-
// FIXME: optimize this
final var path = normalizePath(apiPath);
- if (path instanceof DataPath dataPath) {
+ if (path instanceof Data dataPath) {
return dataPath;
}
- if (path instanceof OperationPath.Action actionPath) {
+ if (path instanceof Action actionPath) {
return actionPath;
}
throw new RestconfDocumentedException("Unexpected path " + path, ErrorType.PROTOCOL, ErrorTag.DATA_MISSING);
private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode,
final @NonNull String value) {
-
- TypeDefinition<? extends TypeDefinition<?>> typedef;
- if (schemaNode instanceof LeafListSchemaNode leafList) {
- typedef = leafList.getType();
- } else {
- typedef = ((LeafSchemaNode) schemaNode).getType();
- }
- if (typedef instanceof LeafrefTypeDefinition leafref) {
- typedef = stack.resolveLeafref(leafref);
- }
-
- if (typedef instanceof IdentityrefTypeDefinition) {
- return toIdentityrefQName(value, schemaNode);
+ if (schemaNode instanceof TypedDataSchemaNode typedSchema) {
+ return parserJsonValue(stack, typedSchema, value);
}
- if (typedef instanceof InstanceIdentifierTypeDefinition) {
- return toInstanceIdentifier(value, schemaNode);
- }
-
- try {
- return verifyNotNull(TypeDefinitionAwareCodec.from(typedef),
- "Unhandled type %s decoding %s", typedef, value).deserialize(value);
- } catch (IllegalArgumentException e) {
- throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
- ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
- }
- }
-
- private NodeWithValue<?> prepareNodeWithValue(final SchemaInferenceStack stack, final QName qname,
- final DataSchemaNode schema, final List<String> keyValues) {
- // TODO: qname should be always equal to schema.getQName(), right?
- return new NodeWithValue<>(qname, prepareValueByType(stack, schema,
- // FIXME: ahem: we probably want to do something differently here
- keyValues.get(0)));
+ throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + value + "'");
}
- private YangInstanceIdentifier toInstanceIdentifier(final String value, final DataSchemaNode schemaNode) {
- if (value.isEmpty() || !value.startsWith("/")) {
- throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
- ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
- }
-
+ private Object parserJsonValue(final SchemaInferenceStack stack, final TypedDataSchemaNode schemaNode,
+ final String value) {
+ // As per https://www.rfc-editor.org/rfc/rfc8040#page-29:
+ // The syntax for
+ // "api-identifier" and "key-value" MUST conform to the JSON identifier
+ // encoding rules in Section 4 of [RFC7951]: The RESTCONF root resource
+ // path is required. Additional sub-resource identifiers are optional.
+ // The characters in a key value string are constrained, and some
+ // characters need to be percent-encoded, as described in Section 3.5.3.
try {
- return normalizeDataPath(ApiPath.parse(value.substring(1))).instance();
- } catch (ParseException | RestconfDocumentedException e) {
+ return databind.jsonCodecs().codecFor(schemaNode, stack).parseValue(null, value);
+ } catch (IllegalArgumentException e) {
throw new RestconfDocumentedException("Invalid value '" + value + "' for " + schemaNode.getQName(),
ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE, e);
}
}
- private QName toIdentityrefQName(final String value, final DataSchemaNode schemaNode) {
- final QNameModule namespace;
- final String localName;
- final int firstColon = value.indexOf(':');
- if (firstColon != -1) {
- namespace = resolveNamespace(value.substring(0, firstColon));
- localName = value.substring(firstColon + 1);
- } else {
- namespace = schemaNode.getQName().getModule();
- localName = value;
- }
-
- return modelContext.getModuleStatement(namespace)
- .streamEffectiveSubstatements(IdentityEffectiveStatement.class)
- .map(IdentityEffectiveStatement::argument)
- .filter(qname -> localName.equals(qname.getLocalName()))
- .findFirst()
- .orElseThrow(() -> new RestconfDocumentedException(
- "No identity found for '" + localName + "' in namespace " + namespace,
- ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE));
- }
-
private @NonNull QNameModule resolveNamespace(final String moduleName) {
- final var it = modelContext.findModuleStatements(moduleName).iterator();
+ final var it = databind.modelContext().findModuleStatements(moduleName).iterator();
if (it.hasNext()) {
return it.next().localQNameModule();
}