Design a Capability object model 46/105846/30
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 5 May 2023 00:21:03 +0000 (02:21 +0200)
committerIvan Hrasko <ivan.hrasko@pantheon.tech>
Thu, 27 Jun 2024 13:10:47 +0000 (13:10 +0000)
Implemented YangModuleCapability and EXiCapability
which extend ParameterizedCapability.
These classes appear to represent YANG module capabilities
and have attributes like content, revision, module name,
features, deviations, etc.
Added enumeration of SimpleCapability which implements
the Capability interface, listing various NETCONF
capabilities based on different RFCs and drafts.

JIRA: NETCONF-1015
Change-Id: I6c935dc4818e1968d656ce00aa08fb82f7650fc5
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
Signed-off-by: Yaroslav Lastivka <yaroslav.lastivka@pantheon.tech>
protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/Capability.java [new file with mode: 0644]
protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/ExiCapability.java [new file with mode: 0644]
protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/ExiSchemas.java [new file with mode: 0644]
protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/ParameterizedCapability.java [new file with mode: 0644]
protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/SimpleCapability.java [new file with mode: 0644]
protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/YangModuleCapability.java [new file with mode: 0644]
protocol/netconf-api/src/test/java/org/opendaylight/netconf/api/capability/ParameterizedCapabilityTest.java [new file with mode: 0644]
protocol/netconf-api/src/test/java/org/opendaylight/netconf/api/capability/ProtocolCapabilityTest.java [new file with mode: 0644]

diff --git a/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/Capability.java b/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/Capability.java
new file mode 100644 (file)
index 0000000..4b8ebc7
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2023 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.netconf.api.capability;
+
+import java.net.URI;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.netconf.api.messages.HelloMessage;
+
+/**
+ * Contains capability URI announced by server hello message and optionally its
+ * corresponding yang schema that can be retrieved by get-schema rpc.
+ */
+@NonNullByDefault
+public sealed interface Capability permits SimpleCapability, ParameterizedCapability {
+    /**
+     * Return this capability's URN.
+     *
+     * @return An URN string
+     */
+    String urn();
+
+    /**
+     * Return this capability formatted as a URI, suitable for encoding as a {@link HelloMessage} advertisement.
+     *
+     * @return A URI
+     */
+    default URI toURI() {
+        return URI.create(urn());
+    }
+}
diff --git a/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/ExiCapability.java b/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/ExiCapability.java
new file mode 100644 (file)
index 0000000..80c3c16
--- /dev/null
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2023 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.netconf.api.capability;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.netconf.api.CapabilityURN;
+
+/**
+ * A capability representing a EXI Capability, as defined in
+ * <a href="https://datatracker.ietf.org/doc/html/draft-varga-netconf-exi-capability-02#section-3">
+ * Efficient XML Interchange Capability for NETCONF, section 3.1</a>.
+ */
+public record ExiCapability(
+        Integer compression,
+        ExiSchemas schemas,
+        @NonNull String urn) implements ParameterizedCapability {
+    private static final @NonNull String COMPRESSION_PARAM = "compression";
+    private static final @NonNull String SCHEMAS_PARAM = "schemas";
+    private static final @NonNull Set<String> PARAMETERS = ImmutableSet.of(COMPRESSION_PARAM, SCHEMAS_PARAM);
+
+    public ExiCapability(final Integer compression, final ExiSchemas schemas) {
+        this(compression, schemas, buildUrn(compression, schemas));
+    }
+
+    private static String buildUrn(final Integer compression, final ExiSchemas schemas) {
+        final var sb = new StringBuilder(CapabilityURN.EXI);
+        boolean isFirstParam = true;
+        if (compression != null) {
+            sb.append("?").append(COMPRESSION_PARAM).append("=").append(compression);
+            isFirstParam = false;
+        }
+        if (schemas != null) {
+            sb.append(isFirstParam ? "?" : "&");
+            sb.append(SCHEMAS_PARAM).append("=").append(schemas.getValue());
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public String urn() {
+        return urn;
+    }
+
+    @Override
+    public Set<String> parameterNames() {
+        return PARAMETERS;
+    }
+}
diff --git a/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/ExiSchemas.java b/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/ExiSchemas.java
new file mode 100644 (file)
index 0000000..6686ac2
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * 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.netconf.api.capability;
+
+public enum ExiSchemas {
+    BUILTIN("builtin"),
+    BASE_1_1("base:1.1"),
+    DYNAMIC("dynamic");
+
+    private final String value;
+
+    ExiSchemas(final String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+}
diff --git a/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/ParameterizedCapability.java b/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/ParameterizedCapability.java
new file mode 100644 (file)
index 0000000..01ceccb
--- /dev/null
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2023 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.netconf.api.capability;
+
+import java.util.Set;
+
+/**
+ * A {@link Capability} which defines a set of URI query parameters.
+ *
+ */
+public sealed interface ParameterizedCapability extends Capability
+        permits YangModuleCapability, ExiCapability {
+    Set<String> parameterNames();
+}
diff --git a/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/SimpleCapability.java b/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/SimpleCapability.java
new file mode 100644 (file)
index 0000000..7b71a71
--- /dev/null
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2023 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.netconf.api.capability;
+
+import static java.util.Objects.requireNonNull;
+
+import java.net.URI;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.netconf.api.CapabilityURN;
+
+/**
+ * Enumeration of all simple
+ * <a href="https://www.iana.org/assignments/netconf-capability-urns/netconf-capability-urns.xhtml#netconf-capability-urns-1">
+ *     NETCONF capabilities,</a>
+ * i.e. those which do not have any additional parameters.
+ */
+public enum SimpleCapability implements Capability {
+    /**
+     * The base NETCONF capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc4741.html#section-8.1">RFC4741, section 8.1</a>.
+     * @deprecated This capability identifies legacy NETCONF devices and has been superseded by {@link #BASE_1_1}, just
+     *             as RFC6241 obsoletes RFC4741.
+     */
+    @Deprecated
+    BASE(":base:1.0", CapabilityURN.BASE),
+    /**
+     * The base NETCONF capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6241.html#section-8.1">RFC6241, section 8.1</a>.
+     */
+    BASE_1_1(":base:1.1", CapabilityURN.BASE_1_1),
+    /**
+     * The Candidate Configuration Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6241.html#section-8.3">RFC6241, section 8.3</a>.
+     */
+    CANDIDATE(":candidate", CapabilityURN.CANDIDATE),
+    /**
+     * The Candidate Configuration Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc4741.html#section-8.3">RFC4741, section 8.3</a>.
+     * @deprecated This capability is superseded by {@link #CONFIRMED_COMMIT_1_1}.
+     */
+    @Deprecated
+    CONFIRMED_COMMIT(":confirmed-commit", CapabilityURN.CONFIRMED_COMMIT),
+    /**
+     * The Rollback-on-Error Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6241.html#section-8.4">RFC6241, section 8.4</a>.
+     */
+    CONFIRMED_COMMIT_1_1(":confirmed-commit:1.1", CapabilityURN.CONFIRMED_COMMIT_1_1),
+    /**
+     * The Interleave Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc5277.html#section-6">RFC5277, section 6</a>.
+     */
+    INTERLEAVE(":interleave", CapabilityURN.INTERLEAVE),
+    /**
+     * The Validate Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc5277.html#section-3.1">RFC5277, section 3.1</a>.
+     */
+    NOTIFICATION(":notification", CapabilityURN.NOTIFICATION),
+    /**
+     * The Partial Locking Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc5717.html#section-2">RFC5715, section 2</a>.
+     */
+    PARTIAL_LOCK(":partial-lock", CapabilityURN.PARTIAL_LOCK),
+    /**
+     * The Rollback-on-Error Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6241.html#section-8.5">RFC6241, section 8.5</a>.
+     */
+    ROLLBACK_ON_ERROR(":rollback-on-error", CapabilityURN.ROLLBACK_ON_ERROR),
+    /**
+     * The Distinct Startup Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6241.html#section-8.7">RFC6241, section 8.7</a>.
+     */
+    STARTUP(":startup", CapabilityURN.STARTUP),
+    /**
+     * The Time Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc7758.html#section-4">RFC7758, section 4</a>.
+     */
+    TIME(":time:1.0", CapabilityURN.TIME),
+    /**
+     * The URL Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6241.html#section-8.8">RFC6241, section 8.8</a>.
+     */
+    URL(":url", CapabilityURN.URL),
+    /**
+     * The Validate Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc4741.html#section-8.6">RFC4741, section 8.6</a>.
+     * @deprecated This capability is superseded by {@link #VALIDATE_1_1}.
+     */
+    @Deprecated
+    VALIDATE(":validate", CapabilityURN.VALIDATE),
+    /**
+     * The Validate Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6241.html#section-8.6">RFC6241, section 8.6</a>.
+     */
+    VALIDATE_1_1(":validate:1.1", CapabilityURN.VALIDATE_1_1),
+    /**
+     * The XPath Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6241.html#section-8.9">RFC6241, section 8.9</a>.
+     */
+    XPATH(":xpath", CapabilityURN.XPATH),
+    /**
+     * The YANG Module Library Capability, as defined in
+     * <a href="hhttps://www.rfc-editor.org/rfc/rfc7950.html#section-5.6.4">RFC7950, section 5.6.4</a> and further
+     * specified by <a href="https://www.rfc-editor.org/rfc/rfc7895">RFC7895</a>. Note this applies to NETCONF endpoints
+     * which DO NOT support Network Management Datastore Architecture as specified by
+     * <a href="https://www.rfc-editor.org/rfc/rfc8342">RFC8342</a>.
+     */
+    YANG_LIBRARY(":yang-library", CapabilityURN.YANG_LIBRARY),
+    /**
+     * The YANG Library Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc8526.html#section-2">RFC8526, section 2</a> and further specified
+     * by <a href="https://www.rfc-editor.org/rfc/rfc8525">RFC8525</a>. Note this applies to NETCONF endpoints
+     * which DO support Network Management Datastore Architecture as specified by
+     * <a href="https://www.rfc-editor.org/rfc/rfc8342">RFC8342</a>.
+     */
+    YANG_LIBRARY_1_1(":yang-library:1.1", CapabilityURN.YANG_LIBRARY_1_1),
+    /**
+     * The With-defaults Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6243.html#section-4">RFC6243, section 4</a>.
+     */
+    WITH_DEFAULTS(":with-defaults", CapabilityURN.WITH_DEFAULTS),
+    /**
+     * The With-defaults Capability, as augmented by
+     * <a href="https://www.rfc-editor.org/rfc/rfc8526#section-3.1.1.2">RFC8526, section 3.1.1.2</a>.
+     */
+    WITH_OPERATIONAL_DEFAULTS(":with-operational-defaults", CapabilityURN.WITH_OPERATIONAL_DEFAULTS),
+    /**
+     * The Writable-Running Capability, as defined in
+     * <a href="https://www.rfc-editor.org/rfc/rfc6241.html#section-8.2">RFC6241, section 8.2</a>.
+     */
+    WRITABLE_RUNNING(":writable-running", CapabilityURN.WRITABLE_RUNNING);
+
+    private final @NonNull String capabilityName;
+    private final @NonNull String urn;
+    private final @NonNull URI uri;
+
+    SimpleCapability(final String capabilityName, final String urn) {
+        this.capabilityName = requireNonNull(capabilityName);
+        this.urn = requireNonNull(urn);
+        uri = URI.create(urn);
+    }
+
+    public @NonNull String capabilityName() {
+        return capabilityName;
+    }
+
+    @Override
+    public String urn() {
+        return urn;
+    }
+
+    @Override
+    public URI toURI() {
+        return uri;
+    }
+
+    /**
+     * Try to match a capability URN to a {@link SimpleCapability}.
+     *
+     * @param urn URN to match
+     * @return A {@link SimpleCapability}
+     * @throws IllegalArgumentException if {@code urn} is {@code null}
+     */
+    public static @NonNull SimpleCapability ofURN(final String urn) {
+        final var capability = forURN(urn);
+        if (capability == null) {
+            throw new IllegalArgumentException(urn + " does not match a known protocol capability");
+        }
+        return capability;
+    }
+
+    /**
+     * Match a capability URN to a {@link SimpleCapability}.
+     *
+     * @param urn URN to match
+     * @return A {@link SimpleCapability}, or {@code null} the URN does not match a known protocol capability
+     * @throws NullPointerException if {@code urn} is {@code null}
+     */
+    public static SimpleCapability forURN(final String urn) {
+        return switch (urn) {
+            case CapabilityURN.BASE -> BASE;
+            case CapabilityURN.BASE_1_1 -> BASE_1_1;
+            case CapabilityURN.CANDIDATE -> CANDIDATE;
+            case CapabilityURN.CONFIRMED_COMMIT -> CONFIRMED_COMMIT;
+            case CapabilityURN.CONFIRMED_COMMIT_1_1 -> CONFIRMED_COMMIT_1_1;
+            case CapabilityURN.INTERLEAVE -> INTERLEAVE;
+            case CapabilityURN.NOTIFICATION -> NOTIFICATION;
+            case CapabilityURN.PARTIAL_LOCK -> PARTIAL_LOCK;
+            case CapabilityURN.ROLLBACK_ON_ERROR -> ROLLBACK_ON_ERROR;
+            case CapabilityURN.STARTUP -> STARTUP;
+            case CapabilityURN.TIME -> TIME;
+            case CapabilityURN.URL -> URL;
+            case CapabilityURN.VALIDATE -> VALIDATE;
+            case CapabilityURN.VALIDATE_1_1 -> VALIDATE_1_1;
+            case CapabilityURN.WITH_DEFAULTS -> WITH_DEFAULTS;
+            case CapabilityURN.WITH_OPERATIONAL_DEFAULTS -> WITH_OPERATIONAL_DEFAULTS;
+            case CapabilityURN.WRITABLE_RUNNING -> WRITABLE_RUNNING;
+            case CapabilityURN.XPATH -> XPATH;
+            case CapabilityURN.YANG_LIBRARY -> YANG_LIBRARY;
+            case CapabilityURN.YANG_LIBRARY_1_1 -> YANG_LIBRARY_1_1;
+            default -> null;
+        };
+    }
+}
diff --git a/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/YangModuleCapability.java b/protocol/netconf-api/src/main/java/org/opendaylight/netconf/api/capability/YangModuleCapability.java
new file mode 100644 (file)
index 0000000..62e78dc
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2023 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.netconf.api.capability;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.List;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
+
+/**
+ * A capability representing a YANG module, as defined in
+ * <a href="https://datatracker.ietf.org/doc/html/rfc6020#section-5.6.4">RFC6020, section 5.6.4</a>.
+ */
+public record YangModuleCapability(
+        @NonNull String moduleNamespace,
+        String moduleName,
+        String revision,
+        List<String> features,
+        List<String> deviations,
+        String urn) implements ParameterizedCapability {
+    private static final @NonNull String MODULE_PARAM = "module";
+    private static final @NonNull String REVISION_PARAM = "revision";
+    private static final @NonNull String FEATURES_PARAM = "features";
+    private static final @NonNull String DEVIATIONS_PARAM = "deviations";
+    private static final @NonNull Set<String> PARAMETERS = ImmutableSet.of(MODULE_PARAM, REVISION_PARAM, FEATURES_PARAM,
+        DEVIATIONS_PARAM);
+
+    public YangModuleCapability(final @NonNull String moduleNamespace, final String moduleName,
+            final String revision, final List<String> features, final List<String> deviations) {
+        this(requireNonNull(moduleNamespace), moduleName, revision,
+            features == null || features.isEmpty() ? null : List.copyOf(features),
+            deviations == null || deviations.isEmpty() ? null : List.copyOf(deviations),
+            buildUrn(moduleNamespace, moduleName, revision, features, deviations));
+    }
+
+    private static String buildUrn(final String moduleNamespace, final String moduleName, final String revision,
+            final List<String> features, final List<String> deviations) {
+        final var sb = new StringBuilder().append(moduleNamespace);
+        boolean isFirstParam = true;
+        if (moduleName != null) {
+            sb.append("?").append(MODULE_PARAM).append("=").append(moduleName);
+            isFirstParam = false;
+        }
+        if (revision != null) {
+            sb.append(isFirstParam ? "?" : "&");
+            sb.append(REVISION_PARAM).append("=").append(revision);
+            isFirstParam = false;
+        }
+        if (features != null && !features.isEmpty()) {
+            sb.append(isFirstParam ? "?" : "&");
+            sb.append(FEATURES_PARAM).append("=").append(String.join(",", features));
+            isFirstParam = false;
+        }
+        if (deviations != null && !deviations.isEmpty()) {
+            sb.append(isFirstParam ? "?" : "&");
+            sb.append(DEVIATIONS_PARAM).append("=").append(String.join(",", deviations));
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public String urn() {
+        return urn;
+    }
+
+    @Override
+    public Set<String> parameterNames() {
+        return PARAMETERS;
+    }
+}
diff --git a/protocol/netconf-api/src/test/java/org/opendaylight/netconf/api/capability/ParameterizedCapabilityTest.java b/protocol/netconf-api/src/test/java/org/opendaylight/netconf/api/capability/ParameterizedCapabilityTest.java
new file mode 100644 (file)
index 0000000..86bf669
--- /dev/null
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2023 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.netconf.api.capability;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Stream;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class ParameterizedCapabilityTest {
+
+    @ParameterizedTest
+    @MethodSource("provideYangModuleCapabilityParams")
+    void testYangModuleCapabilityUrn(final String namespace, final String module, final String revision,
+            final List<String> features, final List<String> deviations, final String expectedUrn) {
+        final var capability = new YangModuleCapability(namespace, module, revision, features, deviations);
+        assertEquals(expectedUrn, capability.urn());
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideExiCapabilityParams")
+    void testExiCapabilityUrn(final Integer compression, final ExiSchemas schema, final String expectedUrn) {
+        final var capability = new ExiCapability(compression, schema);
+        assertEquals(expectedUrn, capability.urn());
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideExiCapabilityParams")
+    void testExiCapabilityEquals(final Integer compression, final ExiSchemas schema) {
+        assertEquals(new ExiCapability(compression, schema), new ExiCapability(compression, schema));
+    }
+
+    @ParameterizedTest
+    @MethodSource("provideYangModuleCapabilityParams")
+    void testYangModuleCapabilityEquals(final String namespace, final String module, final String revision,
+            final List<String> features, final List<String> deviations) {
+        assertEquals(new YangModuleCapability(namespace, module, revision, features, deviations),
+            new YangModuleCapability(namespace, module, revision, features, deviations));
+    }
+
+    static Stream<Arguments> provideExiCapabilityParams() {
+        return Stream.of(
+            Arguments.of(null, null, "urn:ietf:params:netconf:capability:exi:1.0"),
+            Arguments.of(1000, ExiSchemas.BUILTIN,
+                "urn:ietf:params:netconf:capability:exi:1.0?compression=1000&schemas=builtin"),
+            Arguments.of(1000, null, "urn:ietf:params:netconf:capability:exi:1.0?compression=1000"),
+            Arguments.of(null, ExiSchemas.BASE_1_1,
+                "urn:ietf:params:netconf:capability:exi:1.0?schemas=base:1.1"),
+            Arguments.of(null, ExiSchemas.DYNAMIC,
+                "urn:ietf:params:netconf:capability:exi:1.0?schemas=dynamic")
+        );
+    }
+
+    static Stream<Arguments> provideYangModuleCapabilityParams() {
+        return Stream.of(
+            Arguments.of("http://example.com", null, null, null, null, "http://example.com"),
+            Arguments.of("http://example.com", "module", "2023-08-21", List.of("feature1", "feature2"),
+                List.of("deviation1", "deviation2"), "http://example.com?module=module&revision=2023-08-21"
+                    + "&features=feature1,feature2&deviations=deviation1,deviation2"),
+            Arguments.of("http://example.com", null, "2023-08-21", null, null,
+                "http://example.com?revision=2023-08-21"),
+            Arguments.of("http://example.com", null, "2023-08-21", List.of("feature"), List.of("deviation"),
+                "http://example.com?revision=2023-08-21&features=feature&deviations=deviation"),
+            Arguments.of("http://example.com", "module", null, List.of("feature"), List.of("deviation"),
+                "http://example.com?module=module&features=feature&deviations=deviation"),
+            Arguments.of("http://example.com", "module", "2023-08-21", null, List.of("deviation"),
+                "http://example.com?module=module&revision=2023-08-21&deviations=deviation"),
+            Arguments.of("http://example.com", "module", "2023-08-21", List.of("feature"), null,
+                "http://example.com?module=module&revision=2023-08-21&features=feature"),
+            Arguments.of("http://example.com", "module", "2023-08-21", Collections.emptyList(), List.of("deviation"),
+                "http://example.com?module=module&revision=2023-08-21&deviations=deviation"),
+            Arguments.of("http://example.com", "module", "2023-08-21", List.of("feature"), Collections.emptyList(),
+                "http://example.com?module=module&revision=2023-08-21&features=feature")
+        );
+    }
+}
diff --git a/protocol/netconf-api/src/test/java/org/opendaylight/netconf/api/capability/ProtocolCapabilityTest.java b/protocol/netconf-api/src/test/java/org/opendaylight/netconf/api/capability/ProtocolCapabilityTest.java
new file mode 100644 (file)
index 0000000..bb8119d
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2023 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.netconf.api.capability;
+
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+
+import java.util.stream.Stream;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+
+class ProtocolCapabilityTest {
+    @Test
+    void forUnknownURN() {
+        assertNull(SimpleCapability.forURN("tweedly dee"));
+    }
+
+    @Test
+    void forNullURN() {
+        assertThrows(NullPointerException.class, () -> SimpleCapability.forURN(null));
+    }
+
+    @Test
+    void ofUnknownURN() {
+        assertThrows(IllegalArgumentException.class, () -> SimpleCapability.ofURN("tweedly dum"));
+        assertThrows(NullPointerException.class, () -> SimpleCapability.ofURN(null));
+    }
+
+    @ParameterizedTest
+    @MethodSource("protocolCapabilities")
+    void forURN(final String urn, final SimpleCapability expected) {
+        assertSame(expected, SimpleCapability.forURN(urn));
+    }
+
+    static Stream<Arguments> protocolCapabilities() {
+        return Stream.of(
+            arguments("urn:ietf:params:netconf:base:1.0", SimpleCapability.BASE),
+            arguments("urn:ietf:params:netconf:base:1.1", SimpleCapability.BASE_1_1),
+            arguments("urn:ietf:params:netconf:capability:candidate:1.0", SimpleCapability.CANDIDATE),
+            arguments("urn:ietf:params:netconf:capability:confirmed-commit:1.0", SimpleCapability.CONFIRMED_COMMIT),
+            arguments("urn:ietf:params:netconf:capability:confirmed-commit:1.1",
+                SimpleCapability.CONFIRMED_COMMIT_1_1),
+            arguments("urn:ietf:params:netconf:capability:interleave:1.0", SimpleCapability.INTERLEAVE),
+            arguments("urn:ietf:params:netconf:capability:notification:1.0", SimpleCapability.NOTIFICATION),
+            arguments("urn:ietf:params:netconf:capability:partial-lock:1.0", SimpleCapability.PARTIAL_LOCK),
+            arguments("urn:ietf:params:netconf:capability:rollback-on-error:1.0", SimpleCapability.ROLLBACK_ON_ERROR),
+            arguments("urn:ietf:params:netconf:capability:startup:1.0", SimpleCapability.STARTUP),
+            arguments("urn:ietf:params:netconf:capability:time:1.0", SimpleCapability.TIME),
+            arguments("urn:ietf:params:netconf:capability:url:1.0", SimpleCapability.URL),
+            arguments("urn:ietf:params:netconf:capability:validate:1.0", SimpleCapability.VALIDATE),
+            arguments("urn:ietf:params:netconf:capability:validate:1.1", SimpleCapability.VALIDATE_1_1),
+            arguments("urn:ietf:params:netconf:capability:with-defaults:1.0", SimpleCapability.WITH_DEFAULTS),
+            arguments("urn:ietf:params:netconf:capability:with-operational-defaults:1.0",
+                SimpleCapability.WITH_OPERATIONAL_DEFAULTS),
+            arguments("urn:ietf:params:netconf:capability:writable-running:1.0", SimpleCapability.WRITABLE_RUNNING),
+            arguments("urn:ietf:params:netconf:capability:xpath:1.0", SimpleCapability.XPATH),
+            arguments("urn:ietf:params:netconf:capability:yang-library:1.0", SimpleCapability.YANG_LIBRARY),
+            arguments("urn:ietf:params:netconf:capability:yang-library:1.1", SimpleCapability.YANG_LIBRARY_1_1));
+    }
+}