Add UniqueValidation 01/93901/1
authorRobert Varga <robert.varga@pantheon.tech>
Wed, 18 Nov 2020 14:41:30 +0000 (15:41 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Fri, 20 Nov 2020 16:56:35 +0000 (17:56 +0100)
This is a simplement implementation with a stateless external enforcer,
similar to what MinMaxElementsValidation does. The test suite is split
out of If3b94a085be034de28e341ac900142b021cd2a88 and adapted a bit.

JIRA: YANGTOOLS-1177
Change-Id: I3914497981db0281ab8f32dee12a102ce729022e
Signed-off-by: Peter Kajsa <pkajsa@cisco.com>
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
(cherry picked from commit 52e49d63e73b995ea10bbeefb62df9c6101b44c3)

13 files changed:
yang/yang-data-api/src/main/java/module-info.java
yang/yang-data-api/src/main/java/org/opendaylight/yangtools/yang/data/api/schema/tree/UniqueConstraintException.java [new file with mode: 0644]
yang/yang-data-impl/src/main/java/module-info.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/AbstractNodeContainerModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/BinaryValue.java [new file with mode: 0644]
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ListModificationStrategy.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/ModificationApplyOperation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/SchemaAwareApplyOperation.java
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueValidation.java [new file with mode: 0644]
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueValidator.java [new file with mode: 0644]
yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueValues.java [new file with mode: 0644]
yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueConstraintTest.java [new file with mode: 0644]
yang/yang-data-impl/src/test/resources/yt570.yang [new file with mode: 0644]

index cbe3cafd5770b26175aafc52d22bfb0a15054d82..c13d04f6af944dffe99943bf0e056e9d0d7e77dc 100644 (file)
@@ -21,4 +21,5 @@ module org.opendaylight.yangtools.yang.data.api {
 
     // Annotations
     requires static transitive org.eclipse.jdt.annotation;
+    requires static com.github.spotbugs.annotations;
 }
diff --git a/yang/yang-data-api/src/main/java/org/opendaylight/yangtools/yang/data/api/schema/tree/UniqueConstraintException.java b/yang/yang-data-api/src/main/java/org/opendaylight/yangtools/yang/data/api/schema/tree/UniqueConstraintException.java
new file mode 100644 (file)
index 0000000..48cbe7c
--- /dev/null
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2020 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.data.api.schema.tree;
+
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.annotations.Beta;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import java.util.Collections;
+import java.util.Map;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Descendant;
+
+/**
+ * Exception thrown when a {@code unique} statement restrictions are violated.
+ *
+ * @author Robert Varga
+ */
+@Beta
+@NonNullByDefault
+public class UniqueConstraintException extends DataValidationFailedException {
+    private static final long serialVersionUID = 1L;
+
+    // Note: this cannot be an ImmutableMap because we must support null values
+    @SuppressFBWarnings(value = "SE_BAD_FIELD", justification = "Best effort on serialization")
+    private final Map<Descendant, @Nullable Object> values;
+
+    public UniqueConstraintException(final YangInstanceIdentifier path, final Map<Descendant, @Nullable Object> values,
+            final String message) {
+        super(path, message);
+        this.values = requireNonNull(values);
+    }
+
+    public final Map<Descendant, @Nullable Object> values() {
+        return Collections.unmodifiableMap(values);
+    }
+}
index 7901619c0a0d186312a1b8d312546f3ed01f93ca..6935fffe474c013280a21aae83397ec812c52c3f 100644 (file)
@@ -26,11 +26,14 @@ module org.opendaylight.yangtools.yang.data.impl {
     requires org.opendaylight.yangtools.util;
     requires org.opendaylight.yangtools.rfc7952.data.util;
     requires org.opendaylight.yangtools.rfc8528.data.util;
+    requires org.opendaylight.yangtools.yang.common;
+    requires org.opendaylight.yangtools.yang.model.api;
     requires org.opendaylight.yangtools.yang.model.util;
     requires org.slf4j;
 
     // Annotations
     requires static transitive org.eclipse.jdt.annotation;
+    requires static com.github.spotbugs.annotations;
     requires static org.osgi.service.component.annotations;
     requires static javax.inject;
     requires static metainf.services;
index 342745b735999cb1266d5b9b07b22f7601d89d59..50ef3b0b7edc93b2c13ea1285c82d5bee6f7def5 100644 (file)
@@ -172,20 +172,18 @@ abstract class AbstractNodeContainerModificationStrategy<T extends WithStatus>
         }
 
         /*
-         * This is where things get interesting. The user has performed a write and
-         * then she applied some more modifications to it. So we need to make sense
-         * of that an apply the operations on top of the written value. We could have
-         * done it during the write, but this operation is potentially expensive, so
-         * we have left it out of the fast path.
+         * This is where things get interesting. The user has performed a write and then she applied some more
+         * modifications to it. So we need to make sense of that and apply the operations on top of the written value.
          *
-         * As it turns out, once we materialize the written data, we can share the
-         * code path with the subtree change. So let's create an unsealed TreeNode
-         * and run the common parts on it -- which end with the node being sealed.
+         * We could have done it during the write, but this operation is potentially expensive, so we have left it out
+         * of the fast path.
          *
-         * FIXME: this code needs to be moved out from the prepare() path and into
-         *        the read() and seal() paths. Merging of writes needs to be charged
-         *        to the code which originated this, not to the code which is
-         *        attempting to make it visible.
+         * As it turns out, once we materialize the written data, we can share the code path with the subtree change. So
+         * let's create an unsealed TreeNode and run the common parts on it -- which end with the node being sealed.
+         *
+         * FIXME: this code needs to be moved out from the prepare() path and into the read() and seal() paths. Merging
+         *        of writes needs to be charged to the code which originated this, not to the code which is attempting
+         *        to make it visible.
          */
         final MutableTreeNode mutable = newValueMeta.mutable();
         mutable.setSubtreeVersion(version);
@@ -274,12 +272,10 @@ abstract class AbstractNodeContainerModificationStrategy<T extends WithStatus>
             case TOUCH:
 
                 mergeChildrenIntoModification(modification, children, version);
-                // We record empty merge value, since real children merges
-                // are already expanded. This is needed to satisfy non-null for merge
-                // original merge value can not be used since it mean different
-                // order of operation - parent changes are always resolved before
-                // children ones, and having node in TOUCH means children was modified
-                // before.
+                // We record empty merge value, since real children merges are already expanded. This is needed to
+                // satisfy non-null for merge original merge value can not be used since it mean different order of
+                // operation - parent changes are always resolved before children ones, and having node in TOUCH means
+                // children was modified before.
                 modification.updateValue(LogicalOperation.MERGE, support.createEmptyValue(value));
                 return;
             case MERGE:
@@ -320,9 +316,11 @@ abstract class AbstractNodeContainerModificationStrategy<T extends WithStatus>
     @Override
     protected TreeNode applyTouch(final ModifiedNode modification, final TreeNode currentMeta, final Version version) {
         /*
-         * The user may have issued an empty merge operation. In this case we do not perform
-         * a data tree mutation, do not pass GO, and do not collect useless garbage. It
-         * also means the ModificationType is UNMODIFIED.
+         * The user may have issued an empty merge operation. In this case we:
+         * - do not perform a data tree mutation
+         * - do not pass GO, and
+         * - do not collect useless garbage.
+         * It also means the ModificationType is UNMODIFIED.
          */
         final Collection<ModifiedNode> children = modification.getChildren();
         if (!children.isEmpty()) {
@@ -333,12 +331,12 @@ abstract class AbstractNodeContainerModificationStrategy<T extends WithStatus>
             final TreeNode ret = mutateChildren(newMeta, dataBuilder, version, children);
 
             /*
-             * It is possible that the only modifications under this node were empty merges,
-             * which were turned into UNMODIFIED. If that is the case, we can turn this operation
-             * into UNMODIFIED, too, potentially cascading it up to root. This has the benefit
-             * of speeding up any users, who can skip processing child nodes.
+             * It is possible that the only modifications under this node were empty merges, which were turned into
+             * UNMODIFIED. If that is the case, we can turn this operation into UNMODIFIED, too, potentially cascading
+             * it up to root. This has the benefit of speeding up any users, who can skip processing child nodes.
              *
              * In order to do that, though, we have to check all child operations are UNMODIFIED.
+             *
              * Let's do precisely that, stopping as soon we find a different result.
              */
             for (final ModifiedNode child : children) {
diff --git a/yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/BinaryValue.java b/yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/BinaryValue.java
new file mode 100644 (file)
index 0000000..088d0b6
--- /dev/null
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2020 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.data.impl.schema.tree;
+
+import static java.util.Objects.requireNonNull;
+
+import java.util.Arrays;
+import java.util.Base64;
+import org.opendaylight.yangtools.concepts.Immutable;
+
+final class BinaryValue implements Immutable {
+    private final byte[] value;
+
+    private BinaryValue(final byte[] value) {
+        this.value = requireNonNull(value);
+    }
+
+    static Object wrap(final Object value) {
+        return value instanceof byte[] ? new BinaryValue((byte[]) value) : value;
+    }
+
+    static Object wrapToString(final Object value) {
+        return value instanceof byte[] ? toString((byte[]) value) : value;
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(value);
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        return obj == this || obj instanceof BinaryValue && Arrays.equals(value, ((BinaryValue) obj).value);
+    }
+
+    @Override
+    public String toString() {
+        return toString(value);
+    }
+
+    private static String toString(final byte[] value) {
+        return Base64.getEncoder().encodeToString(value);
+    }
+}
index 84e2d7d490d707d074844b609d873819502b6dff..c8018ae90be94d9cc3438022d3ff74da05830e61 100644 (file)
@@ -75,22 +75,20 @@ final class ListModificationStrategy extends SchemaAwareApplyOperation<ListSchem
         }
 
         /*
-         * This is where things get interesting. The user has performed a write and
-         * then she applied some more modifications to it. So we need to make sense
-         * of that an apply the operations on top of the written value. We could have
-         * done it during the write, but this operation is potentially expensive, so
-         * we have left it out of the fast path.
+         * This is where things get interesting. The user has performed a write and then she applied some more
+         * modifications to it. So we need to make sense of that an apply the operations on top of the written value. We
+         * could have done it during the write, but this operation is potentially expensive, so we have left it out of
+         * the fast path.
          *
-         * As it turns out, once we materialize the written data, we can share the
-         * code path with the subtree change. So let's create an unsealed TreeNode
-         * and run the common parts on it -- which end with the node being sealed.
+         * As it turns out, once we materialize the written data, we can share the code path with the subtree change. So
+         * let's create an unsealed TreeNode and run the common parts on it -- which end with the node being sealed.
          */
         final MutableTreeNode mutable = newValueMeta.mutable();
         mutable.setSubtreeVersion(version);
 
         @SuppressWarnings("rawtypes")
-        final NormalizedNodeContainerBuilder dataBuilder = ImmutableUnkeyedListEntryNodeBuilder
-            .create((UnkeyedListEntryNode) newValue);
+        final NormalizedNodeContainerBuilder dataBuilder =
+            ImmutableUnkeyedListEntryNodeBuilder.create((UnkeyedListEntryNode) newValue);
 
         return mutateChildren(mutable, dataBuilder, version, modification.getChildren());
     }
index 9b402a64d79a5296ffdcfa2e0bf48327545d6065..75d4ae93f0fadaf6f22ba5dc9a8b98a86b74ce50 100644 (file)
@@ -18,26 +18,29 @@ import org.opendaylight.yangtools.yang.data.api.schema.tree.spi.TreeNode;
 import org.opendaylight.yangtools.yang.data.api.schema.tree.spi.Version;
 
 /**
- * Operation responsible for applying {@link ModifiedNode} on tree.
- *
- * <p>
- * Operation is composite - operation on top level node consists of
- * suboperations on child nodes. This allows to walk operation hierarchy and
+ * An operation responsible for applying {@link ModifiedNode} on tree. The operation is a hierachical composite -
+ * the operation on top level node consists of suboperations on child nodes. This allows to walk operation hierarchy and
  * invoke suboperations independently.
  *
  * <p>
  * <b>Implementation notes</b>
  * <ul>
- * <li>
- * Implementations MUST expose all nested suboperations which operates on child
- * nodes expose via {@link #getChild(PathArgument)} method.
- * <li>Same suboperations SHOULD be used when invoked via
- * {@link #apply(ModifiedNode, Optional, Version)} if applicable.
+ *   <li>Implementations MUST expose all nested suboperations which operates on child nodes expose via
+ *       {@link #getChild(PathArgument)} method.</li>
+ *   <li>Same suboperations SHOULD be used when invoked via {@link #apply(ModifiedNode, Optional, Version)},
+ *       if applicable.</li>
+ *   <li>There are exactly two base implementations:
+ *     <ul>
+ *       <li>{@link SchemaAwareApplyOperation}, which serves as the base class for stateful mutators -- directly
+ *           impacting the layout and transitions of the {@link TreeNode} hierarchy.
+ *       <li>{@link AbstractValidation}, which serves as the base class for stateless checks, which work purely on top
+ *           of the {@link TreeNode} hierarchy. These are always overlaid on top of some other
+ *           {@link ModificationApplyOperation}, ultimately leading to a {@link SchemaAwareApplyOperation}.
+ *     </ul>
+ *     This allows baseline invocations from {@link OperationWithModification} to be bimorphic in the first line of
+ *     dispatch.
+ *   </li>
  * </ul>
- *
- * <p>
- * Hierarchical composite operation which is responsible for applying
- * modification on particular subtree and creating updated subtree
  */
 abstract class ModificationApplyOperation implements StoreTreeNode<ModificationApplyOperation> {
     /**
index ae8440ce84db1ec3d3dfed30a43f38fbe16f9080..9531d837ac5b4f5d17897dd0d75572cf031d825c 100644 (file)
@@ -104,7 +104,7 @@ abstract class SchemaAwareApplyOperation<T extends WithStatus> extends Modificat
             op = MapModificationStrategy.of(schemaNode, treeConfig);
         }
 
-        return MinMaxElementsValidation.from(op);
+        return UniqueValidation.of(schemaNode, treeConfig, MinMaxElementsValidation.from(op));
     }
 
     protected static void checkNotConflicting(final ModificationPath path, final TreeNode original,
diff --git a/yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueValidation.java b/yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueValidation.java
new file mode 100644 (file)
index 0000000..b308a19
--- /dev/null
@@ -0,0 +1,144 @@
+/*
+ * Copyright (c) 2020 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.data.impl.schema.tree;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+
+import com.google.common.base.MoreObjects.ToStringHelper;
+import com.google.common.base.Stopwatch;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
+import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.UniqueConstraintException;
+import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
+import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Descendant;
+import org.opendaylight.yangtools.yang.model.api.stmt.UniqueEffectiveStatement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A {@link AbstractValidation} which ensures a particular {@code list} node complies with its {@code unique}
+ * constraints.
+ */
+final class UniqueValidation extends AbstractValidation {
+    private static final Logger LOG = LoggerFactory.getLogger(UniqueValidation.class);
+
+    private final @NonNull ImmutableList<UniqueValidator<?>> validators;
+
+    private UniqueValidation(final ModificationApplyOperation delegate, final List<UniqueValidator<?>> validators) {
+        super(delegate);
+        this.validators = ImmutableList.copyOf(validators);
+    }
+
+    static ModificationApplyOperation of(final ListSchemaNode schema, final DataTreeConfiguration treeConfig,
+            final ModificationApplyOperation delegate) {
+        final Collection<? extends @NonNull UniqueEffectiveStatement> uniques = schema.getUniqueConstraints();
+        if (!treeConfig.isUniqueIndexEnabled() || uniques.isEmpty()) {
+            return delegate;
+        }
+
+        final Stopwatch sw = Stopwatch.createStarted();
+        final Map<Descendant, List<NodeIdentifier>> paths = new HashMap<>();
+        final List<UniqueValidator<?>> validators = uniques.stream()
+            .map(unique -> UniqueValidator.of(unique.argument().stream()
+                .map(descendant -> paths.computeIfAbsent(descendant, key -> toDescendantPath(schema, key)))
+                .collect(ImmutableList.toImmutableList())))
+            .collect(ImmutableList.toImmutableList());
+        LOG.debug("Constructed {} validators in {}", validators.size(), sw);
+
+        return validators.isEmpty() ? delegate : new UniqueValidation(delegate, validators);
+    }
+
+    @Override
+    void enforceOnData(final NormalizedNode<?, ?> data) {
+        enforceOnData(data, (message, values) -> new IllegalArgumentException(message));
+    }
+
+    @Override
+    void enforceOnData(final ModificationPath path, final NormalizedNode<?, ?> data)
+            throws UniqueConstraintException {
+        enforceOnData(data, (message, values) -> new UniqueConstraintException(path.toInstanceIdentifier(), values,
+            message));
+    }
+
+    private <T extends @NonNull Exception> void enforceOnData(final NormalizedNode<?, ?> data,
+            final ExceptionSupplier<T> exceptionSupplier) throws T {
+        final Stopwatch sw = Stopwatch.createStarted();
+        verify(data instanceof NormalizedNodeContainer, "Unexpected data %s", data);
+        final var children = ((NormalizedNodeContainer<?, ?, ?>) data).getValue();
+        final var collected = HashMultimap.<UniqueValidator<?>, Object>create(validators.size(), children.size());
+        for (NormalizedNode<?, ?> child : children) {
+            verify(child instanceof DataContainerNode, "Unexpected child %s", child);
+            final DataContainerNode<?> cont = (DataContainerNode<?>) child;
+
+            final Map<List<NodeIdentifier>, Object> valueCache = new HashMap<>();
+            for (UniqueValidator<?> validator : validators) {
+                final Object values = validator.extractValues(valueCache, cont);
+                final Object masked = BinaryValue.wrap(values);
+                if (!collected.put(validator, masked)) {
+                    final Map<Descendant, @Nullable Object> index = validator.indexValues(values);
+                    throw exceptionSupplier.get(cont.getIdentifier()
+                        + " violates unique constraint on " + masked + " of " + index.keySet(), index);
+                }
+            }
+        }
+
+        LOG.trace("Enforced {} validators in {}", validators.size(), sw);
+    }
+
+    @Override
+    ToStringHelper addToStringAttributes(final ToStringHelper helper) {
+        return super.addToStringAttributes(helper.add("validators", validators));
+    }
+
+    private static ImmutableList<NodeIdentifier> toDescendantPath(final ListSchemaNode parent,
+            final Descendant descendant) {
+        final List<QName> qnames = descendant.getNodeIdentifiers();
+        final ImmutableList.Builder<NodeIdentifier> builder = ImmutableList.builderWithExpectedSize(qnames.size());
+        final Iterator<QName> it = descendant.getNodeIdentifiers().iterator();
+        DataNodeContainer current = parent;
+        while (true) {
+            final QName qname = it.next();
+            final DataSchemaNode next = current.findDataChildByName(qname)
+                .orElseThrow(() -> new IllegalStateException("Cannot find component " + qname + " of " + descendant));
+            builder.add(NodeIdentifier.create(qname));
+            if (!it.hasNext()) {
+                checkState(next instanceof TypedDataSchemaNode, "Unexpected schema %s for %s", next, descendant);
+                final ImmutableList<NodeIdentifier> ret = builder.build();
+                LOG.trace("Resolved {} to {}", descendant, ret);
+                return ret;
+            }
+
+            checkState(next instanceof DataNodeContainer, "Unexpected non-container %s for %s", next, descendant);
+            current = (DataNodeContainer) next;
+        }
+    }
+
+    @FunctionalInterface
+    @NonNullByDefault
+    interface ExceptionSupplier<T extends Exception> {
+        T get(String message, Map<Descendant, @Nullable Object> values);
+    }
+}
diff --git a/yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueValidator.java b/yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueValidator.java
new file mode 100644 (file)
index 0000000..b7aeae1
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2020 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.data.impl.schema.tree;
+
+import static com.google.common.base.Preconditions.checkState;
+import static com.google.common.base.Verify.verify;
+import static java.util.Objects.requireNonNull;
+
+import com.google.common.base.MoreObjects;
+import com.google.common.collect.Collections2;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Maps;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerChild;
+import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
+import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
+import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Descendant;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * A validator for a single {@code unique} constraint. This class is further specialized for single- and
+ * multiple-constraint implementations.
+ *
+ * <p>
+ * The basic idea is that for each list entry there is a corresponding value vector of one or more values, each
+ * corresponding to one component of the {@code unique} constraint.
+ */
+abstract class UniqueValidator<T> implements Immutable {
+    private static final class One extends UniqueValidator<Object> {
+        One(final List<NodeIdentifier> path) {
+            super(encodePath(path));
+        }
+
+        @Override
+        Object extractValues(final Map<List<NodeIdentifier>, Object> valueCache, final DataContainerNode<?> data) {
+            return extractValue(valueCache, data, decodePath(descendants));
+        }
+
+        @Override
+        Map<Descendant, @Nullable Object> indexValues(final Object values) {
+            return Collections.singletonMap(decodeDescendant(descendants), values);
+        }
+    }
+
+    private static final class Many extends UniqueValidator<Set<Object>> {
+        Many(final List<List<NodeIdentifier>> descendantPaths) {
+            super(descendantPaths.stream().map(UniqueValidator::encodePath).collect(ImmutableSet.toImmutableSet()));
+        }
+
+        @Override
+        UniqueValues extractValues(final Map<List<NodeIdentifier>, Object> valueCache,
+                final DataContainerNode<?> data) {
+            return descendants.stream()
+                .map(obj -> extractValue(valueCache, data, decodePath(obj)))
+                .collect(UniqueValues.COLLECTOR);
+        }
+
+        @Override
+        Map<Descendant, @Nullable Object> indexValues(final Object values) {
+            final Map<Descendant, @Nullable Object> index = Maps.newHashMapWithExpectedSize(descendants.size());
+            final Iterator<?> it = ((UniqueValues) values).iterator();
+            for (Object obj : descendants) {
+                verify(index.put(decodeDescendant(obj), it.next()) == null);
+            }
+            return index;
+        }
+    }
+
+    private static final Logger LOG = LoggerFactory.getLogger(UniqueValidator.class);
+
+    final @NonNull T descendants;
+
+    UniqueValidator(final T descendants) {
+        this.descendants = requireNonNull(descendants);
+    }
+
+    static UniqueValidator<?> of(final List<List<NodeIdentifier>> descendants) {
+        return descendants.size() == 1 ? new One(descendants.get(0)) : new Many(descendants);
+    }
+
+    /**
+     * Extract a value vector from a particular child.
+     *
+     * @param valueCache Cache of descendants already looked up
+     * @param data Root data node
+     * @return Value vector
+     */
+    abstract @Nullable Object extractValues(Map<List<NodeIdentifier>, Object> valueCache,
+        DataContainerNode<?> data);
+
+    /**
+     * Index a value vector by associating each value with its corresponding {@link Descendant}.
+     *
+     * @param values Value vector
+     * @return Map of Descandant/value relations
+     */
+    abstract Map<Descendant, @Nullable Object> indexValues(Object values);
+
+    /**
+     * Encode a path for storage. Single-element paths are squashed to their only element. The inverse operation is
+     * {@link #decodePath(Object)}.
+     *
+     * @param path Path to encode
+     * @return Encoded path.
+     */
+    static final Object encodePath(final List<NodeIdentifier> path) {
+        return path.size() == 1 ? path.get(0) : ImmutableList.copyOf(path);
+    }
+
+    /**
+     * Decode a path from storage. This is the inverse operation to {@link #encodePath(List)}.
+     *
+     * @param obj Encoded path
+     * @return Decoded path
+     */
+    static final @NonNull ImmutableList<NodeIdentifier> decodePath(final Object obj) {
+        return obj instanceof NodeIdentifier ? ImmutableList.of((NodeIdentifier) obj)
+            : (ImmutableList<NodeIdentifier>) obj;
+    }
+
+    static final @NonNull Descendant decodeDescendant(final Object obj) {
+        return Descendant.of(Collections2.transform(decodePath(obj), NodeIdentifier::getNodeType));
+    }
+
+    /**
+     * Extract the value for a single descendant.
+     *
+     * @param valueCache Cache of descendants already looked up
+     * @param data Root data node
+     * @param path Descendant path
+     * @return Value for the descendant
+     */
+    static final @Nullable Object extractValue(final Map<List<NodeIdentifier>, Object> valueCache,
+            final DataContainerNode<?> data, final List<NodeIdentifier> path) {
+        return valueCache.computeIfAbsent(path, key -> extractValue(data, key));
+    }
+
+    /**
+     * Extract the value for a single descendant.
+     *
+     * @param data Root data node
+     * @param path Descendant path
+     * @return Value for the descendant
+     */
+    private static @Nullable Object extractValue(final DataContainerNode<?> data, final List<NodeIdentifier> path) {
+        DataContainerNode<?> current = data;
+        final Iterator<NodeIdentifier> it = path.iterator();
+        while (true) {
+            final NodeIdentifier step = it.next();
+            final Optional<DataContainerChild<?, ?>> optNext = current.getChild(step);
+            if (optNext.isEmpty()) {
+                return null;
+            }
+
+            final DataContainerChild<?, ?> next = optNext.orElseThrow();
+            if (!it.hasNext()) {
+                checkState(next instanceof LeafNode, "Unexpected node %s at %s", next, path);
+                final Object value = next.getValue();
+                LOG.trace("Resolved {} to value {}", path, value);
+                return value;
+            }
+
+            checkState(next instanceof DataContainerNode, "Unexpected node %s in %s", next, path);
+            current = (DataContainerNode<?>) next;
+        }
+    }
+
+    @Override
+    public final String toString() {
+        return MoreObjects.toStringHelper(this).add("paths", descendants).toString();
+    }
+}
diff --git a/yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueValues.java b/yang/yang-data-impl/src/main/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueValues.java
new file mode 100644 (file)
index 0000000..46ac64d
--- /dev/null
@@ -0,0 +1,94 @@
+/*
+ * Copyright (c) 2020 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.data.impl.schema.tree;
+
+import static com.google.common.base.Verify.verify;
+import static java.util.Objects.requireNonNull;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.stream.Collector;
+import org.opendaylight.yangtools.concepts.Immutable;
+
+/**
+ * A vector of values associated with a unique constraint. This is almost an {@link ArrayList}, except it is
+ * unmodifiable.
+ */
+final class UniqueValues implements Immutable, Iterable<Object> {
+    static final Collector<Object, ?, UniqueValues> COLLECTOR = Collector.of(ArrayList::new, ArrayList::add,
+        (left, right) -> {
+            left.addAll(right);
+            return left;
+        },
+        list -> new UniqueValues(list.toArray()));
+
+    private final Object[] objects;
+    private final int hashCode;
+
+    private UniqueValues(final Object[] objects) {
+        verify(objects.length != 0);
+        this.objects = objects;
+        this.hashCode = Arrays.deepHashCode(objects);
+    }
+
+    @Override
+    public Iterator<Object> iterator() {
+        return new Itr(objects);
+    }
+
+    @Override
+    public int hashCode() {
+        return hashCode;
+    }
+
+    @Override
+    public boolean equals(final Object obj) {
+        return this == obj || obj instanceof UniqueValues && Arrays.deepEquals(objects, ((UniqueValues) obj).objects);
+    }
+
+    @Override
+    public String toString() {
+        return toString(objects);
+    }
+
+    private static String toString(final Object[] objects) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append('[').append(BinaryValue.wrapToString(objects[0]));
+        for (int i = 1; i < objects.length; ++i) {
+            sb.append(", ").append(BinaryValue.wrapToString(objects[i]));
+        }
+        return sb.append(']').toString();
+    }
+
+    private static final class Itr implements Iterator<Object> {
+        private final Object[] objects;
+
+        private int offset = 0;
+
+        Itr(final Object[] objects) {
+            this.objects = requireNonNull(objects);
+        }
+
+        @Override
+        public boolean hasNext() {
+            return offset < objects.length;
+        }
+
+        @Override
+        public Object next() {
+            int local = offset;
+            if (local >= objects.length) {
+                throw new NoSuchElementException();
+            }
+            offset = local + 1;
+            return objects[local];
+        }
+    }
+}
diff --git a/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueConstraintTest.java b/yang/yang-data-impl/src/test/java/org/opendaylight/yangtools/yang/data/impl/schema/tree/UniqueConstraintTest.java
new file mode 100644 (file)
index 0000000..4db997a
--- /dev/null
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2016 Cisco Systems, Inc. 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.data.impl.schema.tree;
+
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertThrows;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
+import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
+import org.opendaylight.yangtools.yang.data.api.schema.MapEntryNode;
+import org.opendaylight.yangtools.yang.data.api.schema.MapNode;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeCandidate;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.DataValidationFailedException;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.TreeType;
+import org.opendaylight.yangtools.yang.data.api.schema.tree.UniqueConstraintException;
+import org.opendaylight.yangtools.yang.data.impl.schema.Builders;
+import org.opendaylight.yangtools.yang.data.impl.schema.ImmutableNodes;
+import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
+import org.opendaylight.yangtools.yang.parser.spi.meta.ReactorException;
+
+public class UniqueConstraintTest {
+    private static final String NS = "foo";
+    private static final String REV = "2016-05-17";
+    private static final QName TASK_CONTAINER = QName.create(NS, REV, "task-container");
+    private static final QName TASK = QName.create(NS, REV, "task");
+    private static final QName TASK_ID = QName.create(NS, REV, "task-id");
+    private static final QName MY_LEAF_1 = QName.create(NS, REV, "my-leaf-1");
+    private static final QName MY_LEAF_2 = QName.create(NS, REV, "my-leaf-2");
+    private static final QName MY_LEAF_3 = QName.create(NS, REV, "my-leaf-3");
+    private static final QName MY_CONTAINER = QName.create(NS, REV, "my-container");
+
+    private static EffectiveModelContext TEST_MODEL;
+
+    @BeforeClass
+    public static void beforeClass() {
+        TEST_MODEL = TestModel.createTestContext("/yt570.yang");
+    }
+
+    @Test
+    public void switchEntriesTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = initDataTree(TEST_MODEL, true);
+        writeMapEntry(inMemoryDataTree, "1", "l1", "l2", "l3");
+        writeMapEntry(inMemoryDataTree, "2", "l2", "l3", "l4");
+
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+
+        final MapEntryNode mapEntry1 = createMapEntry("1", "l2", "l3", "l4");
+        final MapEntryNode mapEntry2 = createMapEntry("2", "l1", "l2", "l3");
+
+        //switch values of map entries
+        modificationTree.write(
+                YangInstanceIdentifier.of(TASK_CONTAINER).node(TASK)
+                        .node(NodeIdentifierWithPredicates.of(TASK, TASK_ID, "1")), mapEntry1);
+        modificationTree.write(
+                YangInstanceIdentifier.of(TASK_CONTAINER).node(TASK)
+                        .node(NodeIdentifierWithPredicates.of(TASK, TASK_ID, "2")), mapEntry2);
+
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    @Test
+    public void mapTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = emptyDataTree(TEST_MODEL, true);
+
+
+        verifyExceptionMessage(assertThrows(IllegalArgumentException.class, () -> writeMap(inMemoryDataTree, true)),
+            "(foo?revision=2016-05-17)task[{(foo?revision=2016-05-17)task-id=",
+            "}] violates unique constraint on [l2, l1] of ",
+            "(foo?revision=2016-05-17)my-leaf-1",
+            "(foo?revision=2016-05-17)my-leaf-2]");
+
+        writeMap(inMemoryDataTree, false);
+        verifyExceptionMessage(assertThrows(UniqueConstraintException.class,
+            () -> writeMapEntry(inMemoryDataTree, "4", "l1", "l2", "l30")),
+            "(foo?revision=2016-05-17)task[{(foo?revision=2016-05-17)task-id=",
+            "}] violates unique constraint on [l2, l1] of ",
+            "(foo?revision=2016-05-17)my-leaf-1",
+            "(foo?revision=2016-05-17)my-leaf-2");
+    }
+
+    @Test
+    public void mapEntryTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = initDataTree(TEST_MODEL, true);
+        writeAndRemoveMapEntries(inMemoryDataTree, true);
+        writeAndRemoveMapEntries(inMemoryDataTree, false);
+    }
+
+    private static void writeAndRemoveMapEntries(final InMemoryDataTree inMemoryDataTree, final boolean clear)
+            throws DataValidationFailedException {
+        writeMapEntry(inMemoryDataTree, "1", "l1", "l2", "l3");
+        writeMapEntry(inMemoryDataTree, "2", "l2", "l3", "l4");
+        writeMapEntry(inMemoryDataTree, "3", "l3", "l4", "l5");
+        writeMapEntry(inMemoryDataTree, "2", "l2", "l3", "l6");
+        writeMapEntry(inMemoryDataTree, "10", "l2", "l10", "l4");
+        verifyExceptionMessage(assertThrows(UniqueConstraintException.class,
+            () -> writeMapEntry(inMemoryDataTree, "4", "l1", "l5", "l3")),
+            "(foo?revision=2016-05-17)task[{(foo?revision=2016-05-17)task-id=",
+                    "}] violates unique constraint on [l1, l3] of ",
+                    "(foo?revision=2016-05-17)my-container, my-leaf-3",
+                    "(foo?revision=2016-05-17)my-leaf-1");
+        writeMapEntry(inMemoryDataTree, "4", "l4", "l5", "l6");
+        verifyExceptionMessage(assertThrows(UniqueConstraintException.class,
+            () -> writeMapEntry(inMemoryDataTree, "5", "l3", "l4", "l7")),
+            "(foo?revision=2016-05-17)task[{(foo?revision=2016-05-17)task-id=",
+            "}] violates unique constraint on [l4, l3] of ",
+            "(foo?revision=2016-05-17)my-leaf-1",
+            "(foo?revision=2016-05-17)my-leaf-2");
+        removeMapEntry(inMemoryDataTree, taskEntryKey("3"));
+        writeMapEntry(inMemoryDataTree, "5", "l3", "l4", "l7");
+        writeMapEntry(inMemoryDataTree, "5", "l3", "l4", "l7");
+        verifyExceptionMessage(assertThrows(UniqueConstraintException.class,
+            () -> writeMapEntry(inMemoryDataTree, "6", "l3", "l4", "l11")),
+            "(foo?revision=2016-05-17)task[{(foo?revision=2016-05-17)task-id=",
+            "}] violates unique constraint on [l4, l3] of ",
+            "(foo?revision=2016-05-17)my-leaf-1",
+            "(foo?revision=2016-05-17)my-leaf-2");
+
+        if (clear) {
+            removeMapEntry(inMemoryDataTree, taskEntryKey("1"));
+            removeMapEntry(inMemoryDataTree, taskEntryKey("2"));
+            removeMapEntry(inMemoryDataTree, taskEntryKey("4"));
+            removeMapEntry(inMemoryDataTree, taskEntryKey("5"));
+            removeMapEntry(inMemoryDataTree, taskEntryKey("10"));
+        }
+    }
+
+    private static void verifyExceptionMessage(final Exception ex, final String expectedStart,
+            final String... expectedLeaves) {
+        verifyExceptionMessage(expectedStart,  ex.getMessage(), expectedLeaves);
+    }
+
+    private static void verifyExceptionMessage(final String expectedStart, final String message,
+            final String... leafs) {
+        assertThat(message, startsWith(expectedStart));
+        for (final String leaf : leafs) {
+            assertThat(message, containsString(leaf));
+        }
+    }
+
+    private static void writeMap(final InMemoryDataTree inMemoryDataTree, final boolean withUniqueViolation)
+            throws DataValidationFailedException {
+        final MapNode taskNode = Builders
+                .mapBuilder()
+                .withNodeIdentifier(new NodeIdentifier(TASK))
+                .withChild(createMapEntry("1", "l1", "l2", "l3"))
+                .withChild(createMapEntry("2", "l2", "l3", "l4"))
+                .withChild(
+                        withUniqueViolation ? createMapEntry("3", "l1", "l2", "l10") : createMapEntry("3", "l3", "l4",
+                                "l5")).build();
+
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.write(YangInstanceIdentifier.of(TASK_CONTAINER).node(TASK), taskNode);
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    private static void writeMapEntry(final InMemoryDataTree inMemoryDataTree, final Object taskIdValue,
+            final Object myLeaf1Value, final Object myLeaf2Value, final Object myLeaf3Value)
+            throws DataValidationFailedException {
+        final MapEntryNode taskEntryNode = Builders
+                .mapEntryBuilder()
+                .withNodeIdentifier(NodeIdentifierWithPredicates.of(TASK, TASK_ID, taskIdValue))
+                .withChild(ImmutableNodes.leafNode(TASK_ID, taskIdValue))
+                .withChild(ImmutableNodes.leafNode(MY_LEAF_1, myLeaf1Value))
+                .withChild(ImmutableNodes.leafNode(MY_LEAF_2, myLeaf2Value))
+                .withChild(
+                        Builders.containerBuilder().withNodeIdentifier(new NodeIdentifier(MY_CONTAINER))
+                                .withChild(ImmutableNodes.leafNode(MY_LEAF_3, myLeaf3Value)).build()).build();
+
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.write(
+                YangInstanceIdentifier.of(TASK_CONTAINER).node(TASK)
+                        .node(NodeIdentifierWithPredicates.of(TASK, TASK_ID, taskIdValue)),
+                taskEntryNode);
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    private static void removeMapEntry(final InMemoryDataTree inMemoryDataTree,
+            final NodeIdentifierWithPredicates mapEntryKey) throws DataValidationFailedException {
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.delete(YangInstanceIdentifier.of(TASK_CONTAINER).node(TASK).node(mapEntryKey));
+        modificationTree.ready();
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+    }
+
+    private static MapEntryNode createMapEntry(final Object taskIdValue, final Object myLeaf1Value,
+            final Object myLeaf2Value, final Object myLeaf3Value) throws DataValidationFailedException {
+        return Builders
+                .mapEntryBuilder()
+                .withNodeIdentifier(NodeIdentifierWithPredicates.of(TASK, TASK_ID, taskIdValue))
+                .withChild(ImmutableNodes.leafNode(TASK_ID, taskIdValue))
+                .withChild(ImmutableNodes.leafNode(MY_LEAF_1, myLeaf1Value))
+                .withChild(ImmutableNodes.leafNode(MY_LEAF_2, myLeaf2Value))
+                .withChild(
+                        Builders.containerBuilder().withNodeIdentifier(new NodeIdentifier(MY_CONTAINER))
+                                .withChild(ImmutableNodes.leafNode(MY_LEAF_3, myLeaf3Value)).build()).build();
+    }
+
+    private static NodeIdentifierWithPredicates taskEntryKey(final String taskId) {
+        return NodeIdentifierWithPredicates.of(TASK, TASK_ID, taskId);
+    }
+
+    @Test
+    public void disabledUniqueIndexTest() throws ReactorException, DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = initDataTree(TEST_MODEL, false);
+
+        writeMapEntry(inMemoryDataTree, "1", "l1", "l2", "l3");
+        writeMapEntry(inMemoryDataTree, "2", "l2", "l3", "l4");
+        writeMapEntry(inMemoryDataTree, "3", "l3", "l4", "l5");
+        writeMapEntry(inMemoryDataTree, "2", "l2", "l3", "l6");
+        writeMapEntry(inMemoryDataTree, "10", "l2", "l10", "l4");
+        writeMapEntry(inMemoryDataTree, "4", "l1", "l5", "l3");
+        writeMapEntry(inMemoryDataTree, "4", "l4", "l5", "l6");
+        writeMapEntry(inMemoryDataTree, "5", "l3", "l4", "l7");
+        removeMapEntry(inMemoryDataTree, taskEntryKey("3"));
+        writeMapEntry(inMemoryDataTree, "5", "l3", "l4", "l7");
+        writeMapEntry(inMemoryDataTree, "5", "l3", "l4", "l7");
+        writeMapEntry(inMemoryDataTree, "6", "l3", "l4", "l7");
+    }
+
+    private static InMemoryDataTree initDataTree(final EffectiveModelContext schemaContext, final boolean uniqueIndex)
+            throws DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = (InMemoryDataTree) new InMemoryDataTreeFactory().create(
+            new DataTreeConfiguration.Builder(TreeType.CONFIGURATION).setUniqueIndexes(uniqueIndex).build());
+        inMemoryDataTree.setEffectiveModelContext(schemaContext);
+
+        final MapNode taskNode = Builders.mapBuilder().withNodeIdentifier(new NodeIdentifier(TASK)).build();
+        final InMemoryDataTreeModification modificationTree = inMemoryDataTree.takeSnapshot().newModification();
+        modificationTree.write(YangInstanceIdentifier.of(TASK_CONTAINER).node(TASK), taskNode);
+        modificationTree.ready();
+
+        inMemoryDataTree.validate(modificationTree);
+        final DataTreeCandidate prepare = inMemoryDataTree.prepare(modificationTree);
+        inMemoryDataTree.commit(prepare);
+        return inMemoryDataTree;
+    }
+
+    private static InMemoryDataTree emptyDataTree(final EffectiveModelContext schemaContext, final boolean uniqueIndex)
+            throws DataValidationFailedException {
+        final InMemoryDataTree inMemoryDataTree = (InMemoryDataTree) new InMemoryDataTreeFactory().create(
+            new DataTreeConfiguration.Builder(TreeType.CONFIGURATION).setUniqueIndexes(uniqueIndex).build());
+        inMemoryDataTree.setEffectiveModelContext(schemaContext);
+
+        return inMemoryDataTree;
+    }
+}
diff --git a/yang/yang-data-impl/src/test/resources/yt570.yang b/yang/yang-data-impl/src/test/resources/yt570.yang
new file mode 100644 (file)
index 0000000..a1ee7fc
--- /dev/null
@@ -0,0 +1,82 @@
+module foo {
+    yang-version 1;
+    namespace "foo";
+    prefix foo;
+
+    revision 2016-05-17 {
+        description "test";
+    }
+
+    container task-container {
+        list task {
+            key "task-id";
+            unique "my-leaf-1 my-leaf-2";
+            unique "my-leaf-1 my-container/my-leaf-3";
+
+            leaf task-id {
+                type string;
+            }
+            leaf my-leaf-1 {
+                type string;
+            }
+            leaf my-leaf-2 {
+                type string;
+            }
+            container my-container {
+                leaf my-leaf-3 {
+                    type string;
+                }
+            }
+        }
+    }
+
+    list inside-list {
+        list inner {
+            leaf inner-leaf {
+                type string;
+            }
+        }
+
+        unique "inner/inner-leaf";
+    }
+
+    list cross-list {
+        key "k";
+        leaf k {
+            type string;
+        }
+        list one {
+            key "one";
+            leaf one {
+                type string;
+            }
+            leaf one-leaf {
+                type string;
+            }
+        }
+
+        list two {
+            key "two";
+            leaf two {
+                type string;
+            }
+            leaf two-leaf {
+                type string;
+            }
+        }
+
+        unique "one/one-leaf two/two-leaf";
+    }
+
+    list nested-list {
+        list one {
+            list two {
+                leaf two-leaf {
+                    type string;
+                }
+            }
+        }
+
+        unique "one/two/two-leaf";
+    }
+}