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
@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();
}
}
--- /dev/null
+/*
+ * 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, "));
+ }
+}
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;
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
}
}
+ 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;
* @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;
}
/**
return (DataTreeEffectiveStatement<?>) child;
}
-
@Override
public TypeDefinition<?> resolveLeafref(final LeafrefTypeDefinition type) {
final SchemaInferenceStack tmp = copy();
* @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());
}
/**
// 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();
}
// 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) {
--- /dev/null
+/*
+ * 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());
+ }
+}
--- /dev/null
+module foo {
+ namespace foo;
+ prefix foo;
+
+ container foo;
+
+ container bar {
+ list foo;
+ }
+}