Add YangTextSchemaContextResolver feature support 37/105037/22
authormatus.matok <matus.matok@pantheon.tech>
Mon, 27 Mar 2023 11:31:23 +0000 (13:31 +0200)
committerRobert Varga <nite@hq.sk>
Wed, 5 Apr 2023 16:18:34 +0000 (16:18 +0000)
YangTextSchemaContextResolver does not provide a facility to control the
set of supported features. Add
YangTextSchemaContextResolver.registeredFeatures() and a specialized
Set<QName> implementation to provide backwards-compatible way of
specifying features.

The specialized implementation does not completely conform to
Set.equals()/Set.hashCode() specification and we therefore need to dance
a bit around it.

JIRA: YANGTOOLS-1463
Change-Id: I443d42a6859ce97f238117fabdb7bbdd42488ee5
Signed-off-by: matus.matok <matus.matok@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
parser/yang-parser-impl/src/main/java/org/opendaylight/yangtools/yang/parser/repo/YangTextSchemaContextResolver.java
parser/yang-parser-impl/src/test/java/org/opendaylight/yangtools/yang/parser/repo/YangTextSchemaContextResolverTest.java
parser/yang-parser-impl/src/test/resources/yang-text-schema-context-resolver-test/aux-feature.yang [new file with mode: 0644]
parser/yang-parser-impl/src/test/resources/yang-text-schema-context-resolver-test/foo-feature.yang [new file with mode: 0644]
parser/yang-parser-reactor/src/main/java/org/opendaylight/yangtools/yang/parser/stmt/reactor/BuildGlobalContext.java
yang/yang-repo-api/src/main/java/org/opendaylight/yangtools/yang/model/repo/api/FeatureSet.java [new file with mode: 0644]
yang/yang-repo-api/src/main/java/org/opendaylight/yangtools/yang/model/repo/api/SchemaContextFactoryConfiguration.java

index 6782eb8c9b6a1e2cc362db778235a4dc2d1e0fc6..215297e71985e3bf9f59a2806593f942afff5b1d 100644 (file)
@@ -13,21 +13,32 @@ import static java.util.Objects.requireNonNull;
 import com.google.common.annotations.Beta;
 import com.google.common.base.Verify;
 import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Multimap;
 import com.google.common.util.concurrent.FluentFuture;
 import java.io.IOException;
 import java.net.URL;
 import java.time.Duration;
+import java.util.ArrayList;
 import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.ConcurrentLinkedDeque;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.atomic.AtomicReference;
+import org.checkerframework.checker.lock.qual.GuardedBy;
 import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.concepts.AbstractRegistration;
+import org.opendaylight.yangtools.concepts.Registration;
 import org.opendaylight.yangtools.util.concurrent.FluentFutures;
+import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.model.repo.api.FeatureSet;
 import org.opendaylight.yangtools.yang.model.repo.api.MissingSchemaSourceException;
 import org.opendaylight.yangtools.yang.model.repo.api.SchemaContextFactoryConfiguration;
 import org.opendaylight.yangtools.yang.model.repo.api.SchemaRepository;
@@ -55,14 +66,19 @@ public final class YangTextSchemaContextResolver implements AutoCloseable, Schem
 
     private final Collection<SourceIdentifier> requiredSources = new ConcurrentLinkedDeque<>();
     private final Multimap<SourceIdentifier, YangTextSchemaSource> texts = ArrayListMultimap.create();
+    @GuardedBy("this")
+    private final Map<QNameModule, List<ImmutableSet<String>>> registeredFeatures = new HashMap<>();
     private final AtomicReference<Optional<EffectiveModelContext>> currentSchemaContext =
             new AtomicReference<>(Optional.empty());
     private final GuavaSchemaSourceCache<YangIRSchemaSource> cache;
     private final SchemaListenerRegistration transReg;
     private final SchemaSourceRegistry registry;
     private final SchemaRepository repository;
+
     private volatile Object version = new Object();
     private volatile Object contextVersion = version;
+    @GuardedBy("this")
+    private FeatureSet supportedFeatures = null;
 
     private YangTextSchemaContextResolver(final SchemaRepository repository, final SchemaSourceRegistry registry) {
         this.repository = requireNonNull(repository);
@@ -168,6 +184,60 @@ public final class YangTextSchemaContextResolver implements AutoCloseable, Schem
         return registerSource(YangTextSchemaSource.forURL(url, guessSourceIdentifier(fileName)));
     }
 
+    /**
+     * Register a {@link QNameModule} as a known module namespace with a set of supported features. Union of these
+     * registrations is forwarded to {@link FeatureSet} and this is then used in {@link #getEffectiveModelContext()} and
+     * related methods.
+     *
+     * @param module Module namespace
+     * @param features Features supported for that module namespace
+     * @return a {@link Registration}, use {@link Registration#close()} to revert the effects of this method
+     * @throws NullPointerException if any argument is {@code null}
+     */
+    public @NonNull Registration registerSupportedFeatures(final QNameModule module, final Set<String> features) {
+        final var checked = requireNonNull(module);
+        final var copy = ImmutableSet.copyOf(features);
+
+        synchronized (this) {
+            version = new Object();
+            supportedFeatures = null;
+            registeredFeatures.computeIfAbsent(module, ignored -> new ArrayList<>()).add(copy);
+        }
+        return new AbstractRegistration() {
+            @Override
+            protected void removeRegistration() {
+                removeFeatures(checked, copy);
+            }
+        };
+    }
+
+    private synchronized void removeFeatures(final QNameModule module, final ImmutableSet<String> features) {
+        final var moduleFeatures = registeredFeatures.get(module);
+        if (moduleFeatures != null && moduleFeatures.remove(features)) {
+            if (moduleFeatures.isEmpty()) {
+                registeredFeatures.remove(module);
+            }
+            supportedFeatures = null;
+            version = new Object();
+        }
+    }
+
+    private synchronized @Nullable FeatureSet getSupportedFeatures() {
+        var local = supportedFeatures;
+        if (local == null && !registeredFeatures.isEmpty()) {
+            final var builder = ImmutableMap.<QNameModule, ImmutableSet<String>>builder();
+            for (var entry : registeredFeatures.entrySet()) {
+                builder.put(entry.getKey(), entry.getValue().stream()
+                    .flatMap(Set::stream)
+                    .distinct()
+                    .sorted()
+                    .collect(ImmutableSet.toImmutableSet()));
+            }
+            supportedFeatures = local = new FeatureSet(builder.build());
+        }
+        return local;
+    }
+
     private static SourceIdentifier guessSourceIdentifier(final @NonNull String fileName) {
         try {
             return YangTextSchemaSource.identifierFromFilename(fileName);
@@ -196,7 +266,6 @@ public final class YangTextSchemaContextResolver implements AutoCloseable, Schem
      */
     public Optional<? extends EffectiveModelContext> getEffectiveModelContext(
             final StatementParserMode statementParserMode) {
-        final var factory = repository.createEffectiveModelContextFactory(config(statementParserMode));
         Optional<EffectiveModelContext> sc;
         Object ver;
         do {
@@ -217,6 +286,9 @@ public final class YangTextSchemaContextResolver implements AutoCloseable, Schem
                 sources = ImmutableSet.copyOf(requiredSources);
             } while (ver != version);
 
+            final var factory = repository.createEffectiveModelContextFactory(
+                config(statementParserMode, getSupportedFeatures()));
+
             while (true) {
                 final var f = factory.createEffectiveModelContext(sources);
                 try {
@@ -282,7 +354,8 @@ public final class YangTextSchemaContextResolver implements AutoCloseable, Schem
     @SuppressWarnings("checkstyle:avoidHidingCauseException")
     public EffectiveModelContext trySchemaContext(final StatementParserMode statementParserMode)
             throws SchemaResolutionException {
-        final var future = repository.createEffectiveModelContextFactory(config(statementParserMode))
+        final var future = repository
+                .createEffectiveModelContextFactory(config(statementParserMode, getSupportedFeatures()))
                 .createEffectiveModelContext(ImmutableSet.copyOf(requiredSources));
 
         try {
@@ -303,7 +376,12 @@ public final class YangTextSchemaContextResolver implements AutoCloseable, Schem
         transReg.close();
     }
 
-    private static @NonNull SchemaContextFactoryConfiguration config(final StatementParserMode statementParserMode) {
-        return SchemaContextFactoryConfiguration.builder().setStatementParserMode(statementParserMode).build();
+    private static @NonNull SchemaContextFactoryConfiguration config(
+            final StatementParserMode statementParserMode, final @Nullable FeatureSet supportedFeatures) {
+        final var builder = SchemaContextFactoryConfiguration.builder().setStatementParserMode(statementParserMode);
+        if (supportedFeatures != null) {
+            builder.setSupportedFeatures(supportedFeatures);
+        }
+        return builder.build();
     }
 }
index b8cb536e8d32c2b9603a6310a94a64a776506a37..0152d7ee0f2553e5c634ae336dbc1ccdb2f33bca 100644 (file)
@@ -5,34 +5,34 @@
  * 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.yangtools.yang.parser.repo;
 
 import static org.hamcrest.CoreMatchers.instanceOf;
 import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
 import com.google.common.util.concurrent.ListenableFuture;
-import java.io.IOException;
 import java.net.URL;
-import java.util.Optional;
+import java.util.List;
+import java.util.Set;
 import java.util.concurrent.ExecutionException;
 import org.junit.Test;
-import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.concepts.Registration;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
 import org.opendaylight.yangtools.yang.model.repo.api.MissingSchemaSourceException;
-import org.opendaylight.yangtools.yang.model.repo.api.SchemaSourceException;
 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
 import org.opendaylight.yangtools.yang.model.repo.api.YangTextSchemaSource;
-import org.opendaylight.yangtools.yang.parser.api.YangSyntaxErrorException;
 
 public class YangTextSchemaContextResolverTest {
 
     @Test
-    public void testYangTextSchemaContextResolver() throws SchemaSourceException, IOException, YangSyntaxErrorException,
-            InterruptedException, ExecutionException {
+    public void testYangTextSchemaContextResolver() throws Exception {
         final YangTextSchemaContextResolver yangTextSchemaContextResolver =
                 YangTextSchemaContextResolver.create("test-bundle");
         assertNotNull(yangTextSchemaContextResolver);
@@ -80,10 +80,9 @@ public class YangTextSchemaContextResolverTest {
         assertThat(cause, instanceOf(MissingSchemaSourceException.class));
         assertEquals("URL for SourceIdentifier [foobar@2016-09-26] not registered", cause.getMessage());
 
-        Optional<? extends SchemaContext> schemaContextOptional =
-            yangTextSchemaContextResolver.getEffectiveModelContext();
+        var schemaContextOptional = yangTextSchemaContextResolver.getEffectiveModelContext();
         assertTrue(schemaContextOptional.isPresent());
-        SchemaContext schemaContext = schemaContextOptional.orElseThrow();
+        var schemaContext = schemaContextOptional.orElseThrow();
         assertEquals(3, schemaContext.getModules().size());
 
         registration1.close();
@@ -97,4 +96,86 @@ public class YangTextSchemaContextResolverTest {
         schemaContext = schemaContextOptional.orElseThrow();
         assertEquals(0, schemaContext.getModules().size());
     }
+
+    @Test
+    public void testFeatureRegistration() throws Exception {
+        final YangTextSchemaContextResolver yangTextSchemaContextResolver =
+                YangTextSchemaContextResolver.create("feature-test-bundle");
+        assertNotNull(yangTextSchemaContextResolver);
+        final URL yangFile1 = getClass().getResource("/yang-text-schema-context-resolver-test/foo-feature.yang");
+        assertNotNull(yangFile1);
+        final URL yangFile2 = getClass().getResource("/yang-text-schema-context-resolver-test/aux-feature.yang");
+        assertNotNull(yangFile2);
+
+        final YangTextSchemaSourceRegistration registration1 =
+                yangTextSchemaContextResolver.registerSource(yangFile1);
+        assertNotNull(registration1);
+        final YangTextSchemaSourceRegistration registration2 =
+                yangTextSchemaContextResolver.registerSource(yangFile2);
+        assertNotNull(registration2);
+
+        final QName cont = QName.create("foo-feature-namespace", "2016-09-26", "bar-feature-container");
+        final QName condLeaf = QName.create("foo-feature-namespace", "2016-09-26", "conditional-leaf");
+        final QName uncondLeaf = QName.create("foo-feature-namespace", "2016-09-26", "unconditional-leaf");
+        final QName auxCont = QName.create("aux-feature-namespace", "2016-09-26", "aux-cond-cont");
+
+        final QName usedFeature = QName.create("foo-feature-namespace", "2016-09-26", "used-feature");
+        final QName unusedFeature = QName.create("foo-feature-namespace", "2016-09-26", "unused-feature");
+
+        Iterable<QName> pathToConditional = List.of(cont, condLeaf);
+        Iterable<QName> pathToUnconditional = List.of(cont, uncondLeaf);
+        Iterable<QName> pathToAuxiliary = List.of(auxCont);
+
+        final EffectiveModelContext context1 = yangTextSchemaContextResolver.getEffectiveModelContext().orElseThrow();
+
+        assertTrue(isModulePresent(context1, condLeaf.getModule(), pathToConditional));
+        assertTrue(isModulePresent(context1, uncondLeaf.getModule(), pathToUnconditional));
+        assertTrue(isModulePresent(context1, auxCont.getModule(), pathToAuxiliary));
+
+        final Registration featRegistration1 = yangTextSchemaContextResolver.registerSupportedFeatures(
+                unusedFeature.getModule(), Set.of(unusedFeature.getLocalName()));
+        final EffectiveModelContext context2 = yangTextSchemaContextResolver.getEffectiveModelContext().orElseThrow();
+
+        assertFalse(isModulePresent(context2, condLeaf.getModule(), pathToConditional));
+        assertTrue(isModulePresent(context2, uncondLeaf.getModule(), pathToUnconditional));
+        assertTrue(isModulePresent(context2, auxCont.getModule(), pathToAuxiliary));
+
+        final Registration featRegistration2 = yangTextSchemaContextResolver.registerSupportedFeatures(
+                unusedFeature.getModule(), Set.of(usedFeature.getLocalName()));
+        final EffectiveModelContext context3 = yangTextSchemaContextResolver.getEffectiveModelContext().orElseThrow();
+
+        assertTrue(isModulePresent(context3, condLeaf.getModule(), pathToConditional));
+
+        final Registration featRegistration3 = yangTextSchemaContextResolver.registerSupportedFeatures(
+                unusedFeature.getModule(), Set.of(usedFeature.getLocalName(), unusedFeature.getLocalName()));
+        featRegistration1.close();
+        featRegistration2.close();
+        final EffectiveModelContext context4 = yangTextSchemaContextResolver.getEffectiveModelContext().orElseThrow();
+
+        assertTrue(isModulePresent(context4, condLeaf.getModule(), pathToConditional));
+        assertTrue(isModulePresent(context4, auxCont.getModule(), pathToAuxiliary));
+
+        featRegistration3.close();
+        final Registration featRegistration4 = yangTextSchemaContextResolver.registerSupportedFeatures(
+                auxCont.getModule(), Set.of());
+        final EffectiveModelContext context5 = yangTextSchemaContextResolver.getEffectiveModelContext().orElseThrow();
+
+        assertTrue(isModulePresent(context5, condLeaf.getModule(), pathToConditional));
+        assertFalse(isModulePresent(context5, auxCont.getModule(), pathToAuxiliary));
+
+        featRegistration4.close();
+        final EffectiveModelContext context6 = yangTextSchemaContextResolver.getEffectiveModelContext().orElseThrow();
+
+        assertTrue(isModulePresent(context6, auxCont.getModule(), pathToAuxiliary));
+    }
+
+    private static boolean isModulePresent(final EffectiveModelContext context, final QNameModule qnameModule,
+            final Iterable<QName> path) {
+        for (var module : context.getModules()) {
+            if (module.getQNameModule().equals(qnameModule)) {
+                return module.findDataTreeChild(path).isPresent();
+            }
+        }
+        throw new AssertionError("No module with given QNameModule present in the context.");
+    }
 }
diff --git a/parser/yang-parser-impl/src/test/resources/yang-text-schema-context-resolver-test/aux-feature.yang b/parser/yang-parser-impl/src/test/resources/yang-text-schema-context-resolver-test/aux-feature.yang
new file mode 100644 (file)
index 0000000..69816b0
--- /dev/null
@@ -0,0 +1,15 @@
+module aux-feature {
+    namespace aux-feature-namespace;
+    prefix aux-feature-prefix;
+
+    revision 2016-09-26;
+
+    feature aux-feature-ft {
+        description
+            "aux feature";
+    }
+
+    container aux-cond-cont {
+        if-feature "aux-feature-ft";
+    }
+}
\ No newline at end of file
diff --git a/parser/yang-parser-impl/src/test/resources/yang-text-schema-context-resolver-test/foo-feature.yang b/parser/yang-parser-impl/src/test/resources/yang-text-schema-context-resolver-test/foo-feature.yang
new file mode 100644 (file)
index 0000000..3daa01b
--- /dev/null
@@ -0,0 +1,26 @@
+module foo-feature {
+    namespace foo-feature-namespace;
+    prefix foo-feature-prefix;
+
+    revision 2016-09-26;
+
+    feature used-feature {
+        description
+            "used feature";
+    }
+
+    feature unused-feature {
+        description
+            "unused feature";
+    }
+
+    container bar-feature-container {
+        leaf conditional-leaf {
+            if-feature "used-feature";
+            type uint64;
+        }
+        leaf unconditional-leaf {
+            type uint64;
+        }
+    }
+}
\ No newline at end of file
index 832e79b5dd15194364ccf997331daf6cb425aea1..e23bd96d907d76a00098c58ed6203f451c60ff12 100644 (file)
@@ -40,6 +40,7 @@ import org.opendaylight.yangtools.yang.common.UnresolvedQName.Unqualified;
 import org.opendaylight.yangtools.yang.common.YangVersion;
 import org.opendaylight.yangtools.yang.model.api.meta.DeclaredStatement;
 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
+import org.opendaylight.yangtools.yang.model.repo.api.FeatureSet;
 import org.opendaylight.yangtools.yang.model.repo.api.SourceIdentifier;
 import org.opendaylight.yangtools.yang.parser.spi.ParserNamespaces;
 import org.opendaylight.yangtools.yang.parser.spi.meta.DerivedNamespaceBehaviour;
@@ -113,7 +114,11 @@ final class BuildGlobalContext extends NamespaceStorageSupport implements Regist
     }
 
     void setSupportedFeatures(final Set<QName> supportedFeatures) {
-        addToNamespace(ParserNamespaces.SUPPORTED_FEATURES, Empty.value(), ImmutableSet.copyOf(supportedFeatures));
+        if (supportedFeatures instanceof FeatureSet) {
+            addToNamespace(ParserNamespaces.SUPPORTED_FEATURES, Empty.value(), supportedFeatures);
+        } else {
+            addToNamespace(ParserNamespaces.SUPPORTED_FEATURES, Empty.value(), ImmutableSet.copyOf(supportedFeatures));
+        }
     }
 
     void setModulesDeviatedByModules(final SetMultimap<QNameModule, QNameModule> modulesDeviatedByModules) {
diff --git a/yang/yang-repo-api/src/main/java/org/opendaylight/yangtools/yang/model/repo/api/FeatureSet.java b/yang/yang-repo-api/src/main/java/org/opendaylight/yangtools/yang/model/repo/api/FeatureSet.java
new file mode 100644 (file)
index 0000000..3bb5d37
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2023 PANTHEON.tech s.r.o. 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.yangtools.yang.model.repo.api;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.AbstractSet;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Set;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+
+/**
+ * Set of features. This is nominally a {@link Set} due to API pre-existing API contracts. This class needs to be used
+ * <b>very carefully</b> because its {@link #hashCode()} and {@link #equals(Object)} contracts do not conform to
+ * the specification laid out by {@link Set} and it cannot enumerate its individual component {@link QName}s -- thus
+ * breaking reflexivity requirement of {@link #equals(Object)}.
+ *
+ * <p>
+ * The semantics of {@link #contains(Object)} is a bit funky, but reflects the default of supporting all encountered
+ * features without enumerating them. The map supplied to the constructor enumerates all {@code module} namespaces,
+ * expressed as {@link QNameModule} for which we have an explicit enumeration of supported features. All other
+ * {@code module} namespaces are treated as if there was no specification of supported features -- e.g. all features
+ * from those namespaces are deemed to be present in the instance.
+ */
+// FIXME: 12.0.0: this should only have 'boolean contains(QName)', with two implementations (Set-based and
+//                module/feature based via a builder). This shouldlive in yang-model-api, where it has a tie-in
+//                with IfFeatureExpr
+@Beta
+public final class FeatureSet extends AbstractSet<QName> implements Immutable {
+    // Note: not a ImmutableSetMultimap because we need to distinguish non-presence vs. empty Set
+    private final ImmutableMap<QNameModule, ImmutableSet<String>> featuresByModule;
+
+    public FeatureSet(final ImmutableMap<QNameModule, ImmutableSet<String>> featuresByModule) {
+        this.featuresByModule = requireNonNull(featuresByModule);
+    }
+
+    @Override
+    @SuppressWarnings("checkstyle:parameterName")
+    public boolean contains(final Object o) {
+        if (o instanceof QName qname) {
+            final var features = featuresByModule.get(qname.getModule());
+            return features == null || features.contains(qname.getLocalName());
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        return featuresByModule.hashCode();
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        return this == obj || obj instanceof FeatureSet other && featuresByModule.equals(other.featuresByModule);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this).add("features", featuresByModule).toString();
+    }
+
+    @Deprecated
+    @Override
+    public Iterator<QName> iterator() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    public boolean isEmpty() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    public int size() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    public Object[] toArray() {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    @SuppressWarnings("checkstyle:parameterName")
+    public <T> T[] toArray(final T[] a) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    @SuppressWarnings("checkstyle:parameterName")
+    public boolean add(final QName e) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    @SuppressWarnings("checkstyle:parameterName")
+    public boolean remove(final Object o) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    @SuppressWarnings("checkstyle:parameterName")
+    public boolean addAll(final Collection<? extends QName> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    @SuppressWarnings("checkstyle:parameterName")
+    public boolean retainAll(final Collection<?> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    @SuppressWarnings("checkstyle:parameterName")
+    public boolean removeAll(final Collection<?> c) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Deprecated
+    @Override
+    public void clear() {
+        throw new UnsupportedOperationException();
+    }
+}
index 879da762f9bbd2aa6a2564d70139e515a5ecbf99..8120518cf3f554b864366d854165396518e74fbd 100644 (file)
@@ -39,12 +39,12 @@ public final class SchemaContextFactoryConfiguration implements Immutable {
 
     private final @NonNull SchemaSourceFilter filter;
     private final @NonNull StatementParserMode statementParserMode;
-    private final @Nullable ImmutableSet<QName> supportedFeatures;
+    private final @Nullable Set<QName> supportedFeatures;
     private final @Nullable ImmutableSetMultimap<QNameModule, QNameModule> modulesDeviatedByModules;
 
     private SchemaContextFactoryConfiguration(final @NonNull SchemaSourceFilter filter,
             final @NonNull StatementParserMode statementParserMode,
-            final @Nullable ImmutableSet<QName> supportedFeatures,
+            final @Nullable Set<QName> supportedFeatures,
             final @Nullable ImmutableSetMultimap<QNameModule, QNameModule> modulesDeviatedByModules) {
         this.filter = requireNonNull(filter);
         this.statementParserMode = requireNonNull(statementParserMode);
@@ -85,10 +85,27 @@ public final class SchemaContextFactoryConfiguration implements Immutable {
     public boolean equals(final Object obj) {
         return this == obj || obj instanceof SchemaContextFactoryConfiguration other && filter.equals(other.filter)
             && statementParserMode.equals(other.statementParserMode)
-            && Objects.equals(supportedFeatures, other.supportedFeatures)
+            && equals(supportedFeatures, other.supportedFeatures)
             && Objects.equals(modulesDeviatedByModules, other.modulesDeviatedByModules);
     }
 
+    // This a bit of a dance to deal with FeatureSet not conforming to Set.equals()
+    private static boolean equals(final @Nullable Set<QName> thisFeatures, final @Nullable Set<QName> otherFeatures) {
+        if (thisFeatures == otherFeatures) {
+            return true;
+        }
+        if (thisFeatures == null || otherFeatures == null) {
+            return false;
+        }
+        if (thisFeatures instanceof FeatureSet) {
+            return thisFeatures.equals(otherFeatures);
+        }
+        if (otherFeatures instanceof FeatureSet) {
+            return otherFeatures.equals(thisFeatures);
+        }
+        return thisFeatures.equals(otherFeatures);
+    }
+
     @Override
     public String toString() {
         return MoreObjects.toStringHelper(this).omitNullValues().add("schemaSourceFilter", filter)
@@ -100,7 +117,7 @@ public final class SchemaContextFactoryConfiguration implements Immutable {
         private @NonNull SchemaSourceFilter filter = SchemaSourceFilter.ALWAYS_ACCEPT;
         private @NonNull StatementParserMode statementParserMode = StatementParserMode.DEFAULT_MODE;
         private ImmutableSetMultimap<QNameModule, QNameModule> modulesDeviatedByModules;
-        private ImmutableSet<QName> supportedFeatures;
+        private Set<QName> supportedFeatures;
 
         /**
          * Set schema source filter which will filter available schema sources using the provided filter.
@@ -133,7 +150,11 @@ public final class SchemaContextFactoryConfiguration implements Immutable {
          * @return this builder
          */
         public @NonNull Builder setSupportedFeatures(final Set<QName> supportedFeatures) {
-            this.supportedFeatures = supportedFeatures != null ? ImmutableSet.copyOf(supportedFeatures) : null;
+            if (supportedFeatures == null || supportedFeatures instanceof FeatureSet) {
+                this.supportedFeatures = supportedFeatures;
+            } else {
+                this.supportedFeatures = ImmutableSet.copyOf(supportedFeatures);
+            }
             return this;
         }