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