From: Samuel Schneider Date: Tue, 5 Apr 2022 11:23:48 +0000 (+0200) Subject: Add DefaultSchemaTreeInference.unsafeOf() X-Git-Tag: v8.0.3~3 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=yangtools.git;a=commitdiff_plain;h=046b425b63f0c3edb6d9dbc702f6255c09eef994 Add DefaultSchemaTreeInference.unsafeOf() SchemaInferenceStack is routinely converted to DefaultSchemaTreeInference as well as reconstructed from it. This process currently involves a complete schema tree walk in both directions. Introduce DefaultSchemaTreeInference.unsafeOf() which nominally trusts the provided argument, so that we use it as is. The method can be instructed to verify the provided argument by setting a system property. On the SchemaInferenceStack side, trust the documented well-formedness of DefaultSchemaTreeInference. JIRA: YANGTOOLS-1414 Change-Id: Ifa2f0418c561795595d8dae70f6b0c61a33d8f43 Signed-off-by: Samuel Schneider Signed-off-by: Robert Varga --- diff --git a/model/yang-model-spi/src/main/java/org/opendaylight/yangtools/yang/model/spi/DefaultSchemaTreeInference.java b/model/yang-model-spi/src/main/java/org/opendaylight/yangtools/yang/model/spi/DefaultSchemaTreeInference.java index 94003e1503..4e01e45517 100644 --- a/model/yang-model-spi/src/main/java/org/opendaylight/yangtools/yang/model/spi/DefaultSchemaTreeInference.java +++ b/model/yang-model-spi/src/main/java/org/opendaylight/yangtools/yang/model/spi/DefaultSchemaTreeInference.java @@ -10,18 +10,20 @@ package org.opendaylight.yangtools.yang.model.spi; import static com.google.common.base.Preconditions.checkArgument; import com.google.common.annotations.Beta; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.common.collect.Lists; import java.util.Iterator; import java.util.List; import org.eclipse.jdt.annotation.NonNullByDefault; import org.opendaylight.yangtools.yang.common.QName; import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; import org.opendaylight.yangtools.yang.model.api.SchemaTreeInference; -import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement; import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute; import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeAwareEffectiveStatement; import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement; import org.opendaylight.yangtools.yang.model.spi.AbstractEffectiveStatementInference.WithPath; +import org.slf4j.LoggerFactory; /** * Default implementation of a a {@link SchemaTreeInference}. Guaranteed to be consistent with its @@ -31,43 +33,89 @@ import org.opendaylight.yangtools.yang.model.spi.AbstractEffectiveStatementInfer @NonNullByDefault public final class DefaultSchemaTreeInference extends WithPath> implements SchemaTreeInference { + private static final String VERIFY_UNSAFE_PROP = + "org.opendaylight.yangtools.yang.model.spi.DefaultSchemaTreeInference.verifyUnsafeOf"; + private static final boolean VERIFY_UNSAFE = Boolean.getBoolean(VERIFY_UNSAFE_PROP); + + static { + if (VERIFY_UNSAFE) { + LoggerFactory.getLogger(DefaultSchemaTreeInference.class) + .info("DefaultSchemaTreeInference.unsafeOf() arguments are being verified"); + } + } + private DefaultSchemaTreeInference(final EffectiveModelContext modelContext, - final ImmutableList> path) { + final ImmutableList> path) { super(modelContext, path); } /** - * Create a new instance. + * Create a new instance based on an {@link EffectiveModelContext} and an {@link Absolute} schema node identifier. * * @param modelContext Associated {@link EffectiveModelContext} * @param path An absolute schema node identifier * @return A new instance + * @throws NullPointerException if any argument is null + * @throws IllegalArgumentException if the provided {@code path} cannot be resolved in {@code modelContext} */ public static DefaultSchemaTreeInference of(final EffectiveModelContext modelContext, final Absolute path) { - final List steps = path.getNodeIdentifiers(); - final QName first = steps.get(0); - final ModuleEffectiveStatement module = modelContext.findModuleStatement(first.getModule()).orElseThrow( + return new DefaultSchemaTreeInference(modelContext, resolveSteps(modelContext, path.getNodeIdentifiers())); + } + + /** + * Create a new instance based on an {@link EffectiveModelContext} and a resolved sequence of statements. Provided + * statements are expected to have been produced in a validated manner and are normally trusted to be accurate. + * + *

+ * Run-time verification of {@code path} can be enabled by setting the {@value #VERIFY_UNSAFE_PROP} system property + * to {@code true}. + * + * @param modelContext Associated {@link EffectiveModelContext} + * @param path Resolved statement path + * @return A new instance + * @throws NullPointerException if any argument is null + * @throws IllegalArgumentException if {@code path} is empty or when verification is enabled and the {@code path} + * does not match the {@code modelContext}'s schema tree + */ + public static DefaultSchemaTreeInference unsafeOf(final EffectiveModelContext modelContext, + final ImmutableList> path) { + checkArgument(!path.isEmpty(), "Path must not be empty"); + return VERIFY_UNSAFE ? verifiedOf(modelContext, path) : new DefaultSchemaTreeInference(modelContext, path); + } + + @VisibleForTesting + static DefaultSchemaTreeInference verifiedOf(final EffectiveModelContext modelContext, + final ImmutableList> path) { + final var resolved = resolveSteps(modelContext, Lists.transform(path, SchemaTreeEffectiveStatement::argument)); + checkArgument(path.equals(resolved), "Provided path %s is not consistent with resolved path %s", path, + resolved); + return new DefaultSchemaTreeInference(modelContext, path); + } + + private static ImmutableList> resolveSteps(final EffectiveModelContext modelContext, + final List steps) { + final var first = steps.get(0); + final var module = modelContext.findModuleStatement(first.getModule()).orElseThrow( () -> new IllegalArgumentException("No module for " + first)); - final ImmutableList.Builder> builder = - ImmutableList.builderWithExpectedSize(steps.size()); + final var builder = ImmutableList.>builderWithExpectedSize(steps.size()); SchemaTreeAwareEffectiveStatement parent = module; final Iterator it = steps.iterator(); while (true) { - final QName qname = it.next(); - final SchemaTreeEffectiveStatement found = parent.findSchemaTreeNode(qname).orElseThrow( + final var qname = it.next(); + final var found = parent.findSchemaTreeNode(qname).orElseThrow( () -> new IllegalArgumentException("Cannot resolve step " + qname + " in " + builder.build())); builder.add(found); - if (it.hasNext()) { - checkArgument(found instanceof SchemaTreeAwareEffectiveStatement, "Cannot resolve steps %s past %s", - steps, found); - parent = (SchemaTreeAwareEffectiveStatement) found; - } else { + if (!it.hasNext()) { break; } + + checkArgument(found instanceof SchemaTreeAwareEffectiveStatement, "Cannot resolve steps %s past %s", steps, + found); + parent = (SchemaTreeAwareEffectiveStatement) found; } - return new DefaultSchemaTreeInference(modelContext, builder.build()); + return builder.build(); } } diff --git a/model/yang-model-spi/src/test/java/org/opendaylight/yangtools/yang/model/spi/YT1414Test.java b/model/yang-model-spi/src/test/java/org/opendaylight/yangtools/yang/model/spi/YT1414Test.java new file mode 100644 index 0000000000..ed47938287 --- /dev/null +++ b/model/yang-model-spi/src/test/java/org/opendaylight/yangtools/yang/model/spi/YT1414Test.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2022 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.yangtools.yang.model.spi; + +import static org.hamcrest.CoreMatchers.startsWith; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import com.google.common.collect.ImmutableList; +import java.util.Optional; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext; +import org.opendaylight.yangtools.yang.model.api.stmt.ContainerEffectiveStatement; +import org.opendaylight.yangtools.yang.model.api.stmt.ListEffectiveStatement; +import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement; + +@RunWith(MockitoJUnitRunner.StrictStubs.class) +public class YT1414Test { + @Mock + public EffectiveModelContext modelContext; + @Mock + public ContainerEffectiveStatement container; + + @Test + public void testUnsafeOf() { + final var path = ImmutableList.of(container); + final var inference = DefaultSchemaTreeInference.unsafeOf(modelContext, path); + assertSame(modelContext, inference.getEffectiveModelContext()); + assertSame(path, inference.statementPath()); + } + + @Test + public void testVerifiedOf() { + final var qname = QName.create("foo", "foo"); + doReturn(qname).when(container).argument(); + + final var module = mock(ModuleEffectiveStatement.class); + doReturn(Optional.of(module)).when(modelContext).findModuleStatement(qname.getModule()); + doReturn(Optional.of(container)).when(module).findSchemaTreeNode(qname); + + final var path = ImmutableList.of(container); + final var inference = DefaultSchemaTreeInference.verifiedOf(modelContext, path); + + assertSame(modelContext, inference.getEffectiveModelContext()); + assertSame(path, inference.statementPath()); + } + + @Test + public void testVerifiedOfNegative() { + final var qname = QName.create("foo", "foo"); + doReturn(qname).when(container).argument(); + + final var module = mock(ModuleEffectiveStatement.class); + doReturn(Optional.of(module)).when(modelContext).findModuleStatement(qname.getModule()); + doReturn(Optional.of(mock(ListEffectiveStatement.class))).when(module).findSchemaTreeNode(qname); + + assertThat(assertThrows(IllegalArgumentException.class, + () -> DefaultSchemaTreeInference.verifiedOf(modelContext, ImmutableList.of(container))) + .getMessage(), startsWith( + "Provided path [container] is not consistent with resolved path [Mock for ListEffectiveStatement, ")); + } +} diff --git a/model/yang-model-util/src/main/java/org/opendaylight/yangtools/yang/model/util/SchemaInferenceStack.java b/model/yang-model-util/src/main/java/org/opendaylight/yangtools/yang/model/util/SchemaInferenceStack.java index 205530f602..62c9b67375 100644 --- a/model/yang-model-util/src/main/java/org/opendaylight/yangtools/yang/model/util/SchemaInferenceStack.java +++ b/model/yang-model-util/src/main/java/org/opendaylight/yangtools/yang/model/util/SchemaInferenceStack.java @@ -14,6 +14,7 @@ import static com.google.common.base.Verify.verifyNotNull; import static java.util.Objects.requireNonNull; import com.google.common.annotations.Beta; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.VerifyException; import com.google.common.collect.ImmutableList; @@ -67,6 +68,7 @@ import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.AxisStep; import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.QNameStep; import org.opendaylight.yangtools.yang.xpath.api.YangLocationPath.Step; import org.opendaylight.yangtools.yang.xpath.api.YangXPathAxis; +import org.slf4j.LoggerFactory; /** * A state tracking utility for walking {@link EffectiveModelContext}'s contents along schema/grouping namespaces. This @@ -130,6 +132,18 @@ public final class SchemaInferenceStack implements Mutable, EffectiveModelContex } } + private static final String VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE_PROP = + "org.opendaylight.yangtools.yang.model.util.SchemaInferenceStack.verifyDefaultSchemaTreeInference"; + private static final boolean VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE = + Boolean.getBoolean(VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE_PROP); + + static { + if (VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE) { + LoggerFactory.getLogger(SchemaInferenceStack.class) + .info("SchemaTreeStack.ofInference(DefaultSchemaTreeInference) argument is being verified"); + } + } + private final @NonNull EffectiveModelContext effectiveModel; private final ArrayDeque> deque; @@ -225,7 +239,44 @@ public final class SchemaInferenceStack implements Mutable, EffectiveModelContex * @throws IllegalArgumentException if {@code inference} cannot be resolved to a valid stack */ public static @NonNull SchemaInferenceStack ofInference(final SchemaTreeInference inference) { - return of(inference.getEffectiveModelContext(), inference.toSchemaNodeIdentifier()); + return inference instanceof DefaultSchemaTreeInference ? ofInference((DefaultSchemaTreeInference) inference) + : of(inference.getEffectiveModelContext(), inference.toSchemaNodeIdentifier()); + } + + /** + * Create a new stack from an {@link DefaultSchemaTreeInference}. The argument is nominally trusted to be an + * accurate representation of the schema tree. + * + *

+ * Run-time verification of {@code inference} can be enabled by setting the + * {@value #VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE_PROP} system property to {@code true}. + * + * @param inference DefaultSchemaTreeInference to use for initialization + * @return A new stack + * @throws NullPointerException if {@code inference} is null + * @throws IllegalArgumentException if {@code inference} refers to a missing module or when verification is enabled + * and it does not match its context's scheam tree + */ + public static @NonNull SchemaInferenceStack ofInference(final DefaultSchemaTreeInference inference) { + return VERIFY_DEFAULT_SCHEMA_TREE_INFERENCE ? ofUntrusted(inference) : ofTrusted(inference); + } + + private static @NonNull SchemaInferenceStack ofTrusted(final DefaultSchemaTreeInference inference) { + final var path = inference.statementPath(); + final var ret = new SchemaInferenceStack(inference.getEffectiveModelContext(), path.size()); + ret.currentModule = ret.getModule(path.get(0).argument()); + path.forEach(ret.deque::push); + return ret; + } + + @VisibleForTesting + static @NonNull SchemaInferenceStack ofUntrusted(final DefaultSchemaTreeInference inference) { + final var ret = of(inference.getEffectiveModelContext(), inference.toSchemaNodeIdentifier()); + if (!Iterators.elementsEqual(ret.deque.descendingIterator(), inference.statementPath().iterator())) { + throw new IllegalArgumentException("Provided " + inference + " is not consistent with resolved path " + + ret.toSchemaTreeInference()); + } + return ret; } /** @@ -559,7 +610,6 @@ public final class SchemaInferenceStack implements Mutable, EffectiveModelContex return (DataTreeEffectiveStatement) child; } - @Override public TypeDefinition resolveLeafref(final LeafrefTypeDefinition type) { final SchemaInferenceStack tmp = copy(); @@ -690,7 +740,13 @@ public final class SchemaInferenceStack implements Mutable, EffectiveModelContex * @throws IllegalStateException if current state cannot be converted to a {@link SchemaTreeInference} */ public @NonNull SchemaTreeInference toSchemaTreeInference() { - return DefaultSchemaTreeInference.of(getEffectiveModelContext(), toSchemaNodeIdentifier()); + checkState(inInstantiatedContext(), "Cannot convert uninstantiated context %s", this); + final var cleanDeque = clean ? deque : reconstructSchemaInferenceStack().deque; + return DefaultSchemaTreeInference.unsafeOf(getEffectiveModelContext(), + ImmutableList.>builderWithExpectedSize(cleanDeque.size()) + .addAll(Iterators.transform(cleanDeque.descendingIterator(), + stmt -> (SchemaTreeEffectiveStatement) stmt)) + .build()); } /** @@ -859,6 +915,10 @@ public final class SchemaInferenceStack implements Mutable, EffectiveModelContex // of schema tree items. This means at least N searches, but after they are done, we get an opportunity to set the // clean flag. private Iterator reconstructQNames() { + return reconstructSchemaInferenceStack().iterateQNames(); + } + + private SchemaInferenceStack reconstructSchemaInferenceStack() { // Let's walk all statements and decipher them into a temporary stack final SchemaInferenceStack tmp = new SchemaInferenceStack(effectiveModel, deque.size()); final Iterator> it = deque.descendingIterator(); @@ -881,8 +941,11 @@ public final class SchemaInferenceStack implements Mutable, EffectiveModelContex } // if the sizes match, we did not jump through hoops. let's remember that for future. - clean = deque.size() == tmp.deque.size(); - return tmp.iterateQNames(); + if (deque.size() == tmp.deque.size()) { + clean = true; + return this; + } + return tmp; } private void resolveChoiceSteps(final @NonNull QName nodeIdentifier) { diff --git a/model/yang-model-util/src/test/java/org/opendaylight/yangtools/yang/model/util/YT1414Test.java b/model/yang-model-util/src/test/java/org/opendaylight/yangtools/yang/model/util/YT1414Test.java new file mode 100644 index 0000000000..bb299511dd --- /dev/null +++ b/model/yang-model-util/src/test/java/org/opendaylight/yangtools/yang/model/util/YT1414Test.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2022 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.yangtools.yang.model.util; + +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import java.util.List; +import org.junit.Test; +import org.opendaylight.yangtools.yang.common.QName; +import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Absolute; +import org.opendaylight.yangtools.yang.model.spi.DefaultSchemaTreeInference; +import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils; + +public class YT1414Test { + private static final QName MY_CONTAINER = QName.create("uri:my-module", "2014-10-07", "my-container"); + private static final QName MY_LIST = QName.create(MY_CONTAINER, "my-list"); + private static final Absolute MY_LIST_ID = Absolute.of(MY_CONTAINER, MY_LIST); + + private static final QName FOO = QName.create("foo", "foo"); + private static final QName BAR = QName.create(FOO, "bar"); + private static final Absolute BAR_FOO_ID = Absolute.of(BAR, FOO); + + @Test + public void testToFromSchemaTreeInference() { + final var stack = SchemaInferenceStack.of( + YangParserTestUtils.parseYangResourceDirectory("/schema-context-util")); + stack.enterSchemaTree(MY_LIST_ID); + final var inference = stack.toSchemaTreeInference(); + assertThat(inference, instanceOf(DefaultSchemaTreeInference.class)); + assertEquals(MY_LIST_ID, inference.toSchemaNodeIdentifier()); + assertEquals(MY_LIST_ID, stack.toSchemaNodeIdentifier()); + assertEquals(MY_LIST_ID, SchemaInferenceStack.ofInference(inference).toSchemaNodeIdentifier()); + } + + @Test + public void testOfUntrustedSchemaTreeInference() { + final var context = YangParserTestUtils.parseYangResource("/yt1414.yang"); + final var foo = context.findSchemaTreeNode(Absolute.of(FOO)).orElseThrow(); + final var bar = context.findSchemaTreeNode(Absolute.of(BAR)).orElseThrow(); + final var barFoo = context.findSchemaTreeNode(BAR_FOO_ID).orElseThrow(); + + // Let's check that correct thing works out + final var correct = DefaultSchemaTreeInference.of(context, BAR_FOO_ID); + assertEquals(List.of(bar, barFoo), correct.statementPath()); + assertEquals(correct.statementPath(), + SchemaInferenceStack.ofUntrusted(correct).toSchemaTreeInference().statementPath()); + + // Now let's try some abuse: we use 'foo' instead of 'barFoo', created unsafely ... + final var incorrect = DefaultSchemaTreeInference.unsafeOf(context, ImmutableList.of(bar, foo)); + // ... the default non-verify method is happy to oblige ... + assertEquals(incorrect.statementPath(), + SchemaInferenceStack.ofInference(incorrect).toSchemaTreeInference().statementPath()); + // ... but ofUntrusted() will reject it + assertEquals("Provided " + incorrect + " is not consistent with resolved path " + correct, + assertThrows(IllegalArgumentException.class, () -> SchemaInferenceStack.ofUntrusted(incorrect)) + .getMessage()); + } +} diff --git a/model/yang-model-util/src/test/resources/yt1414.yang b/model/yang-model-util/src/test/resources/yt1414.yang new file mode 100644 index 0000000000..85611708d5 --- /dev/null +++ b/model/yang-model-util/src/test/resources/yt1414.yang @@ -0,0 +1,10 @@ +module foo { + namespace foo; + prefix foo; + + container foo; + + container bar { + list foo; + } +}