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.Optional;
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;
36 final class LeafrefXPathStringParsingPathArgumentBuilder implements Builder<List<PathArgument>> {
38 private static final String UP_ONE_LEVEL = "..";
39 private static final String CURRENT_FUNCTION_INVOCATION_STR = "current()";
42 private static final String NODE_IDENTIFIER_STR = "([A-Za-z_][A-Za-z0-9_\\.-]*:)?([A-Za-z_][A-Za-z0-9_\\.-]*)";
45 * Pattern matching node-identifier YANG ABNF token.
47 private static final Pattern NODE_IDENTIFIER_PATTERN = Pattern.compile(NODE_IDENTIFIER_STR);
50 * Matcher matching WSP YANG ABNF token.
52 private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
55 * Matcher matching IDENTIFIER first char token.
57 private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z')
58 .or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed();
60 * Matcher matching IDENTIFIER token.
62 private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
63 .or(CharMatcher.anyOf(".-")).precomputed();
65 private static final Splitter SLASH_SPLITTER = Splitter.on('/');
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 = ']';
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<>();
79 private int offset = 0;
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;
90 public List<PathArgument> build() {
91 while (!allCharactersConsumed()) {
92 product.add(computeNextArgument());
94 return ImmutableList.copyOf(product);
97 private PathArgument computeNextArgument() {
98 checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
100 checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
102 final QName name = nextQName();
103 if (allCharactersConsumed() || SLASH == currentChar()) {
104 return new NodeIdentifier(name);
107 checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
108 return computeIdentifierWithPredicate(name);
111 private PathArgument computeIdentifierWithPredicate(final QName name) {
112 product.add(new NodeIdentifier(name));
114 ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
115 while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
118 final QName key = nextQName();
121 checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
123 final Object keyValue = nextCurrentFunctionPathValue();
125 checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
127 keyValues.put(key, keyValue);
129 return new NodeIdentifierWithPredicates(name, keyValues.build());
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());
137 offset += CURRENT_FUNCTION_INVOCATION_STR.length();
139 checkCurrentAndSkip(SLASH, "Expression 'current()' must be followed by slash.");
142 final List<String> pathComponents = SLASH_SPLITTER.trimResults().omitEmptyStrings()
143 .splitToList(relPathKeyExpression);
144 checkValid(!pathComponents.isEmpty(), "Malformed path key expression: '%s'.", pathKeyExpression);
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;
164 throw new IllegalArgumentException(String.format(
165 "Could not parse leafref path '%s'. Offset: %s : Reason: Malformed path component: '%s'.",
166 xpathString, offset, pathComponent));
170 if (currentNode != null && currentNode instanceof LeafNode) {
171 return currentNode.getValue();
174 throw new IllegalArgumentException("Could not resolve current function path value.");
179 * Returns following QName and sets offset to end of QName.
181 * @return following QName.
183 private QName nextQName() {
184 // Consume prefix or identifier
185 final String maybePrefix = nextIdentifier();
187 final String localName;
188 if (!allCharactersConsumed() && COLON == currentChar()) {
189 // previous token is prefix;
190 prefix = maybePrefix;
192 localName = nextIdentifier();
195 localName = maybePrefix;
197 return createQName(prefix, localName);
201 * Returns true if all characters from input string were consumed.
203 * @return true if all characters from input string were consumed.
205 private boolean allCharactersConsumed() {
206 return offset == xpathString.length();
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);
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);
224 throw new IllegalArgumentException(String.format("Failed to lookup a module for prefix %s", prefix));
228 * Skips current char if it equals expected otherwise fails parsing.
230 * @param expected Expected character
231 * @param errorMsg Error message if {@link #currentChar()} does not match expected.
233 private void checkCurrentAndSkip(final char expected, final String errorMsg) {
234 checkValid(expected == currentChar(), errorMsg);
239 * Fails parsing if condition is not met.
242 * In case of error provides pointer to failed leafref, offset on which failure occured with explanation.
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
248 private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
250 throw new IllegalArgumentException(String.format(
251 "Could not parse leafref path '%s'. Offset: %s : Reason: %s", xpathString, offset,
252 String.format(errorMsg, attributes)));
257 * Returns character at current offset.
259 * @return character at current offset.
261 private char currentChar() {
262 return xpathString.charAt(offset);
266 * Increments processing offset by 1.
268 private void skipCurrentChar() {
273 * Skip whitespace characters, sets offset to first following non-whitespace character.
275 private void skipWhitespaces() {
276 nextSequenceEnd(WSP);
280 * Returns a string which matches IDENTIFIER YANG ABNF token
281 * and sets processing offset after the end of identifier.
283 * @return string which matches IDENTIFIER YANG ABNF token
285 private String nextIdentifier() {
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);
293 private void nextSequenceEnd(final CharMatcher matcher) {
294 while (!allCharactersConsumed() && matcher.matches(xpathString.charAt(offset))) {