X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=blobdiff_plain;f=restconf%2Frestconf-nb%2Fsrc%2Fmain%2Fjava%2Forg%2Fopendaylight%2Frestconf%2Fserver%2Fspi%2FApiPathNormalizer.java;h=8505e0486352bc1dd93ca1810d534d606afff1e3;hb=76b38ec7221f507106a99b7b0e0a95c78e124559;hp=5cae3c8e6b527a1bd49db212a2971dc3537adcd8;hpb=32fad0d2b1161304ff9a9d18ea2f4e72dff591d5;p=netconf.git diff --git a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/ApiPathNormalizer.java b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/ApiPathNormalizer.java index 5cae3c8e6b..8505e04863 100644 --- a/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/ApiPathNormalizer.java +++ b/restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/ApiPathNormalizer.java @@ -11,17 +11,24 @@ import static com.google.common.base.Verify.verify; 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.util.ArrayList; +import java.util.Iterator; import java.util.List; import org.eclipse.jdt.annotation.NonNull; -import org.eclipse.jdt.annotation.NonNullByDefault; import org.opendaylight.restconf.api.ApiPath; import org.opendaylight.restconf.api.ApiPath.ListInstance; +import org.opendaylight.restconf.api.ApiPath.Step; import org.opendaylight.restconf.common.errors.RestconfDocumentedException; import org.opendaylight.restconf.nb.rfc8040.Insert.PointNormalizer; +import org.opendaylight.restconf.server.api.DatabindAware; import org.opendaylight.restconf.server.api.DatabindContext; -import org.opendaylight.restconf.server.spi.ApiPathNormalizer.OperationPath.Rpc; +import org.opendaylight.restconf.server.api.DatabindPath; +import org.opendaylight.restconf.server.api.DatabindPath.Action; +import org.opendaylight.restconf.server.api.DatabindPath.Data; +import org.opendaylight.restconf.server.api.DatabindPath.InstanceReference; +import org.opendaylight.restconf.server.api.DatabindPath.Rpc; import org.opendaylight.yangtools.yang.common.ErrorTag; import org.opendaylight.yangtools.yang.common.ErrorType; import org.opendaylight.yangtools.yang.common.QName; @@ -30,100 +37,42 @@ import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier; 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.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.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.TypedDataSchemaNode; 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 DatabindPath} interface hierarchy. + * + *

+ * This process is governed by + * RFC8040, section 3.5.3. The URI provides the + * equivalent of NETCONF XML filter encoding, with data values being escaped RFC7891 strings. */ -public final class ApiPathNormalizer implements PointNormalizer { - @NonNullByDefault - public sealed interface 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(); - - record Action(Inference inference, YangInstanceIdentifier instance, ActionEffectiveStatement action) - implements OperationPath, InstanceReference { - public Action { - requireNonNull(inference); - requireNonNull(action); - requireNonNull(instance); - } - - @Override - public InputEffectiveStatement inputStatement() { - return action.input(); - } - } - - record Rpc(Inference inference, RpcEffectiveStatement rpc) implements OperationPath { - public Rpc { - requireNonNull(inference); - requireNonNull(rpc); - } - - @Override - public InputEffectiveStatement inputStatement() { - return rpc.input(); - } - } - } - - private final @NonNull ApiPathInstanceIdentifierCodec instanceIdentifierCodec; - private final @NonNull EffectiveModelContext modelContext; +public final class ApiPathNormalizer implements DatabindAware, PointNormalizer { private final @NonNull DatabindContext databind; public ApiPathNormalizer(final DatabindContext databind) { this.databind = requireNonNull(databind); - modelContext = databind.modelContext(); - instanceIdentifierCodec = new ApiPathInstanceIdentifierCodec(databind); } - public @NonNull Path normalizePath(final ApiPath apiPath) { + @Override + public DatabindContext databind() { + return databind; + } + + public @NonNull DatabindPath normalizePath(final ApiPath apiPath) { final var it = apiPath.steps().iterator(); if (!it.hasNext()) { - return new DataPath(Inference.ofDataTreePath(modelContext), YangInstanceIdentifier.of(), - databind.schemaTree().getRoot()); + return new Data(databind); } // First step is somewhat special: @@ -131,18 +80,20 @@ public final class ApiPathNormalizer implements PointNormalizer { // - it has to consider RPCs, for which we need SchemaContext // // We therefore peel that first iteration here and not worry about those details in further iterations - var step = it.next(); - final var firstModule = step.module(); + final var firstStep = it.next(); + final var firstModule = firstStep.module(); if (firstModule == null) { throw new RestconfDocumentedException( - "First member must use namespace-qualified form, '" + step.identifier() + "' does not", + "First member must use namespace-qualified form, '" + firstStep.identifier() + "' does not", ErrorType.PROTOCOL, ErrorTag.MALFORMED_MESSAGE); } var namespace = resolveNamespace(firstModule); + var step = firstStep; 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()) { @@ -161,12 +112,22 @@ public final class ApiPathNormalizer implements PointNormalizer { 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(databind, stack.toInference(), rpc); } - final var stack = SchemaInferenceStack.of(modelContext); - final var path = new ArrayList(); - DataSchemaContext parentNode = databind.schemaTree().getRoot(); + return normalizeSteps(SchemaInferenceStack.of(modelContext), databind.schemaTree().getRoot(), List.of(), + namespace, firstStep, it); + } + + @NonNull DatabindPath normalizeSteps(final SchemaInferenceStack stack, final @NonNull DataSchemaContext rootNode, + final @NonNull List pathPrefix, final @NonNull QNameModule firstNamespace, + final @NonNull Step firstStep, final Iterator<@NonNull Step> it) { + var parentNode = rootNode; + var namespace = firstNamespace; + var step = firstStep; + var qname = step.identifier().bindTo(namespace); + + final var path = new ArrayList<>(pathPrefix); while (true) { final var parentSchema = parentNode.dataSchemaNode(); if (parentSchema instanceof ActionNodeContainer actionParent) { @@ -188,7 +149,7 @@ public final class ApiPathNormalizer implements PointNormalizer { 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(databind, stack.toInference(), YangInstanceIdentifier.of(path), actionStmt); } } @@ -212,9 +173,19 @@ public final class ApiPathNormalizer implements PointNormalizer { 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( @@ -227,7 +198,7 @@ public final class ApiPathNormalizer implements PointNormalizer { path.add(pathArg); if (!it.hasNext()) { - return new DataPath(stack.toInference(), YangInstanceIdentifier.of(path), childNode); + return new Data(databind, stack.toInference(), YangInstanceIdentifier.of(path), childNode); } parentNode = childNode; @@ -241,9 +212,9 @@ public final class ApiPathNormalizer implements PointNormalizer { } } - 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, @@ -253,7 +224,7 @@ public final class ApiPathNormalizer implements PointNormalizer { @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; @@ -284,7 +255,7 @@ public final class ApiPathNormalizer implements PointNormalizer { 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); @@ -293,21 +264,19 @@ public final class ApiPathNormalizer implements PointNormalizer { ErrorTag.DATA_MISSING, e); } if (stmt instanceof RpcEffectiveStatement rpc) { - return new Rpc(stack.toInference(), rpc); + return new Rpc(databind, stack.toInference(), rpc); } throw new RestconfDocumentedException(qname + " does not refer to an RPC", ErrorType.PROTOCOL, ErrorTag.DATA_MISSING); } 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); @@ -339,66 +308,31 @@ public final class ApiPathNormalizer implements PointNormalizer { private Object prepareValueByType(final SchemaInferenceStack stack, final DataSchemaNode schemaNode, final @NonNull String value) { - - 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); } + throw new VerifyException("Unhandled schema " + schemaNode + " decoding '" + 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 { - if (typedef instanceof InstanceIdentifierTypeDefinition) { - return instanceIdentifierCodec.deserialize(value); - } - - return verifyNotNull(TypeDefinitionAwareCodec.from(typedef), - "Unhandled type %s decoding %s", typedef, value).deserialize(value); + 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 NodeWithValue prepareNodeWithValue(final SchemaInferenceStack stack, final QName qname, - final DataSchemaNode schema, final List 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))); - } - - 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(); }