/*
* Copyright (c) 2017 Cisco Systems, Inc. and others. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
package org.opendaylight.yangtools.yang.data.jaxen;
import com.google.common.annotations.Beta;
import com.google.common.base.CharMatcher;
import com.google.common.base.Optional;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.RegEx;
import org.opendaylight.yangtools.concepts.Builder;
import org.opendaylight.yangtools.yang.common.QName;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifier;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.NodeIdentifierWithPredicates;
import org.opendaylight.yangtools.yang.data.api.YangInstanceIdentifier.PathArgument;
import org.opendaylight.yangtools.yang.data.api.schema.DataContainerNode;
import org.opendaylight.yangtools.yang.data.api.schema.LeafNode;
import org.opendaylight.yangtools.yang.data.api.schema.NormalizedNode;
import org.opendaylight.yangtools.yang.model.api.Module;
import org.opendaylight.yangtools.yang.model.api.ModuleImport;
import org.opendaylight.yangtools.yang.model.api.SchemaContext;
import org.opendaylight.yangtools.yang.model.api.TypedSchemaNode;
@Beta
final class LeafrefXPathStringParsingPathArgumentBuilder implements Builder<List<PathArgument>> {
private static final String UP_ONE_LEVEL = "..";
private static final String CURRENT_FUNCTION_INVOCATION_STR = "current()";
@RegEx
private static final String NODE_IDENTIFIER_STR = "([A-Za-z_][A-Za-z0-9_\\.-]*:)?([A-Za-z_][A-Za-z0-9_\\.-]*)";
/**
* Pattern matching node-identifier YANG ABNF token
*/
private static final Pattern NODE_IDENTIFIER_PATTERN = Pattern.compile(NODE_IDENTIFIER_STR);
/**
* Matcher matching WSP YANG ABNF token
*
*/
private static final CharMatcher WSP = CharMatcher.anyOf(" \t");
/**
* Matcher matching IDENTIFIER first char token.
*
*/
private static final CharMatcher IDENTIFIER_FIRST_CHAR = CharMatcher.inRange('a', 'z')
.or(CharMatcher.inRange('A', 'Z')).or(CharMatcher.is('_')).precomputed();
/**
*
* Matcher matching IDENTIFIER token
*
*/
private static final CharMatcher IDENTIFIER = IDENTIFIER_FIRST_CHAR.or(CharMatcher.inRange('0', '9'))
.or(CharMatcher.anyOf(".-")).precomputed();
private static final Splitter SLASH_SPLITTER = Splitter.on('/');
private static final char SLASH = '/';
private static final char COLON = ':';
private static final char EQUALS = '=';
private static final char PRECONDITION_START = '[';
private static final char PRECONDITION_END = ']';
private final String xPathString;
private final SchemaContext schemaContext;
private final TypedSchemaNode schemaNode;
private final NormalizedNodeContext currentNodeCtx;
private final List<PathArgument> product = new ArrayList<>();
private int offset = 0;
LeafrefXPathStringParsingPathArgumentBuilder(final String xPathString, final SchemaContext schemaContext,
final TypedSchemaNode schemaNode, final NormalizedNodeContext currentNodeCtx) {
this.xPathString = xPathString;
this.schemaContext = schemaContext;
this.schemaNode = schemaNode;
this.currentNodeCtx = currentNodeCtx;
}
@Override
public List<PathArgument> build() {
while (!allCharactersConsumed()) {
product.add(computeNextArgument());
}
return ImmutableList.copyOf(product);
}
private PathArgument computeNextArgument() {
checkValid(SLASH == currentChar(), "Identifier must start with '/'.");
skipCurrentChar();
checkValid(!allCharactersConsumed(), "Identifier cannot end with '/'.");
final QName name = nextQName();
if (allCharactersConsumed() || SLASH == currentChar()) {
return new NodeIdentifier(name);
} else {
checkValid(PRECONDITION_START == currentChar(), "Last element must be identifier, predicate or '/'");
return computeIdentifierWithPredicate(name);
}
}
private PathArgument computeIdentifierWithPredicate(final QName name) {
product.add(new NodeIdentifier(name));
ImmutableMap.Builder<QName, Object> keyValues = ImmutableMap.builder();
while (!allCharactersConsumed() && PRECONDITION_START == currentChar()) {
skipCurrentChar();
skipWhitespaces();
final QName key = nextQName();
skipWhitespaces();
checkCurrentAndSkip(EQUALS, "Precondition must contain '='");
skipWhitespaces();
final Object keyValue = nextCurrentFunctionPathValue();
skipWhitespaces();
checkCurrentAndSkip(PRECONDITION_END, "Precondition must ends with ']'");
keyValues.put(key, keyValue);
}
return new NodeIdentifierWithPredicates(name, keyValues.build());
}
private Object nextCurrentFunctionPathValue() {
final String xPathSubStr = xPathString.substring(offset);
final String pathKeyExpression = xPathSubStr.substring(0, xPathSubStr.indexOf(PRECONDITION_END));
final String relPathKeyExpression = pathKeyExpression.substring(CURRENT_FUNCTION_INVOCATION_STR.length());
offset += CURRENT_FUNCTION_INVOCATION_STR.length();
skipWhitespaces();
checkCurrentAndSkip(SLASH, "Expression 'current()' must be followed by slash.");
skipWhitespaces();
final List<String> pathComponents = SLASH_SPLITTER.trimResults().omitEmptyStrings()
.splitToList(relPathKeyExpression);
checkValid(!pathComponents.isEmpty(), "Malformed path key expression: '%s'.", pathKeyExpression);
boolean inNodeIdentifierPart = false;
NormalizedNodeContext currentNodeCtx = this.currentNodeCtx;
NormalizedNode<?, ?> currentNode = null;
for (String pathComponent : pathComponents) {
final Matcher matcher = NODE_IDENTIFIER_PATTERN.matcher(pathComponent);
if (UP_ONE_LEVEL.equals(pathComponent)) {
checkValid(!inNodeIdentifierPart, "Up-one-level expression cannot follow concrete path component.");
currentNodeCtx = currentNodeCtx.getParent();
currentNode = currentNodeCtx.getNode();
offset += UP_ONE_LEVEL.length() + 1;
} else if (matcher.matches()) {
inNodeIdentifierPart = true;
if (currentNode != null && currentNode instanceof DataContainerNode) {
final DataContainerNode dcn = (DataContainerNode) currentNode;
final Optional<NormalizedNode<?, ?>> possibleChild = dcn.getChild(new NodeIdentifier(nextQName()));
currentNode = possibleChild.isPresent() ? possibleChild.get() : null;
}
} else {
throw new IllegalArgumentException(String.format(
"Could not parse leafref path '%s'. Offset: %s : Reason: Malformed path component: '%s'.",
xPathString, offset, pathComponent));
}
}
if (currentNode != null && currentNode instanceof LeafNode) {
return currentNode.getValue();
}
throw new IllegalArgumentException("Could not resolve current function path value.");
}
/**
*
* Returns following QName and sets offset to end of QName.
*
* @return following QName.
*/
private QName nextQName() {
// Consume prefix or identifier
final String maybePrefix = nextIdentifier();
final String prefix, localName;
if (!allCharactersConsumed() && COLON == currentChar()) {
// previous token is prefix;
prefix = maybePrefix;
skipCurrentChar();
localName = nextIdentifier();
} else {
prefix = "";
localName = maybePrefix;
}
return createQName(prefix, localName);
}
/**
* Returns true if all characters from input string
* were consumed.
*
* @return true if all characters from input string
* were consumed.
*/
private boolean allCharactersConsumed() {
return offset == xPathString.length();
}
private QName createQName(final String prefix, final String localName) {
final Module module = schemaContext.findModuleByNamespaceAndRevision(schemaNode.getQName().getNamespace(),
schemaNode.getQName().getRevision());
if (prefix.isEmpty() || module.getPrefix().equals(prefix)) {
return QName.create(module.getQNameModule(), localName);
}
for (final ModuleImport moduleImport : module.getImports()) {
if (prefix.equals(moduleImport.getPrefix())) {
final Module importedModule = schemaContext.findModuleByName(moduleImport.getModuleName(),
moduleImport.getRevision());
return QName.create(importedModule.getQNameModule(),localName);
}
}
throw new IllegalArgumentException(String.format("Failed to lookup a module for prefix %s", prefix));
}
/**
*
* Skips current char if it equals expected otherwise fails parsing.
*
* @param expected Expected character
* @param errorMsg Error message if {@link #currentChar()} does not match expected.
*/
private void checkCurrentAndSkip(final char expected, final String errorMsg) {
checkValid(expected == currentChar(), errorMsg);
offset++;
}
/**
*
* Fails parsing if condition is not met.
*
* In case of error provides pointer to failed leafref,
* offset on which failure occured with explanation.
*
* @param condition Fails parsing if {@code condition} is false
* @param errorMsg Error message which will be provided to user.
* @param attributes
*/
private void checkValid(final boolean condition, final String errorMsg, final Object... attributes) {
if (!condition) {
throw new IllegalArgumentException(String.format(
"Could not parse leafref path '%s'. Offset: %s : Reason: %s", xPathString, offset,
String.format(errorMsg, attributes)));
}
}
/**
* Returns character at current offset.
*
* @return character at current offset.
*/
private char currentChar() {
return xPathString.charAt(offset);
}
/**
* Increases processing offset by 1
*/
private void skipCurrentChar() {
offset++;
}
/**
* Skip whitespace characters, sets offset to first following
* non-whitespace character.
*/
private void skipWhitespaces() {
nextSequenceEnd(WSP);
}
/**
* Returns a string which matches IDENTIFIER YANG ABNF token
* and sets processing offset after the end of identifier.
*
* @return string which matches IDENTIFIER YANG ABNF token
*/
private String nextIdentifier() {
int start = offset;
checkValid(IDENTIFIER_FIRST_CHAR.matches(currentChar()), "Identifier must start with character from set 'a-zA-Z_'");
nextSequenceEnd(IDENTIFIER);
return xPathString.substring(start, offset);
}
private void nextSequenceEnd(final CharMatcher matcher) {
while (!allCharactersConsumed() && matcher.matches(xPathString.charAt(offset))) {
offset++;
}
}
}