2 * Copyright (c) 2017 Cisco Systems, Inc. and others. All rights reserved.
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
9 package org.opendaylight.yangtools.yang.data.jaxen;
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;
35 final class LeafrefXPathStringParsingPathArgumentBuilder implements Builder<List<PathArgument>> {
37 private static final String UP_ONE_LEVEL = "..";
38 private static final String CURRENT_FUNCTION_INVOCATION_STR = "current()";
41 private static final String NODE_IDENTIFIER_STR = "([A-Za-z_][A-Za-z0-9_\\.-]*:)?([A-Za-z_][A-Za-z0-9_\\.-]*)";
44 * Pattern matching node-identifier YANG ABNF token.
46 private static final Pattern NODE_IDENTIFIER_PATTERN = Pattern.compile(NODE_IDENTIFIER_STR);
49 * Matcher matching WSP YANG ABNF token.
51 private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
54 * Matcher matching IDENTIFIER first char token.
56 private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z')
57 .or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed();
59 * Matcher matching IDENTIFIER token.
61 private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
62 .or(CharMatcher.anyOf(".-")).precomputed();
64 private static final Splitter SLASH_SPLITTER = Splitter.on('/');
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 = ']';
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<>();
78 private int offset = 0;
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;
89 public List<PathArgument> build() {
90 while (!allCharactersConsumed()) {
91 product.add(computeNextArgument());
93 return ImmutableList.copyOf(product);
96 private PathArgument computeNextArgument() {
97 checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
99 checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
101 final QName name = nextQName();
102 if (allCharactersConsumed() || SLASH == currentChar()) {
103 return new NodeIdentifier(name);
106 checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
107 return computeIdentifierWithPredicate(name);
110 private PathArgument computeIdentifierWithPredicate(final QName name) {
111 product.add(new NodeIdentifier(name));
113 ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
114 while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
117 final QName key = nextQName();
120 checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
122 final Object keyValue = nextCurrentFunctionPathValue();
124 checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
126 keyValues.put(key, keyValue);
128 return NodeIdentifierWithPredicates.of(name, keyValues.build());
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());
136 offset += CURRENT_FUNCTION_INVOCATION_STR.length();
138 checkCurrentAndSkip(SLASH, "Expression 'current()' must be followed by slash.");
141 final List<String> pathComponents = SLASH_SPLITTER.trimResults().omitEmptyStrings()
142 .splitToList(relPathKeyExpression);
143 checkValid(!pathComponents.isEmpty(), "Malformed path key expression: '%s'.", pathKeyExpression);
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()));
161 throw new IllegalArgumentException(String.format(
162 "Could not parse leafref path '%s'. Offset: %s : Reason: Malformed path component: '%s'.",
163 xpathString, offset, pathComponent));
167 if (node != null && node instanceof LeafNode) {
171 throw new IllegalArgumentException("Could not resolve current function path value.");
176 * Returns following QName and sets offset to end of QName.
178 * @return following QName.
180 private QName nextQName() {
181 // Consume prefix or identifier
182 final String maybePrefix = nextIdentifier();
184 final String localName;
185 if (!allCharactersConsumed() && COLON == currentChar()) {
186 // previous token is prefix
187 prefix = maybePrefix;
189 localName = nextIdentifier();
192 localName = maybePrefix;
194 return createQName(prefix, localName);
198 * Returns true if all characters from input string were consumed.
200 * @return true if all characters from input string were consumed.
202 private boolean allCharactersConsumed() {
203 return offset == xpathString.length();
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);
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);
220 throw new IllegalArgumentException(String.format("Failed to lookup a module for prefix %s", prefix));
224 * Skips current char if it equals expected otherwise fails parsing.
226 * @param expected Expected character
227 * @param errorMsg Error message if {@link #currentChar()} does not match expected.
229 private void checkCurrentAndSkip(final char expected, final String errorMsg) {
230 checkValid(expected == currentChar(), errorMsg);
235 * Fails parsing if condition is not met.
238 * In case of error provides pointer to failed leafref, offset on which failure occured with explanation.
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
244 private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
246 throw new IllegalArgumentException(String.format(
247 "Could not parse leafref path '%s'. Offset: %s : Reason: %s", xpathString, offset,
248 String.format(errorMsg, attributes)));
253 * Returns character at current offset.
255 * @return character at current offset.
257 private char currentChar() {
258 return xpathString.charAt(offset);
262 * Increments processing offset by 1.
264 private void skipCurrentChar() {
269 * Skip whitespace characters, sets offset to first following non-whitespace character.
271 private void skipWhitespaces() {
272 nextSequenceEnd(WSP);
276 * Returns a string which matches IDENTIFIER YANG ABNF token
277 * and sets processing offset after the end of identifier.
279 * @return string which matches IDENTIFIER YANG ABNF token
281 private String nextIdentifier() {
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);
289 private void nextSequenceEnd(final CharMatcher matcher) {
290 while (!allCharactersConsumed() && matcher.matches(xpathString.charAt(offset))) {