Report ServerException from ApiPathCanonizer 28/111728/3
authorRobert Varga <robert.varga@pantheon.tech>
Sun, 19 May 2024 13:29:45 +0000 (15:29 +0200)
committerRobert Varga <nite@hq.sk>
Sun, 19 May 2024 15:45:55 +0000 (15:45 +0000)
This is the next step in RDE eradication.

JIRA: NETCONF-1188
Change-Id: I9123b37849636e7d38167b280265d0d900565a96
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/nb/rfc8040/rests/transactions/RestconfStrategy.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/devnotif/SubscribeDeviceNotificationRpc.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/mdsal/streams/dtcl/CreateDataChangeEventSubscriptionRpc.java
restconf/restconf-nb/src/main/java/org/opendaylight/restconf/server/spi/ApiPathCanonizer.java
restconf/restconf-nb/src/test/java/org/opendaylight/restconf/server/spi/ApiPathCanonizerTest.java

index 884e9ac38b40311dcb3fbe5baa5d704b27ce5295..cc27d74014bff786f51a3b448e20f66b5302752a 100644 (file)
@@ -516,9 +516,17 @@ public abstract class RestconfStrategy implements DatabindAware {
         Futures.addCallback(future, new FutureCallback<CommitInfo>() {
             @Override
             public void onSuccess(final CommitInfo result) {
-                ret.set(new CreateResourceResult(new ApiPathCanonizer(databind).dataToApiPath(
-                    data instanceof MapNode mapData && !mapData.isEmpty()
-                        ? path.node(mapData.body().iterator().next().name()) : path)));
+                final ApiPath apiPath;
+                try {
+                    apiPath = new ApiPathCanonizer(databind).dataToApiPath(
+                        data instanceof MapNode mapData && !mapData.isEmpty()
+                        ? path.node(mapData.body().iterator().next().name()) : path);
+                } catch (ServerException e) {
+                    // This should never happen
+                    ret.setFailure(e.toLegacy());
+                    return;
+                }
+                ret.set(new CreateResourceResult(apiPath));
             }
 
             @Override
index 75529b3f60bd22732857f5a5763d71488032f6a4..99de8f847c7aa62b2293415858db7b9b760299da 100644 (file)
@@ -13,8 +13,10 @@ import java.net.URI;
 import javax.inject.Inject;
 import javax.inject.Singleton;
 import org.opendaylight.mdsal.dom.api.DOMMountPointService;
+import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.server.api.ServerException;
 import org.opendaylight.restconf.server.spi.ApiPathCanonizer;
 import org.opendaylight.restconf.server.spi.OperationInput;
 import org.opendaylight.restconf.server.spi.RestconfStream;
@@ -79,9 +81,15 @@ public final class SubscribeDeviceNotificationRpc extends RpcImplementation {
                 ErrorType.APPLICATION, ErrorTag.INVALID_VALUE));
         }
 
+        final ApiPath apiPath;
+        try {
+            apiPath = new ApiPathCanonizer(input.path().databind()).dataToApiPath(path);
+        } catch (ServerException e) {
+            return RestconfFuture.failed(e.toLegacy());
+        }
+
         return streamRegistry.createStream(restconfURI, new DeviceNotificationSource(mountPointService, path),
-            "All YANG notifications occuring on mount point /"
-                + new ApiPathCanonizer(input.path().databind()).dataToApiPath(path).toString())
+            "All YANG notifications occuring on mount point /" + apiPath.toString())
             .transform(stream -> ImmutableNodes.newContainerBuilder()
                 .withNodeIdentifier(new NodeIdentifier(SubscribeDeviceNotificationOutput.QNAME))
                 .withChild(ImmutableNodes.leafNode(DEVICE_NOTIFICATION_STREAM_NAME_NODEID, stream.name()))
index e12d7867e4b06cacbbf805de257dd22eab9dca6f..148fd1cc4ccd767bde6453d5099f50f59230c4e4 100644 (file)
@@ -16,8 +16,10 @@ import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.mdsal.common.api.LogicalDatastoreType;
 import org.opendaylight.mdsal.dom.api.DOMDataBroker;
 import org.opendaylight.mdsal.dom.api.DOMDataBroker.DataTreeChangeExtension;
+import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
 import org.opendaylight.restconf.common.errors.RestconfFuture;
+import org.opendaylight.restconf.server.api.ServerException;
 import org.opendaylight.restconf.server.spi.ApiPathCanonizer;
 import org.opendaylight.restconf.server.spi.DatabindProvider;
 import org.opendaylight.restconf.server.spi.OperationInput;
@@ -108,10 +110,16 @@ public final class CreateDataChangeEventSubscriptionRpc extends RpcImplementatio
                 new RestconfDocumentedException("missing path", ErrorType.APPLICATION, ErrorTag.MISSING_ELEMENT));
         }
 
+        final ApiPath apiPath;
+        try {
+            apiPath = new ApiPathCanonizer(input.path().databind()).dataToApiPath(path);
+        } catch (ServerException e) {
+            return RestconfFuture.failed(e.toLegacy());
+        }
+
         return streamRegistry.createStream(restconfURI,
             new DataTreeChangeSource(databindProvider, changeService, datastore, path),
-            "Events occuring in " + datastore + " datastore under /"
-                + new ApiPathCanonizer(input.path().databind()).dataToApiPath(path).toString())
+            "Events occuring in " + datastore + " datastore under /" + apiPath.toString())
             .transform(stream -> ImmutableNodes.newContainerBuilder()
                 .withNodeIdentifier(OUTPUT_NODEID)
                 .withChild(ImmutableNodes.leafNode(STREAM_NAME_NODEID, stream.name()))
index 8b0b21210bfd1415d012a1fc99fea38eff1648f4..e9955a3aee7a1ab618706210c8a782b83d304bf9 100644 (file)
@@ -16,8 +16,8 @@ import org.opendaylight.restconf.api.ApiPath;
 import org.opendaylight.restconf.api.ApiPath.ApiIdentifier;
 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.server.api.DatabindContext;
+import org.opendaylight.restconf.server.api.ServerException;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
 import org.opendaylight.yangtools.yang.common.QName;
@@ -53,8 +53,9 @@ public final class ApiPathCanonizer {
      *
      * @param path {@link YangInstanceIdentifier} to canonicalize
      * @return An {@link ApiPath}
+     * @throws ServerException if an error occurs
      */
-    public @NonNull ApiPath dataToApiPath(final YangInstanceIdentifier path) {
+    public @NonNull ApiPath dataToApiPath(final YangInstanceIdentifier path) throws ServerException {
         final var it = path.getPathArguments().iterator();
         if (!it.hasNext()) {
             return ApiPath.empty();
@@ -74,9 +75,9 @@ public final class ApiPathCanonizer {
 
             final var childContext = context instanceof Composite composite ? composite.enterChild(stack, arg) : null;
             if (childContext == null) {
-                throw new RestconfDocumentedException(
-                    "Invalid input '%s': schema for argument '%s' (after '%s') not found".formatted(path, arg,
-                        ApiPath.of(builder.build())), ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT);
+                throw new ServerException(ErrorType.APPLICATION, ErrorTag.UNKNOWN_ELEMENT,
+                    "Invalid input '%s': schema for argument '%s' (after '%s') not found"path, arg,
+                    ApiPath.of(builder.build()));
             }
 
             context = childContext;
@@ -91,7 +92,7 @@ public final class ApiPathCanonizer {
     }
 
     private @NonNull Step argToStep(final PathArgument arg, final QNameModule prevNamespace,
-            final SchemaInferenceStack stack, final DataSchemaContext context) {
+            final SchemaInferenceStack stack, final DataSchemaContext context) throws ServerException {
         // append namespace before every node which is defined in other module than its parent
         // condition is satisfied also for the first path argument
         final var nodeType = arg.getNodeType();
@@ -107,9 +108,8 @@ public final class ApiPathCanonizer {
         final var schema = context.dataSchemaNode();
         if (arg instanceof NodeWithValue<?> withValue) {
             if (!(schema instanceof LeafListSchemaNode leafList)) {
-                throw new RestconfDocumentedException(
-                    "Argument '%s' does not map to a leaf-list, but %s".formatted(arg, schema),
-                    ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+                throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
+                    "Argument '%s' does not map to a leaf-list, but %s", arg, schema);
             }
             return ListInstance.of(module, identifier, valueToString(stack, leafList, withValue.getValue()));
         }
@@ -121,23 +121,20 @@ public final class ApiPathCanonizer {
         // A NodeIdentifierWithPredicates adresses a MapEntryNode and maps to a ListInstance with one or more values:
         // 1) schema has to be a ListSchemaNode
         if (!(schema instanceof ListSchemaNode list)) {
-            throw new RestconfDocumentedException(
-                "Argument '%s' does not map to a list, but %s".formatted(arg, schema),
-                ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+            throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
+                "Argument '%s' does not map to a list, but %s", arg, schema);
         }
         // 2) the key definition must be non-empty
         final var keyDef = list.getKeyDefinition();
         final var size = keyDef.size();
         if (size == 0) {
-            throw new RestconfDocumentedException(
-                "Argument '%s' maps a list without any keys %s".formatted(arg, schema),
-                ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+            throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
+                "Argument '%s' maps a list without any keys %s", arg, schema);
         }
         // 3) the number of predicates has to match the number of keys
         if (size != withPredicates.size()) {
-            throw new RestconfDocumentedException(
-                "Argument '%s' does not match required keys %s".formatted(arg, keyDef),
-                ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+            throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
+                "Argument '%s' does not match required keys %s", arg, keyDef);
         }
 
         // ListSchemaNode implies the context is a composite, verify that instead of an unexplained cast when we look
@@ -150,8 +147,8 @@ public final class ApiPathCanonizer {
         for (var key : keyDef) {
             final var value = withPredicates.getValue(key);
             if (value == null) {
-                throw new RestconfDocumentedException("Argument '%s' is missing predicate for %s".formatted(arg, key),
-                    ErrorType.APPLICATION, ErrorTag.INVALID_VALUE);
+                throw new ServerException(ErrorType.APPLICATION, ErrorTag.INVALID_VALUE,
+                    "Argument '%s' is missing predicate for %s", arg, key);
             }
 
             final var tmpStack = stack.copy();
index bab88e8b8dd547040dad8c1cba61c2026ac2f3f8..716d20677079485602c61ff4404b7309acb5aac9 100644 (file)
@@ -16,10 +16,11 @@ import java.text.ParseException;
 import java.util.Map;
 import org.junit.jupiter.api.Test;
 import org.opendaylight.restconf.api.ApiPath;
+import org.opendaylight.restconf.api.ErrorMessage;
 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
-import org.opendaylight.restconf.common.errors.RestconfError;
 import org.opendaylight.restconf.server.api.DatabindContext;
 import org.opendaylight.restconf.server.api.DatabindPath.Data;
+import org.opendaylight.restconf.server.api.ServerError;
 import org.opendaylight.restconf.server.api.ServerException;
 import org.opendaylight.yangtools.yang.common.ErrorTag;
 import org.opendaylight.yangtools.yang.common.ErrorType;
@@ -110,11 +111,11 @@ class ApiPathCanonizerTest {
             .node(QName.create("serializer:test", "2016-06-06", "list-one-key"))
             .nodeWithKey(QName.create("serializer:test", "2016-06-06", "list-one-key"), Map.of())
             .build());
-        assertEquals("""
+        assertEquals(new ErrorMessage("""
             Argument '(serializer:test?revision=2016-06-06)list-one-key[{}]' does not match required keys \
-            [(serializer:test?revision=2016-06-06)name]""", error.getErrorMessage());
-        assertEquals(ErrorType.APPLICATION, error.getErrorType());
-        assertEquals(ErrorTag.INVALID_VALUE, error.getErrorTag());
+            [(serializer:test?revision=2016-06-06)name]"""), error.message());
+        assertEquals(ErrorType.APPLICATION, error.type());
+        assertEquals(ErrorTag.INVALID_VALUE, error.tag());
     }
 
     /**
@@ -229,12 +230,12 @@ class ApiPathCanonizerTest {
         final var error = assertError(YangInstanceIdentifier.of(
             QName.create("serializer:test", "2016-06-06", "contA"),
             QName.create("serializer:test", "2016-06-06", "not-existing-leaf")));
-        assertEquals("""
+        assertEquals(new ErrorMessage("""
             Invalid input '/(serializer:test?revision=2016-06-06)contA/not-existing-leaf': schema for argument \
-            '(serializer:test?revision=2016-06-06)not-existing-leaf' (after 'serializer-test:contA') not found""",
-            error.getErrorMessage());
-        assertEquals(ErrorType.APPLICATION, error.getErrorType());
-        assertEquals(ErrorTag.UNKNOWN_ELEMENT, error.getErrorTag());
+            '(serializer:test?revision=2016-06-06)not-existing-leaf' (after 'serializer-test:contA') not found"""),
+            error.message());
+        assertEquals(ErrorType.APPLICATION, error.type());
+        assertEquals(ErrorTag.UNKNOWN_ELEMENT, error.tag());
     }
 
     /**
@@ -296,13 +297,13 @@ class ApiPathCanonizerTest {
             // child should has different namespace
             .node(QName.create("serializer:test:included", "2016-06-06", "augmented-leaf"))
             .build());
-        assertEquals("""
+        assertEquals(new ErrorMessage("""
             Invalid input '/(serializer:test:included?revision=2016-06-06)augmented-list/augmented-list[{(\
             serializer:test:included?revision=2016-06-06)list-key=100}]/augmented-leaf': schema for argument \
             '(serializer:test:included?revision=2016-06-06)augmented-leaf' (after \
-            'serializer-test-included:augmented-list=100') not found""", error.getErrorMessage());
-        assertEquals(ErrorType.APPLICATION, error.getErrorType());
-        assertEquals(ErrorTag.UNKNOWN_ELEMENT, error.getErrorTag());
+            'serializer-test-included:augmented-list=100') not found"""), error.message());
+        assertEquals(ErrorType.APPLICATION, error.type());
+        assertEquals(ErrorTag.UNKNOWN_ELEMENT, error.tag());
     }
 
     /**
@@ -350,7 +351,11 @@ class ApiPathCanonizerTest {
     }
 
     private static void assertApiPath(final String expected, final YangInstanceIdentifier path) {
-        assertEquals(newApiPath(expected), CANONIZER.dataToApiPath(path));
+        try {
+            assertEquals(newApiPath(expected), CANONIZER.dataToApiPath(path));
+        } catch (ServerException e) {
+            throw new AssertionError(e);
+        }
     }
 
     private static YangInstanceIdentifier assertNormalized(final String str) {
@@ -361,11 +366,8 @@ class ApiPathCanonizerTest {
         }
     }
 
-    private static RestconfError assertError(final YangInstanceIdentifier path) {
-        final var ex = assertThrows(RestconfDocumentedException.class, () -> CANONIZER.dataToApiPath(path));
-        final var errors = ex.getErrors();
-        assertEquals(1, errors.size());
-        return errors.get(0);
+    private static ServerError assertError(final YangInstanceIdentifier path) {
+        return assertThrows(ServerException.class, () -> CANONIZER.dataToApiPath(path)).error();
     }
 
     private static ApiPath newApiPath(final String apiPath) {