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