YANG 1.1 (RFC7950) introduces a set of yang-specific XPath functions.
YANG statement parser can now parse YANG 1.1 models which contain
these functions
Change-Id: Icfbf95db0d04a3f077133c38341f250cde4e56eb
Signed-off-by: Igor Foltin <ifoltin@cisco.com>
public static final URI RFC7950_YANG_LIBRARY_CAPABILITY =
URI.create("urn:ietf:params:netconf:capability:yang-library:1.0");
+ /**
+ * Prefix for YANG-specific XPath functions
+ */
+ public static final String YANG_XPATH_FUNCTIONS_PREFIX = "yang";
+
private YangConstants() {
throw new UnsupportedOperationException("Utility class");
}
import com.google.common.base.Preconditions;
import com.google.common.base.Verify;
+import com.google.common.collect.BiMap;
+import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.Iterators;
import java.util.Iterator;
import javax.xml.namespace.NamespaceContext;
*/
final class StmtNamespaceContext implements NamespaceContext {
private final StmtContext<?, ?, ?> ctx;
+ private final BiMap<String, String> URIToPrefixMap;
private String localNamespaceURI;
private StmtNamespaceContext(final StmtContext<?, ?, ?> ctx) {
+ this(ctx, ImmutableBiMap.of());
+ }
+
+ private StmtNamespaceContext(final StmtContext<?, ?, ?> ctx, final BiMap<String, String> URIToPrefixMap) {
this.ctx = Preconditions.checkNotNull(ctx);
+ this.URIToPrefixMap = ImmutableBiMap.copyOf(Preconditions.checkNotNull(URIToPrefixMap));
}
public static NamespaceContext create(final StmtContext<?, ?, ?> ctx) {
return new StmtNamespaceContext(ctx);
}
+ public static NamespaceContext create(final StmtContext<?, ?, ?> ctx, final BiMap<String, String> URIToPrefixMap) {
+ return new StmtNamespaceContext(ctx, URIToPrefixMap);
+ }
+
private String localNamespaceURI() {
if (localNamespaceURI == null) {
localNamespaceURI = Verify.verifyNotNull(
// API-mandated by NamespaceContext
Preconditions.checkArgument(prefix != null);
+ final String uri = URIToPrefixMap.inverse().get(prefix);
+ if (uri != null) {
+ return uri;
+ }
+
if (prefix.isEmpty()) {
return localNamespaceURI();
}
// API-mandated by NamespaceContext
Preconditions.checkArgument(namespaceURI != null);
+ final String prefix = URIToPrefixMap.get(namespaceURI);
+ if (prefix != null) {
+ return prefix;
+ }
+
if (localNamespaceURI().equals(namespaceURI)) {
return "";
}
@Override
public Iterator<String> getPrefixes(final String namespaceURI) {
// Ensures underlying map remains constant
- return Iterators.unmodifiableIterator(
- ctx.getAllFromNamespace(URIStringToImpPrefix.class).values().iterator());
+ return Iterators.unmodifiableIterator(Iterators.concat(
+ ctx.getAllFromNamespace(URIStringToImpPrefix.class).values().iterator(),
+ URIToPrefixMap.values().iterator()));
}
}
*/
package org.opendaylight.yangtools.yang.parser.stmt.rfc6020;
+import static org.opendaylight.yangtools.yang.common.YangConstants.RFC6020_YANG_NAMESPACE;
+import static org.opendaylight.yangtools.yang.common.YangConstants.YANG_XPATH_FUNCTIONS_PREFIX;
import static org.opendaylight.yangtools.yang.parser.spi.meta.StmtContextUtils.firstAttributeOf;
import com.google.common.base.CharMatcher;
import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.Nullable;
+import javax.annotation.RegEx;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
private static final Splitter SPACE_SPLITTER = Splitter.on(' ').omitEmptyStrings().trimResults();
private static final Splitter COLON_SPLITTER = Splitter.on(":").omitEmptyStrings().trimResults();
private static final Pattern PATH_ABS = Pattern.compile("/[^/].*");
+ @RegEx
+ private static final String YANG_XPATH_FUNCTIONS_STRING =
+ "(re-match|deref|derived-from(-or-self)?|enum-value|bit-is-set)(\\()";
+ private static final Pattern YANG_XPATH_FUNCTIONS_PATTERN = Pattern.compile(YANG_XPATH_FUNCTIONS_STRING);
private static final Pattern BETWEEN_CURLY_BRACES_PATTERN = Pattern.compile("\\{(.+?)\\}");
private static final Set<String> JAVA_UNICODE_BLOCKS = ImmutableSet.<String>builder()
.add("AegeanNumbers")
static RevisionAwareXPath parseXPath(final StmtContext<?, ?, ?> ctx, final String path) {
final XPath xPath = XPATH_FACTORY.get().newXPath();
- xPath.setNamespaceContext(StmtNamespaceContext.create(ctx));
+ xPath.setNamespaceContext(StmtNamespaceContext.create(ctx,
+ ImmutableBiMap.of(RFC6020_YANG_NAMESPACE.toString(), YANG_XPATH_FUNCTIONS_PREFIX)));
final String trimmed = trimSingleLastSlashFromXPath(path);
try {
+ // XPath extension functions have to be prefixed
+ // yang-specific XPath functions are in fact extended functions, therefore we have to add
+ // "yang" prefix to them so that they can be properly validated with the XPath.compile() method
+ // the "yang" prefix is bound to RFC6020 YANG namespace
+ final String prefixedXPath = addPrefixToYangXPathFunctions(trimmed, ctx);
// TODO: we could capture the result and expose its 'evaluate' method
- xPath.compile(trimmed);
+ xPath.compile(prefixedXPath);
} catch (final XPathExpressionException e) {
LOG.warn("Argument \"{}\" is not valid XPath string at \"{}\"", path, ctx.getStatementSourceReference(), e);
}
return new RevisionAwareXPathImpl(path, PATH_ABS.matcher(path).matches());
}
+ private static String addPrefixToYangXPathFunctions(final String path, final StmtContext<?, ?, ?> ctx) {
+ if (ctx.getRootVersion() == YangVersion.VERSION_1_1) {
+ // FIXME once Java 9 is available, change this to StringBuilder as Matcher.appendReplacement() and
+ // Matcher.appendTail() will accept StringBuilder parameter in Java 9
+ final StringBuffer result = new StringBuffer();
+ final String prefix = YANG_XPATH_FUNCTIONS_PREFIX + ":";
+ final Matcher matcher = YANG_XPATH_FUNCTIONS_PATTERN.matcher(path);
+ while (matcher.find()) {
+ matcher.appendReplacement(result, prefix + matcher.group());
+ }
+
+ matcher.appendTail(result);
+ return result.toString();
+ }
+
+ return path;
+ }
+
public static QName trimPrefix(final QName identifier) {
final String prefixedLocalName = identifier.getLocalName();
final String[] namesParts = prefixedLocalName.split(":");
--- /dev/null
+/*
+ * 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.parser.stmt.rfc7950;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.stmt.StmtTestUtils;
+
+public class Bug6878Test {
+
+ @Test
+ public void testParsingXPathWithYang11Functions() throws Exception {
+ final PrintStream stdout = System.out;
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final String testLog;
+
+ System.setOut(new PrintStream(output, true, "UTF-8"));
+
+ final SchemaContext schemaContext = StmtTestUtils.parseYangSource("/rfc7950/bug6878/foo.yang");
+ assertNotNull(schemaContext);
+
+ testLog = output.toString();
+ assertFalse(testLog.contains("Could not find function: "));
+ System.setOut(stdout);
+ }
+
+ @Test
+ public void shouldLogInvalidYang10XPath() throws Exception {
+ final PrintStream stdout = System.out;
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final String testLog;
+
+ System.setOut(new PrintStream(output, true, "UTF-8"));
+
+ StmtTestUtils.parseYangSource("/rfc7950/bug6878/foo10-invalid.yang");
+
+ testLog = output.toString();
+ assertTrue(testLog.contains("Could not find function: re-match"));
+ System.setOut(stdout);
+ }
+
+ @Test
+ public void shouldLogInvalidYang10XPath2() throws Exception {
+ final PrintStream stdout = System.out;
+ final ByteArrayOutputStream output = new ByteArrayOutputStream();
+ final String testLog;
+
+ System.setOut(new PrintStream(output, true, "UTF-8"));
+
+ StmtTestUtils.parseYangSource("/rfc7950/bug6878/foo10-invalid-2.yang");
+
+ testLog = output.toString();
+ assertTrue(testLog.contains("Could not find function: deref"));
+ System.setOut(stdout);
+ }
+}
--- /dev/null
+module foo {
+ namespace foo;
+ prefix foo;
+ yang-version 1.1;
+
+ revision 2016-02-24;
+
+ identity interface-type;
+
+ identity ethernet {
+ base interface-type;
+ }
+
+ identity fast-ethernet {
+ base ethernet;
+ }
+
+ identity gigabit-ethernet {
+ base ethernet;
+ }
+
+ list interface {
+ key "name";
+ leaf name {
+ type string;
+ }
+
+ leaf type {
+ type identityref {
+ base interface-type;
+ }
+ }
+
+ leaf enabled {
+ type boolean;
+ }
+ }
+
+ leaf outgoing-interface {
+ type leafref {
+ path "/interface/name";
+ }
+
+ must 'count(/interface[re-match(name, "eth0\.\d+")]) = 3';
+ }
+
+ container mgmt-interface {
+ leaf name {
+ type leafref {
+ path "/interface/name";
+ }
+ }
+ leaf type {
+ type leafref {
+ path "/interface[name=current()/../name]/type";
+ }
+ must 'derived-from-or-self(deref(.), "foo:ethernet")';
+ }
+ }
+
+ container my-cont {
+ leaf enum-value {
+ type enumeration {
+ enum a {
+ value 1;
+ }
+ enum b {
+ value 2;
+ }
+ }
+ }
+
+ must "enum-value(current()/enum-value) = 1";
+
+ leaf bit-is-set {
+ type bits {
+ bit x;
+ bit y;
+ bit z;
+ }
+ }
+
+ leaf bits-leaf {
+ type bits {
+ bit a;
+ bit b;
+ bit c;
+ }
+ }
+
+ must 'bit-is-set(current()/bit-is-set, "z") and bit-is-set(current()/bits-leaf, "c")';
+ }
+}
--- /dev/null
+module foo {
+ namespace foo;
+ prefix foo;
+
+ revision 2017-02-10;
+
+ list interface {
+ key "name type";
+ leaf name {
+ type string;
+ }
+ leaf type {
+ type string;
+ }
+ leaf enabled {
+ type boolean;
+ }
+ }
+
+ container mgmt-interface {
+ leaf name {
+ type leafref {
+ path "/interface/name";
+ }
+ }
+ leaf type {
+ type leafref {
+ path "/interface[name=current()/../name]/type";
+ }
+ must "deref(.)/../enabled = 'true'" {
+ error-message "The management interface cannot be disabled.";
+ }
+ }
+ }
+}
\ No newline at end of file
--- /dev/null
+module foo {
+ namespace foo;
+ prefix foo;
+
+ revision 2017-02-10;
+
+ list interface {
+ key "name";
+ leaf name {
+ type string;
+ }
+
+ leaf enabled {
+ type boolean;
+ }
+ }
+
+ leaf outgoing-interface {
+ type leafref {
+ path "/interface/name";
+ }
+
+ must "count(/interface[re-match(name, 'eth0\.\d+')]) = 3";
+ }
+}
\ No newline at end of file