Cleanup more tests
[yangtools.git] / attic / yang-data-jaxen / src / main / java / org / opendaylight / yangtools / yang / data / jaxen / LeafrefXPathStringParsingPathArgumentBuilder.java
1 /*
2  * Copyright (c) 2017 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
9 package org.opendaylight.yangtools.yang.data.jaxen;
10
11 import com.google.common.annotations.Beta;
12 import com.google.common.base.CharMatcher;
13 import com.google.common.base.Splitter;
14 import com.google.common.collect.ImmutableList;
15 import com.google.common.collect.ImmutableMap;
16 import java.util.ArrayList;
17 import java.util.List;
18 import java.util.regex.Matcher;
19 import java.util.regex.Pattern;
20 import org.checkerframework.checker.regex.qual.Regex;
21 import org.opendaylight.yangtools.concepts.Builder;
22 import org.opendaylight.yangtools.yang.common.QName;
23 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
24 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
25 import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
26 import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
27 import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
28 import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
29 import org.opendaylight.yangtools.yang.model.api.Module;
30 import org.opendaylight.yangtools.yang.model.api.ModuleImport;
31 import org.opendaylight.yangtools.yang.model.api.SchemaContext;
32 import org.opendaylight.yangtools.yang.model.api.TypedDataSchemaNode;
33
34 @Beta
35 final class LeafrefXPathStringParsingPathArgumentBuilder implements Builder<List<PathArgument>> {
36
37     private static final String UP_ONE_LEVEL = "..";
38     private static final String CURRENT_FUNCTION_INVOCATION_STR = "current()";
39
40     @Regex
41     private static final String NODE_IDENTIFIER_STR = "([A-Za-z_][A-Za-z0-9_\\.-]*:)?([A-Za-z_][A-Za-z0-9_\\.-]*)";
42
43     /**
44      * Pattern matching node-identifier YANG ABNF token.
45      */
46     private static final Pattern NODE_IDENTIFIER_PATTERN = Pattern.compile(NODE_IDENTIFIER_STR);
47
48     /**
49      * Matcher matching WSP YANG ABNF token.
50      */
51     private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
52
53     /**
54      * Matcher matching IDENTIFIER first char token.
55      */
56     private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z')
57             .or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed();
58     /**
59      * Matcher matching IDENTIFIER token.
60      */
61     private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
62             .or(CharMatcher.anyOf(".-")).precomputed();
63
64     private static final Splitter SLASH_SPLITTER = Splitter.on('/');
65
66     private static final char SLASH = '/';
67     private static final char COLON = ':';
68     private static final char EQUALS = '=';
69     private static final char PRECONDITION_START = '[';
70     private static final char PRECONDITION_END = ']';
71
72     private final String xpathString;
73     private final SchemaContext schemaContext;
74     private final TypedDataSchemaNode schemaNode;
75     private final NormalizedNodeContext currentNodeCtx;
76     private final List<PathArgument> product = new ArrayList<>();
77
78     private int offset = 0;
79
80     LeafrefXPathStringParsingPathArgumentBuilder(final String xpathString, final SchemaContext schemaContext,
81             final TypedDataSchemaNode schemaNode, final NormalizedNodeContext currentNodeCtx) {
82         this.xpathString = xpathString;
83         this.schemaContext = schemaContext;
84         this.schemaNode = schemaNode;
85         this.currentNodeCtx = currentNodeCtx;
86     }
87
88     @Override
89     public List<PathArgument> build() {
90         while (!allCharactersConsumed()) {
91             product.add(computeNextArgument());
92         }
93         return ImmutableList.copyOf(product);
94     }
95
96     private PathArgument computeNextArgument() {
97         checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
98         skipCurrentChar();
99         checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
100
101         final QName name = nextQName();
102         if (allCharactersConsumed() || SLASH == currentChar()) {
103             return new NodeIdentifier(name);
104         }
105
106         checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
107         return computeIdentifierWithPredicate(name);
108     }
109
110     private PathArgument computeIdentifierWithPredicate(final QName name) {
111         product.add(new NodeIdentifier(name));
112
113         ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
114         while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
115             skipCurrentChar();
116             skipWhitespaces();
117             final QName key = nextQName();
118
119             skipWhitespaces();
120             checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
121             skipWhitespaces();
122             final Object keyValue = nextCurrentFunctionPathValue();
123             skipWhitespaces();
124             checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
125
126             keyValues.put(key, keyValue);
127         }
128         return NodeIdentifierWithPredicates.of(name, keyValues.build());
129     }
130
131     private Object nextCurrentFunctionPathValue() {
132         final String xPathSubStr = xpathString.substring(offset);
133         final String pathKeyExpression = xPathSubStr.substring(0, xPathSubStr.indexOf(PRECONDITION_END));
134         final String relPathKeyExpression = pathKeyExpression.substring(CURRENT_FUNCTION_INVOCATION_STR.length());
135
136         offset += CURRENT_FUNCTION_INVOCATION_STR.length();
137         skipWhitespaces();
138         checkCurrentAndSkip(SLASH, "Expression 'current()' must be followed by slash.");
139         skipWhitespaces();
140
141         final List<String> pathComponents = SLASH_SPLITTER.trimResults().omitEmptyStrings()
142                 .splitToList(relPathKeyExpression);
143         checkValid(!pathComponents.isEmpty(), "Malformed path key expression: '%s'.", pathKeyExpression);
144
145         boolean inNodeIdentifierPart = false;
146         NormalizedNodeContext nodeCtx = this.currentNodeCtx;
147         NormalizedNode node = null;
148         for (String pathComponent : pathComponents) {
149             final Matcher matcher = NODE_IDENTIFIER_PATTERN.matcher(pathComponent);
150             if (UP_ONE_LEVEL.equals(pathComponent)) {
151                 checkValid(!inNodeIdentifierPart, "Up-one-level expression cannot follow concrete path component.");
152                 nodeCtx = nodeCtx.getParent();
153                 node = nodeCtx.getNode();
154                 offset += UP_ONE_LEVEL.length() + 1;
155             } else if (matcher.matches()) {
156                 inNodeIdentifierPart = true;
157                 if (node != null && node instanceof DataContainerNode) {
158                     node = ((DataContainerNode<?>) node).childByArg(new NodeIdentifier(nextQName()));
159                 }
160             } else {
161                 throw new IllegalArgumentException(String.format(
162                         "Could not parse leafref path '%s'. Offset: %s : Reason: Malformed path component: '%s'.",
163                         xpathString, offset, pathComponent));
164             }
165         }
166
167         if (node != null && node instanceof LeafNode) {
168             return node.body();
169         }
170
171         throw new IllegalArgumentException("Could not resolve current function path value.");
172
173     }
174
175     /**
176      * Returns following QName and sets offset to end of QName.
177      *
178      * @return following QName.
179      */
180     private QName nextQName() {
181         // Consume prefix or identifier
182         final String maybePrefix = nextIdentifier();
183         final String prefix;
184         final String localName;
185         if (!allCharactersConsumed() && COLON == currentChar()) {
186             // previous token is prefix
187             prefix = maybePrefix;
188             skipCurrentChar();
189             localName = nextIdentifier();
190         } else {
191             prefix = "";
192             localName = maybePrefix;
193         }
194         return createQName(prefix, localName);
195     }
196
197     /**
198      * Returns true if all characters from input string were consumed.
199      *
200      * @return true if all characters from input string were consumed.
201      */
202     private boolean allCharactersConsumed() {
203         return offset == xpathString.length();
204     }
205
206     private QName createQName(final String prefix, final String localName) {
207         final Module module = schemaContext.findModule(schemaNode.getQName().getModule()).get();
208         if (prefix.isEmpty() || module.getPrefix().equals(prefix)) {
209             return QName.create(module.getQNameModule(), localName);
210         }
211
212         for (final ModuleImport moduleImport : module.getImports()) {
213             if (prefix.equals(moduleImport.getPrefix())) {
214                 final Module importedModule = schemaContext.findModule(moduleImport.getModuleName(),
215                         moduleImport.getRevision()).get();
216                 return QName.create(importedModule.getQNameModule(),localName);
217             }
218         }
219
220         throw new IllegalArgumentException(String.format("Failed to lookup a module for prefix %s", prefix));
221     }
222
223     /**
224      * Skips current char if it equals expected otherwise fails parsing.
225      *
226      * @param expected Expected character
227      * @param errorMsg Error message if {@link #currentChar()} does not match expected.
228      */
229     private void checkCurrentAndSkip(final char expected, final String errorMsg) {
230         checkValid(expected == currentChar(), errorMsg);
231         offset++;
232     }
233
234     /**
235      * Fails parsing if condition is not met.
236      *
237      * <p>
238      * In case of error provides pointer to failed leafref, offset on which failure occured with explanation.
239      *
240      * @param condition Fails parsing if {@code condition} is false
241      * @param errorMsg Error message which will be provided to user.
242      * @param attributes Message attributes
243      */
244     private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
245         if (!condition) {
246             throw new IllegalArgumentException(String.format(
247                     "Could not parse leafref path '%s'. Offset: %s : Reason: %s", xpathString, offset,
248                     String.format(errorMsg, attributes)));
249         }
250     }
251
252     /**
253      * Returns character at current offset.
254      *
255      * @return character at current offset.
256      */
257     private char currentChar() {
258         return xpathString.charAt(offset);
259     }
260
261     /**
262      * Increments processing offset by 1.
263      */
264     private void skipCurrentChar() {
265         offset++;
266     }
267
268     /**
269      * Skip whitespace characters, sets offset to first following non-whitespace character.
270      */
271     private void skipWhitespaces() {
272         nextSequenceEnd(WSP);
273     }
274
275     /**
276      * Returns a string which matches IDENTIFIER YANG ABNF token
277      * and sets processing offset after the end of identifier.
278      *
279      * @return string which matches IDENTIFIER YANG ABNF token
280      */
281     private String nextIdentifier() {
282         int start = offset;
283         checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()),
284                 "Identifier must start with character from set 'a-zA-Z_'");
285         nextSequenceEnd(IDENTIFIER);
286         return xpathString.substring(start, offset);
287     }
288
289     private void nextSequenceEnd(final CharMatcher matcher) {
290         while (!allCharactersConsumed() && matcher.matches(xpathString.charAt(offset))) {
291             offset++;
292         }
293     }
294 }