185a626dac15517466e6a4a76011253929a037ef
[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.Optional;
19 import java.util.regex.Matcher;
20 import java.util.regex.Pattern;
21 import org.checkerframework.checker.regex.qual.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.TypedDataSchemaNode;
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 TypedDataSchemaNode 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 TypedDataSchemaNode 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 NodeIdentifierWithPredicates.of(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 nodeCtx = this.currentNodeCtx;
148         NormalizedNode<?, ?> node = 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                 nodeCtx = nodeCtx.getParent();
154                 node = nodeCtx.getNode();
155                 offset += UP_ONE_LEVEL.length() + 1;
156             } else if (matcher.matches()) {
157                 inNodeIdentifierPart = true;
158                 if (node != null && node instanceof DataContainerNode) {
159                     final DataContainerNode dcn = (DataContainerNode) node;
160                     final Optional<NormalizedNode<?, ?>> possibleChild = dcn.getChild(new NodeIdentifier(nextQName()));
161                     node = 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 (node != null && node instanceof LeafNode) {
171             return node.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.findModule(schemaNode.getQName().getModule()).get();
211         if (prefix.isEmpty() || module.getPrefix().equals(prefix)) {
212             return QName.create(module.getQNameModule(), localName);
213         }
214
215         for (final ModuleImport moduleImport : module.getImports()) {
216             if (prefix.equals(moduleImport.getPrefix())) {
217                 final Module importedModule = schemaContext.findModule(moduleImport.getModuleName(),
218                         moduleImport.getRevision()).get();
219                 return QName.create(importedModule.getQNameModule(),localName);
220             }
221         }
222
223         throw new IllegalArgumentException(String.format("Failed to lookup a module for prefix %s", prefix));
224     }
225
226     /**
227      * Skips current char if it equals expected otherwise fails parsing.
228      *
229      * @param expected Expected character
230      * @param errorMsg Error message if {@link #currentChar()} does not match expected.
231      */
232     private void checkCurrentAndSkip(final char expected, final String errorMsg) {
233         checkValid(expected == currentChar(), errorMsg);
234         offset++;
235     }
236
237     /**
238      * Fails parsing if condition is not met.
239      *
240      * <p>
241      * In case of error provides pointer to failed leafref, offset on which failure occured with explanation.
242      *
243      * @param condition Fails parsing if {@code condition} is false
244      * @param errorMsg Error message which will be provided to user.
245      * @param attributes Message attributes
246      */
247     private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
248         if (!condition) {
249             throw new IllegalArgumentException(String.format(
250                     "Could not parse leafref path '%s'. Offset: %s : Reason: %s", xpathString, offset,
251                     String.format(errorMsg, attributes)));
252         }
253     }
254
255     /**
256      * Returns character at current offset.
257      *
258      * @return character at current offset.
259      */
260     private char currentChar() {
261         return xpathString.charAt(offset);
262     }
263
264     /**
265      * Increments processing offset by 1.
266      */
267     private void skipCurrentChar() {
268         offset++;
269     }
270
271     /**
272      * Skip whitespace characters, sets offset to first following non-whitespace character.
273      */
274     private void skipWhitespaces() {
275         nextSequenceEnd(WSP);
276     }
277
278     /**
279      * Returns a string which matches IDENTIFIER YANG ABNF token
280      * and sets processing offset after the end of identifier.
281      *
282      * @return string which matches IDENTIFIER YANG ABNF token
283      */
284     private String nextIdentifier() {
285         int start = offset;
286         checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()),
287                 "Identifier must start with character from set 'a-zA-Z_'");
288         nextSequenceEnd(IDENTIFIER);
289         return xpathString.substring(start, offset);
290     }
291
292     private void nextSequenceEnd(final CharMatcher matcher) {
293         while (!allCharactersConsumed() && matcher.matches(xpathString.charAt(offset))) {
294             offset++;
295         }
296     }
297 }