Add DefaultSchemaTreeInference.unsafeOf() 14/100414/14
authorSamuel Schneider <samuel.schneider@pantheon.tech>
Tue, 5 Apr 2022 11:23:48 +0000 (13:23 +0200)
committerRobert Varga <nite@hq.sk>
Sun, 10 Apr 2022 16:02:56 +0000 (16:02 +0000)
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 <samuel.schneider@pantheon.tech>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
model/yang-model-spi/src/main/java/org/opendaylight/yangtools/yang/model/spi/DefaultSchemaTreeInference.java
model/yang-model-spi/src/test/java/org/opendaylight/yangtools/yang/model/spi/YT1414Test.java [new file with mode: 0644]
model/yang-model-util/src/main/java/org/opendaylight/yangtools/yang/model/util/SchemaInferenceStack.java
model/yang-model-util/src/test/java/org/opendaylight/yangtools/yang/model/util/YT1414Test.java [new file with mode: 0644]
model/yang-model-util/src/test/resources/yt1414.yang [new file with mode: 0644]

index 94003e15033603c7b686322e4c89f0aa92972d52..4e01e4551707916f33618ce09e0d0f7940998950 100644 (file)
@@ -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<SchemaTreeEffectiveStatement<?>>
         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<SchemaTreeEffectiveStatement<?>> path) {
+            final ImmutableList<? extends SchemaTreeEffectiveStatement<?>> 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<QName> 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.
+     *
+     * <p>
+     * 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<? extends SchemaTreeEffectiveStatement<?>> 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<? extends SchemaTreeEffectiveStatement<?>> 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<SchemaTreeEffectiveStatement<?>> resolveSteps(final EffectiveModelContext modelContext,
+            final List<QName> steps) {
+        final var first = steps.get(0);
+        final var module = modelContext.findModuleStatement(first.getModule()).orElseThrow(
             () -> new IllegalArgumentException("No module for " + first));
 
-        final ImmutableList.Builder<SchemaTreeEffectiveStatement<?>> builder =
-            ImmutableList.builderWithExpectedSize(steps.size());
+        final var builder = ImmutableList.<SchemaTreeEffectiveStatement<?>>builderWithExpectedSize(steps.size());
         SchemaTreeAwareEffectiveStatement<?, ?> parent = module;
         final Iterator<QName> 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 (file)
index 0000000..ed47938
--- /dev/null
@@ -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, "));
+    }
+}
index 205530f6029efe94112c772833255435e6237285..62c9b673752ca4d284872ed3d0ddb1697ddec945 100644 (file)
@@ -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<EffectiveStatement<?, ?>> 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.
+     *
+     * <p>
+     * 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.<SchemaTreeEffectiveStatement<?>>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<QName> 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<EffectiveStatement<?, ?>> 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 (file)
index 0000000..bb29951
--- /dev/null
@@ -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 (file)
index 0000000..8561170
--- /dev/null
@@ -0,0 +1,10 @@
+module foo {
+  namespace foo;
+  prefix foo;
+
+  container foo;
+
+  container bar {
+    list foo;
+  }
+}