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;
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);
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);
*/
public Optional<? extends EffectiveModelContext> getEffectiveModelContext(
final StatementParserMode statementParserMode) {
- final var factory = repository.createEffectiveModelContextFactory(config(statementParserMode));
Optional<EffectiveModelContext> sc;
Object ver;
do {
sources = ImmutableSet.copyOf(requiredSources);
} while (ver != version);
+ final var factory = repository.createEffectiveModelContextFactory(
+ config(statementParserMode, getSupportedFeatures()));
+
while (true) {
final var f = factory.createEffectiveModelContext(sources);
try {
@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 {
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();
}
}
* 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);
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();
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.");
+ }
}
--- /dev/null
+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
--- /dev/null
+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
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;
}
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) {
--- /dev/null
+/*
+ * 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();
+ }
+}
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);
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)
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.
* @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;
}