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.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;
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
53 private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
56 * Matcher matching IDENTIFIER first char token.
59 private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z')
60 .or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed();
63 * Matcher matching IDENTIFIER token
66 private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
67 .or(CharMatcher.anyOf(".-")).precomputed();
69 private static final Splitter SLASH_SPLITTER = Splitter.on('/');
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 = ']';
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<>();
83 private int offset = 0;
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;
94 public List<PathArgument> build() {
95 while (!allCharactersConsumed()) {
96 product.add(computeNextArgument());
98 return ImmutableList.copyOf(product);
101 private PathArgument computeNextArgument() {
102 checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
104 checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
106 final QName name = nextQName();
107 if (allCharactersConsumed() || SLASH == currentChar()) {
108 return new NodeIdentifier(name);
110 checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
111 return computeIdentifierWithPredicate(name);
115 private PathArgument computeIdentifierWithPredicate(final QName name) {
116 product.add(new NodeIdentifier(name));
118 ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
119 while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
122 final QName key = nextQName();
125 checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
127 final Object keyValue = nextCurrentFunctionPathValue();
129 checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
131 keyValues.put(key, keyValue);
133 return new NodeIdentifierWithPredicates(name, keyValues.build());
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());
141 offset += CURRENT_FUNCTION_INVOCATION_STR.length();
143 checkCurrentAndSkip(SLASH, "Expression 'current()' must be followed by slash.");
146 final List<String> pathComponents = SLASH_SPLITTER.trimResults().omitEmptyStrings()
147 .splitToList(relPathKeyExpression);
148 checkValid(!pathComponents.isEmpty(), "Malformed path key expression: '%s'.", pathKeyExpression);
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;
168 throw new IllegalArgumentException(String.format(
169 "Could not parse leafref path '%s'. Offset: %s : Reason: Malformed path component: '%s'.",
170 xPathString, offset, pathComponent));
174 if (currentNode != null && currentNode instanceof LeafNode) {
175 return currentNode.getValue();
178 throw new IllegalArgumentException("Could not resolve current function path value.");
184 * Returns following QName and sets offset to end of QName.
186 * @return following QName.
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;
196 localName = nextIdentifier();
199 localName = maybePrefix;
201 return createQName(prefix, localName);
205 * Returns true if all characters from input string
208 * @return true if all characters from input string
211 private boolean allCharactersConsumed() {
212 return offset == xPathString.length();
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);
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);
230 throw new IllegalArgumentException(String.format("Failed to lookup a module for prefix %s", prefix));
235 * Skips current char if it equals expected otherwise fails parsing.
237 * @param expected Expected character
238 * @param errorMsg Error message if {@link #currentChar()} does not match expected.
240 private void checkCurrentAndSkip(final char expected, final String errorMsg) {
241 checkValid(expected == currentChar(), errorMsg);
247 * Fails parsing if condition is not met.
249 * In case of error provides pointer to failed leafref,
250 * offset on which failure occured with explanation.
252 * @param condition Fails parsing if {@code condition} is false
253 * @param errorMsg Error message which will be provided to user.
256 private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
258 throw new IllegalArgumentException(String.format(
259 "Could not parse leafref path '%s'. Offset: %s : Reason: %s", xPathString, offset,
260 String.format(errorMsg, attributes)));
265 * Returns character at current offset.
267 * @return character at current offset.
269 private char currentChar() {
270 return xPathString.charAt(offset);
274 * Increases processing offset by 1
276 private void skipCurrentChar() {
281 * Skip whitespace characters, sets offset to first following
282 * non-whitespace character.
284 private void skipWhitespaces() {
285 nextSequenceEnd(WSP);
289 * Returns a string which matches IDENTIFIER YANG ABNF token
290 * and sets processing offset after the end of identifier.
292 * @return string which matches IDENTIFIER YANG ABNF token
294 private String nextIdentifier() {
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);
301 private void nextSequenceEnd(final CharMatcher matcher) {
302 while (!allCharactersConsumed() && matcher.matches(xPathString.charAt(offset))) {