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