Add UniqueValidation
[yangtools.git] / yang / yang-data-impl / src / main / java / org / opendaylight / yangtools / yang / data / impl / schema / tree / UniqueValidation.java
1 /*
2  * Copyright (c) 2020 PANTHEON.tech, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.data.impl.schema.tree;
9
10 import static com.google.common.base.Preconditions.checkState;
11 import static com.google.common.base.Verify.verify;
12
13 import com.google.common.base.MoreObjects.ToStringHelper;
14 import com.google.common.base.Stopwatch;
15 import com.google.common.collect.HashMultimap;
16 import com.google.common.collect.ImmutableList;
17 import java.util.Collection;
18 import java.util.HashMap;
19 import java.util.Iterator;
20 import java.util.List;
21 import java.util.Map;
22 import org.eclipse.jdt.annotation.NonNull;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.opendaylight.yangtools.yang.common.QName;
26 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
27 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
28 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
29 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNodeContainer;
30 import org.opendaylight.yangtools.yang.data.api.schema.tree.DataTreeConfiguration;
31 import org.opendaylight.yangtools.yang.data.api.schema.tree.UniqueConstraintException;
32 import org.opendaylight.yangtools.yang.model.api.DataNodeContainer;
33 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
34 import org.opendaylight.yangtools.yang.model.api.ListSchemaNode;
35 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
36 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier.Descendant;
37 import org.opendaylight.yangtools.yang.model.api.stmt.UniqueEffectiveStatement;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
40
41 /**
42  * A {@link AbstractValidation} which ensures a particular {@code list} node complies with its {@code unique}
43  * constraints.
44  */
45 final class UniqueValidation extends AbstractValidation {
46     private static final Logger LOG = LoggerFactory.getLogger(UniqueValidation.class);
47
48     private final @NonNull ImmutableList<UniqueValidator<?>> validators;
49
50     private UniqueValidation(final ModificationApplyOperation delegate, final List<UniqueValidator<?>> validators) {
51         super(delegate);
52         this.validators = ImmutableList.copyOf(validators);
53     }
54
55     static ModificationApplyOperation of(final ListSchemaNode schema, final DataTreeConfiguration treeConfig,
56             final ModificationApplyOperation delegate) {
57         final Collection<? extends @NonNull UniqueEffectiveStatement> uniques = schema.getUniqueConstraints();
58         if (!treeConfig.isUniqueIndexEnabled() || uniques.isEmpty()) {
59             return delegate;
60         }
61
62         final Stopwatch sw = Stopwatch.createStarted();
63         final Map<Descendant, List<NodeIdentifier>> paths = new HashMap<>();
64         final List<UniqueValidator<?>> validators = uniques.stream()
65             .map(unique -> UniqueValidator.of(unique.argument().stream()
66                 .map(descendant -> paths.computeIfAbsent(descendant, key -> toDescendantPath(schema, key)))
67                 .collect(ImmutableList.toImmutableList())))
68             .collect(ImmutableList.toImmutableList());
69         LOG.debug("Constructed {} validators in {}", validators.size(), sw);
70
71         return validators.isEmpty() ? delegate : new UniqueValidation(delegate, validators);
72     }
73
74     @Override
75     void enforceOnData(final NormalizedNode<?, ?> data) {
76         enforceOnData(data, (message, values) -> new IllegalArgumentException(message));
77     }
78
79     @Override
80     void enforceOnData(final ModificationPath path, final NormalizedNode<?, ?> data)
81             throws UniqueConstraintException {
82         enforceOnData(data, (message, values) -> new UniqueConstraintException(path.toInstanceIdentifier(), values,
83             message));
84     }
85
86     private <T extends @NonNull Exception> void enforceOnData(final NormalizedNode<?, ?> data,
87             final ExceptionSupplier<T> exceptionSupplier) throws T {
88         final Stopwatch sw = Stopwatch.createStarted();
89         verify(data instanceof NormalizedNodeContainer, "Unexpected data %s", data);
90         final var children = ((NormalizedNodeContainer<?, ?, ?>) data).getValue();
91         final var collected = HashMultimap.<UniqueValidator<?>, Object>create(validators.size(), children.size());
92         for (NormalizedNode<?, ?> child : children) {
93             verify(child instanceof DataContainerNode, "Unexpected child %s", child);
94             final DataContainerNode<?> cont = (DataContainerNode<?>) child;
95
96             final Map<List<NodeIdentifier>, Object> valueCache = new HashMap<>();
97             for (UniqueValidator<?> validator : validators) {
98                 final Object values = validator.extractValues(valueCache, cont);
99                 final Object masked = BinaryValue.wrap(values);
100                 if (!collected.put(validator, masked)) {
101                     final Map<Descendant, @Nullable Object> index = validator.indexValues(values);
102                     throw exceptionSupplier.get(cont.getIdentifier()
103                         + " violates unique constraint on " + masked + " of " + index.keySet(), index);
104                 }
105             }
106         }
107
108         LOG.trace("Enforced {} validators in {}", validators.size(), sw);
109     }
110
111     @Override
112     ToStringHelper addToStringAttributes(final ToStringHelper helper) {
113         return super.addToStringAttributes(helper.add("validators", validators));
114     }
115
116     private static ImmutableList<NodeIdentifier> toDescendantPath(final ListSchemaNode parent,
117             final Descendant descendant) {
118         final List<QName> qnames = descendant.getNodeIdentifiers();
119         final ImmutableList.Builder<NodeIdentifier> builder = ImmutableList.builderWithExpectedSize(qnames.size());
120         final Iterator<QName> it = descendant.getNodeIdentifiers().iterator();
121         DataNodeContainer current = parent;
122         while (true) {
123             final QName qname = it.next();
124             final DataSchemaNode next = current.findDataChildByName(qname)
125                 .orElseThrow(() -> new IllegalStateException("Cannot find component " + qname + " of " + descendant));
126             builder.add(NodeIdentifier.create(qname));
127             if (!it.hasNext()) {
128                 checkState(next instanceof TypedDataSchemaNode, "Unexpected schema %s for %s", next, descendant);
129                 final ImmutableList<NodeIdentifier> ret = builder.build();
130                 LOG.trace("Resolved {} to {}", descendant, ret);
131                 return ret;
132             }
133
134             checkState(next instanceof DataNodeContainer, "Unexpected non-container %s for %s", next, descendant);
135             current = (DataNodeContainer) next;
136         }
137     }
138
139     @FunctionalInterface
140     @NonNullByDefault
141     interface ExceptionSupplier<T extends Exception> {
142         T get(String message, Map<Descendant, @Nullable Object> values);
143     }
144 }