Fix incorrect level assignment during fields parsing
[netconf.git] / restconf / restconf-nb / src / main / java / org / opendaylight / restconf / nb / rfc8040 / utils / parser / AbstractFieldsTranslator.java
1 /*
2  * Copyright (c) 2016 Cisco Systems, Inc. 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.restconf.nb.rfc8040.utils.parser;
9
10 import java.util.ArrayList;
11 import java.util.HashSet;
12 import java.util.List;
13 import java.util.Optional;
14 import java.util.Set;
15 import org.eclipse.jdt.annotation.NonNull;
16 import org.opendaylight.restconf.common.context.InstanceIdentifierContext;
17 import org.opendaylight.restconf.common.errors.RestconfDocumentedException;
18 import org.opendaylight.restconf.nb.rfc8040.FieldsParam;
19 import org.opendaylight.restconf.nb.rfc8040.FieldsParam.NodeSelector;
20 import org.opendaylight.yangtools.yang.common.ErrorTag;
21 import org.opendaylight.yangtools.yang.common.ErrorType;
22 import org.opendaylight.yangtools.yang.common.QName;
23 import org.opendaylight.yangtools.yang.common.QNameModule;
24 import org.opendaylight.yangtools.yang.data.util.DataSchemaContextNode;
25 import org.opendaylight.yangtools.yang.model.api.DataSchemaNode;
26 import org.opendaylight.yangtools.yang.model.api.EffectiveModelContext;
27
28 /**
29  * Utilities used for parsing of fields query parameter content.
30  *
31  * @param <T> type of identifier
32  */
33 public abstract class AbstractFieldsTranslator<T> {
34     AbstractFieldsTranslator() {
35         // Hidden on purpose
36     }
37
38     /**
39      * Parse fields parameter and return complete list of child nodes organized into levels.
40      *
41      * @param identifier identifier context created from request URI
42      * @param input input value of fields parameter
43      * @return {@link List} of levels; each level contains {@link Set} of identifiers of type {@link T}
44      */
45     protected final @NonNull List<Set<T>> parseFields(final @NonNull InstanceIdentifierContext identifier,
46                                                       final @NonNull FieldsParam input) {
47         final DataSchemaContextNode<?> startNode = DataSchemaContextNode.fromDataSchemaNode(
48                 (DataSchemaNode) identifier.getSchemaNode());
49
50         if (startNode == null) {
51             throw new RestconfDocumentedException(
52                     "Start node missing in " + input, ErrorType.PROTOCOL, ErrorTag.INVALID_VALUE);
53         }
54
55         final List<Set<T>> parsed = new ArrayList<>();
56         processSelectors(parsed, identifier.getSchemaContext(), identifier.getSchemaNode().getQName().getModule(),
57             startNode, input.nodeSelectors(), 0);
58         return parsed;
59     }
60
61     /**
62      * Add parsed child of current node to result for current level.
63      *
64      * @param currentNode current node
65      * @param childQName parsed identifier of child node
66      * @param level current nodes level
67      * @return {@link DataSchemaContextNode}
68      */
69     protected abstract @NonNull DataSchemaContextNode<?> addChildToResult(@NonNull DataSchemaContextNode<?> currentNode,
70             @NonNull QName childQName, @NonNull Set<T> level);
71
72     private void processSelectors(final List<Set<T>> parsed, final EffectiveModelContext context,
73                                   final QNameModule startNamespace, final DataSchemaContextNode<?> startNode,
74                                   final List<NodeSelector> selectors, final int index) {
75         final Set<T> startLevel;
76         if (parsed.size() <= index) {
77             startLevel = new HashSet<>();
78             parsed.add(startLevel);
79         } else {
80             startLevel = parsed.get(index);
81         }
82         for (var selector : selectors) {
83             var node = startNode;
84             var namespace = startNamespace;
85             var level = startLevel;
86             var levelIndex = index;
87
88             // Note: path is guaranteed to have at least one step
89             final var it = selector.path().iterator();
90             while (true) {
91                 // FIXME: The layout of this loop is rather weird, which is due to how prepareQNameLevel() operates. We
92                 //        need to call it only when we know there is another identifier coming, otherwise we would end
93                 //        up with empty levels sneaking into the mix.
94                 //
95                 //        Dealing with that weirdness requires understanding what the expected end results are and a
96                 //        larger rewrite of the algorithms involved.
97                 final var step = it.next();
98                 final var module = step.module();
99                 if (module != null) {
100                     // FIXME: this is not defensive enough, as we can fail to find the module
101                     namespace = context.findModules(module).iterator().next().getQNameModule();
102                 }
103
104                 // add parsed identifier to results for current level
105                 node = addChildToResult(node, step.identifier().bindTo(namespace), level);
106                 if (!it.hasNext()) {
107                     break;
108                 }
109
110                 // go one level down
111                 level = prepareQNameLevel(parsed, level);
112                 levelIndex++;
113             }
114
115             final var subs = selector.subSelectors();
116             if (!subs.isEmpty()) {
117                 processSelectors(parsed, context, namespace, node, subs, levelIndex + 1);
118             }
119         }
120     }
121
122     /**
123      * Preparation of the identifiers level that is used as storage for parsed identifiers. If the current level exist
124      * at the index that doesn't equal to the last index of already parsed identifiers, a new level of identifiers
125      * is allocated and pushed to input parsed identifiers.
126      *
127      * @param parsedIdentifiers Already parsed list of identifiers grouped to multiple levels.
128      * @param currentLevel Current level of identifiers (set).
129      * @return Existing or new level of identifiers.
130      */
131     private Set<T> prepareQNameLevel(final List<Set<T>> parsedIdentifiers, final Set<T> currentLevel) {
132         final Optional<Set<T>> existingLevel = parsedIdentifiers.stream()
133                 .filter(qNameSet -> qNameSet.equals(currentLevel))
134                 .findAny();
135         if (existingLevel.isPresent()) {
136             final int index = parsedIdentifiers.indexOf(existingLevel.get());
137             if (index == parsedIdentifiers.size() - 1) {
138                 final Set<T> nextLevel = new HashSet<>();
139                 parsedIdentifiers.add(nextLevel);
140                 return nextLevel;
141             }
142
143             return parsedIdentifiers.get(index + 1);
144         }
145
146         final Set<T> nextLevel = new HashSet<>();
147         parsedIdentifiers.add(nextLevel);
148         return nextLevel;
149     }
150 }