Add YangTextSnippet to yang-model-export 93/67893/4
authorRobert Varga <robert.varga@pantheon.tech>
Sat, 3 Feb 2018 19:33:57 +0000 (20:33 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Wed, 7 Feb 2018 12:06:40 +0000 (13:06 +0100)
This is a simple class generating a sequence of strings, concatenation
of which produces YANG text corresponding to a DeclaredStatement.

The snipped is properly indented, can omit user-specified statements,
and by default omits statements with default values as per RFC6087.

JIRA: MDSAL-301
Change-Id: Ie45e947dccd33b851e23effb827ddc66690451f6
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
yang/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/stmt/ModuleEffectiveStatement.java
yang/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippet.java [new file with mode: 0644]
yang/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippetIterator.java [new file with mode: 0644]
yang/yang-model-export/src/test/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippetTest.java [new file with mode: 0644]

index 2feac3d08fb0698b82c9dee0ea5dc241baa5f4f5..8a05f8711daf5bf9834125b5285602f56e18a3de 100644 (file)
@@ -19,7 +19,8 @@ public interface ModuleEffectiveStatement extends EffectiveStatement<String, Mod
      * Namespace mapping all known prefixes in a module to their modules. Note this namespace includes the module
      * in which it is instantiated.
      */
-    abstract class PrefixToEffectiveModuleNamespace implements IdentifierNamespace<String, ModuleEffectiveStatement> {
+    abstract class PrefixToEffectiveModuleNamespace
+            implements IdentifierNamespace<String, @NonNull ModuleEffectiveStatement> {
         private PrefixToEffectiveModuleNamespace() {
             // This class should never be subclassed
         }
@@ -29,7 +30,7 @@ public interface ModuleEffectiveStatement extends EffectiveStatement<String, Mod
      * Namespace mapping all known {@link QNameModule}s to their encoding prefixes. This includes the declaration
      * from prefix/namespace/revision and all imports as they were resolved.
      */
-    abstract class QNameModuleToPrefixNamespace implements IdentifierNamespace<QNameModule, String> {
+    abstract class QNameModuleToPrefixNamespace implements IdentifierNamespace<QNameModule, @NonNull String> {
         private QNameModuleToPrefixNamespace() {
             // This class should never be subclassed
         }
diff --git a/yang/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippet.java b/yang/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippet.java
new file mode 100644 (file)
index 0000000..6993381
--- /dev/null
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. 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.model.export;
+
+import static java.util.Objects.requireNonNull;
+import static org.eclipse.jdt.annotation.DefaultLocation.PARAMETER;
+import static org.eclipse.jdt.annotation.DefaultLocation.RETURN_TYPE;
+
+import com.google.common.annotations.Beta;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+import javax.annotation.concurrent.ThreadSafe;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.opendaylight.yangtools.concepts.Immutable;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.model.api.meta.DeclaredStatement;
+import org.opendaylight.yangtools.yang.model.api.meta.StatementDefinition;
+import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement.QNameModuleToPrefixNamespace;
+
+/**
+ * A YANG text snippet generated from a {@link DeclaredStatement}. Generated {@link #stream()} or {@link #iterator()}
+ * are guaranteed to not contain null nor empty strings. Furthermore, newlines are always emitted at the end
+ * on the generated string -- which can be checked with {@link #isEolString(String)} utility method.
+ *
+ * <p>
+ * This allows individual strings to be escaped as needed and external indentation can be accounted for by inserting
+ * outer document indentation after the string which matched {@link #isEolString(String)} is emitted to the stream.
+ *
+ * @author Robert Varga
+ */
+@Beta
+@ThreadSafe
+@NonNullByDefault({ PARAMETER, RETURN_TYPE })
+public final class YangTextSnippet implements Immutable, Iterable<@NonNull String> {
+    private final Set<@NonNull StatementDefinition> ignoredStatements;
+    private final Map<QNameModule, @NonNull String> mapper;
+    private final DeclaredStatement<?> statement;
+    private final boolean omitDefaultStatements;
+
+    private YangTextSnippet(final DeclaredStatement<?> statement,
+            final Map<QNameModule, @NonNull String> namespaces,
+            final Set<@NonNull StatementDefinition> ignoredStatements, final boolean omitDefaultStatements) {
+        this.statement = requireNonNull(statement);
+        this.mapper = requireNonNull(namespaces);
+        this.ignoredStatements = requireNonNull(ignoredStatements);
+        this.omitDefaultStatements = omitDefaultStatements;
+    }
+
+    public static Builder builder(final ModuleEffectiveStatement module, final DeclaredStatement<?> statement) {
+        return new Builder(module, statement);
+    }
+
+    @Override
+    public Iterator<@NonNull String> iterator() {
+        return new YangTextSnippetIterator(statement, mapper, ignoredStatements, omitDefaultStatements);
+    }
+
+    @Override
+    @SuppressWarnings("null")
+    public Spliterator<@NonNull String> spliterator() {
+        return Spliterators.spliteratorUnknownSize(iterator(),
+            Spliterator.ORDERED | Spliterator.IMMUTABLE | Spliterator.NONNULL);
+    }
+
+    @SuppressWarnings("null")
+    public Stream<@NonNull String> stream() {
+        return StreamSupport.stream(spliterator(), false);
+    }
+
+    /**
+     * Check if an emitted string contains End-Of-Line character.
+     *
+     * @param str String to be checked
+     * @return True if the string contains end of line.
+     */
+    public static boolean isEolString(final String str) {
+        return str.charAt(str.length() - 1) == '\n';
+    }
+
+    @Override
+    @SuppressWarnings("null")
+    public String toString() {
+        return stream().collect(Collectors.joining());
+    }
+
+    /**
+     * Builder class for instantiation of a customized {@link YangTextSnippet}.
+     */
+    @Beta
+    public static final class Builder implements org.opendaylight.yangtools.concepts.Builder<@NonNull YangTextSnippet> {
+        private final List<@NonNull StatementDefinition> ignoredStatements = new ArrayList<>();
+        private final @NonNull DeclaredStatement<?> statement;
+        private final @NonNull ModuleEffectiveStatement module;
+        private boolean retainDefaultStatements;
+
+        Builder(final ModuleEffectiveStatement module, final DeclaredStatement<?> statement) {
+            this.module = requireNonNull(module);
+            this.statement = requireNonNull(statement);
+        }
+
+        /**
+         * Add a statement which should be skipped along with any of its children.
+         *
+         * @param statementDef Statement to be ignored
+         * @return This builder
+         */
+        public Builder addIgnoredStatement(final StatementDefinition statementDef) {
+            ignoredStatements.add(requireNonNull(statementDef));
+            return this;
+        }
+
+        /**
+         * Retain common known statements whose argument matches semantics of not being present. By default these
+         * statements are omitted from output.
+         *
+         * @return This builder
+         */
+        public Builder retainDefaultStatements() {
+            retainDefaultStatements = true;
+            return this;
+        }
+
+        @Override
+        public YangTextSnippet build() {
+            return new YangTextSnippet(statement, module.getAll(QNameModuleToPrefixNamespace.class),
+                ImmutableSet.copyOf(ignoredStatements), !retainDefaultStatements);
+        }
+    }
+}
diff --git a/yang/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippetIterator.java b/yang/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippetIterator.java
new file mode 100644 (file)
index 0000000..5830f18
--- /dev/null
@@ -0,0 +1,302 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. 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.model.export;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Verify.verify;
+import static java.util.Objects.requireNonNull;
+import static org.eclipse.jdt.annotation.DefaultLocation.PARAMETER;
+import static org.eclipse.jdt.annotation.DefaultLocation.RETURN_TYPE;
+
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.AbstractIterator;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import java.util.ArrayDeque;
+import java.util.Collection;
+import java.util.Deque;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Queue;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+import org.opendaylight.yangtools.yang.common.YangConstants;
+import org.opendaylight.yangtools.yang.model.api.YangStmtMapping;
+import org.opendaylight.yangtools.yang.model.api.meta.DeclaredStatement;
+import org.opendaylight.yangtools.yang.model.api.meta.StatementDefinition;
+
+/**
+ * This is an iterator over strings needed to assemble a YANG snippet.
+ *
+ * @author Robert Varga
+ */
+@NonNullByDefault({ PARAMETER, RETURN_TYPE })
+final class YangTextSnippetIterator extends AbstractIterator<@NonNull String> {
+    // https://tools.ietf.org/html/rfc7950#section-6.1.3
+    //            An unquoted string is any sequence of characters that does not
+    //            contain any space, tab, carriage return, or line feed characters, a
+    //            single or double quote character, a semicolon (";"), braces ("{" or
+    //            "}"), or comment sequences ("//", "/*", or "*/").
+    // Newline is treated separately, so it is not included here
+    private static final CharMatcher NEED_QUOTE_MATCHER = CharMatcher.anyOf(" \t\r'\";{}");
+    private static final CharMatcher DQUOT_MATCHER = CharMatcher.is('"');
+    private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n');
+    private static final Collection<StatementDefinition> QUOTE_MULTILINE_STATEMENTS = ImmutableSet.of(
+        YangStmtMapping.CONTACT,
+        YangStmtMapping.DESCRIPTION,
+        YangStmtMapping.ERROR_MESSAGE,
+        YangStmtMapping.ORGANIZATION,
+        YangStmtMapping.REFERENCE);
+
+    /*
+     * https://tools.ietf.org/html/rfc6087#section-4.3:
+     *            In general, it is suggested that substatements containing very common
+     *            default values SHOULD NOT be present.  The following substatements
+     *            are commonly used with the default value, which would make the module
+     *            difficult to read if used everywhere they are allowed.
+     */
+    private static final Map<StatementDefinition, String> DEFAULT_STATEMENTS =
+            ImmutableMap.<StatementDefinition, String>builder()
+            .put(YangStmtMapping.CONFIG, "true")
+            .put(YangStmtMapping.MANDATORY, "true")
+            .put(YangStmtMapping.MAX_ELEMENTS, "unbounded")
+            .put(YangStmtMapping.MIN_ELEMENTS, "0")
+            .put(YangStmtMapping.ORDERED_BY, "system")
+            .put(YangStmtMapping.REQUIRE_INSTANCE, "true")
+            .put(YangStmtMapping.STATUS, "current")
+            .put(YangStmtMapping.YIN_ELEMENT, "false")
+            .build();
+
+    private static final String INDENT = "  ";
+    private static final int INDENT_STRINGS_SIZE = 16;
+    private static final String[] INDENT_STRINGS = new String[INDENT_STRINGS_SIZE];
+
+    static {
+        for (int i = 0; i < INDENT_STRINGS_SIZE; i++) {
+            INDENT_STRINGS[i] = Strings.repeat(INDENT, i).intern();
+        }
+    }
+
+    private enum Quoting {
+        /**
+         * No quoting necessary.
+         */
+        NONE,
+        /**
+         * Argument is empty, quote an empty string.
+         */
+        EMPTY,
+        /**
+         * Quote on the same line.
+         */
+        SIMPLE,
+        /**
+         * Quote starting on next line.
+         */
+        MULTILINE;
+    }
+
+    /*
+     * We normally have up to 10 strings:
+     *               <indent>
+     *               <prefix>
+     *               ":"
+     *               <name>
+     *               " \n"
+     *               <indent>
+     *               "\""
+     *               <argument>
+     *               "\""
+     *               ";\n"
+     *
+     * But all of this is typically not used:
+     * - statements usually do not have a prefix, saving two items
+     * - arguments are not typically quoted, saving another two
+     *
+     * In case we get into a multi-line argument, we are already splitting strings, so the cost of growing
+     * the queue is negligible
+     */
+    private final Queue<String> strings = new ArrayDeque<>(8);
+    // Let's be modest, 16-level deep constructs are not exactly common.
+    private final Deque<Iterator<? extends DeclaredStatement<?>>> stack = new ArrayDeque<>(8);
+    private final Map<QNameModule, @NonNull String> namespaces;
+    private final Set<StatementDefinition> ignoredStatements;
+    private final boolean omitDefaultStatements;
+
+    YangTextSnippetIterator(final DeclaredStatement<?> stmt, final Map<QNameModule, @NonNull String> namespaces,
+        final Set<StatementDefinition> ignoredStatements, final boolean omitDefaultStatements) {
+        this.namespaces = requireNonNull(namespaces);
+        this.ignoredStatements = requireNonNull(ignoredStatements);
+        this.omitDefaultStatements = omitDefaultStatements;
+        pushStatement(requireNonNull(stmt));
+    }
+
+    @Override
+    protected @NonNull String computeNext() {
+        // We may have some strings stashed, take one out, if that is the case
+        final String nextString = strings.poll();
+        if (nextString != null) {
+            return nextString;
+        }
+
+        final Iterator<? extends DeclaredStatement<?>> it = stack.peek();
+        if (it == null) {
+            endOfData();
+            // Post-end of data, the user will never see this
+            return "";
+        }
+
+        // Try to push next child
+        while (it.hasNext()) {
+            if (pushStatement(it.next())) {
+                return strings.remove();
+            }
+        }
+
+        // End of children, close the parent statement
+        stack.pop();
+        addIndent();
+        strings.add("}\n");
+        return strings.remove();
+    }
+
+    /**
+     * Push a statement to the stack. A successfully-pushed statement results in strings not being empty.
+     *
+     * @param stmt Statement to push into strings
+     * @return True if the statement was pushed. False if the statement was suppressed.
+     */
+    private boolean pushStatement(final DeclaredStatement<?> stmt) {
+        final StatementDefinition def = stmt.statementDefinition();
+        if (ignoredStatements.contains(def)) {
+            return false;
+        }
+
+        final Collection<? extends DeclaredStatement<?>> children = stmt.declaredSubstatements();
+        if (omitDefaultStatements && children.isEmpty()) {
+            // This statement does not have substatements, check if its value matches the declared default, like
+            // "config true", "mandatory false", etc.
+            final String suppressValue = DEFAULT_STATEMENTS.get(def);
+            if (suppressValue != null && suppressValue.equals(stmt.rawArgument())) {
+                return false;
+            }
+        }
+
+        // New statement: push indent
+        addIndent();
+
+        // Add statement prefixed with namespace if needed
+        final QName stmtName = def.getStatementName();
+        addNamespace(stmtName.getModule());
+        strings.add(stmtName.getLocalName());
+
+        // Add argument, quoted and properly indented if need be
+        addArgument(def, stmt.rawArgument());
+
+        if (!children.isEmpty()) {
+            // Open the statement and push child iterator
+            strings.add(" {\n");
+            stack.push(children.iterator());
+        } else {
+            // Close the statement
+            strings.add(";\n");
+        }
+
+        return true;
+    }
+
+    private void addIndent() {
+        int depth = stack.size();
+        while (depth >= INDENT_STRINGS_SIZE) {
+            strings.add(INDENT_STRINGS[INDENT_STRINGS_SIZE - 1]);
+            depth -= INDENT_STRINGS_SIZE;
+        }
+        if (depth > 0) {
+            strings.add(INDENT_STRINGS[depth]);
+        }
+    }
+
+    private void addNamespace(final QNameModule namespace) {
+        if (YangConstants.RFC6020_YIN_MODULE.equals(namespace)) {
+            // Default namespace, no prefix needed
+            return;
+        }
+
+        final @Nullable String prefix = namespaces.get(namespace);
+        checkArgument(prefix != null, "Failed to find prefix for namespace %s", namespace);
+        verify(!prefix.isEmpty(), "Empty prefix for namespace %s", namespace);
+        strings.add(prefix);
+        strings.add(":");
+    }
+
+    private void addArgument(final StatementDefinition def, final @Nullable String arg) {
+        if (arg == null) {
+            // No argument, nothing to do
+            return;
+        }
+
+        switch (quoteKind(def, arg)) {
+            case EMPTY:
+                strings.add(" \"\"");
+                break;
+            case NONE:
+                strings.add(" ");
+                strings.add(arg);
+                break;
+            case SIMPLE:
+                strings.add(" \"");
+                strings.add(DQUOT_MATCHER.replaceFrom(arg, "\\\""));
+                strings.add("\"");
+                break;
+            case MULTILINE:
+                strings.add("\n");
+                addIndent();
+                strings.add(INDENT + '\"');
+
+                final Iterator<String> it = NEWLINE_SPLITTER.split(DQUOT_MATCHER.replaceFrom(arg, "\\\"")).iterator();
+                final String first = it.next();
+                if (!first.isEmpty()) {
+                    strings.add(first);
+                }
+
+                while (it.hasNext()) {
+                    strings.add("\n");
+                    final String str = it.next();
+                    if (!str.isEmpty()) {
+                        addIndent();
+                        strings.add(INDENT + ' ');
+                        strings.add(str);
+                    }
+                }
+                strings.add("\"");
+                break;
+            default:
+                throw new IllegalStateException("Illegal quoting for " + def + " argument \"" + arg + "\"");
+        }
+    }
+
+    private static Quoting quoteKind(final StatementDefinition def, final String str) {
+        if (str.isEmpty()) {
+            return Quoting.EMPTY;
+        }
+        if (QUOTE_MULTILINE_STATEMENTS.contains(def) || str.indexOf('\n') != -1) {
+            return Quoting.MULTILINE;
+        }
+        if (NEED_QUOTE_MATCHER.matchesAnyOf(str) || str.contains("//") || str.contains("/*") || str.contains("*/")) {
+            return Quoting.SIMPLE;
+        }
+
+        return Quoting.NONE;
+    }
+}
diff --git a/yang/yang-model-export/src/test/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippetTest.java b/yang/yang-model-export/src/test/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippetTest.java
new file mode 100644 (file)
index 0000000..7ae29ed
--- /dev/null
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2018 Pantheon Technologies, s.r.o. 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.model.export;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.model.api.Module;
+import org.opendaylight.yangtools.yang.model.api.SchemaContext;
+import org.opendaylight.yangtools.yang.model.api.stmt.ModuleEffectiveStatement;
+import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+
+public class YangTextSnippetTest {
+    @Test
+    public void testNotification() {
+        final SchemaContext schema = YangParserTestUtils.parseYangResource("/bugs/bug2444/yang/notification.yang");
+
+        for (Module module : schema.getModules()) {
+            assertTrue(module instanceof ModuleEffectiveStatement);
+            final ModuleEffectiveStatement stmt = (ModuleEffectiveStatement) module;
+
+            final String str = YangTextSnippet.builder(stmt, stmt.getDeclared()).build().toString();
+            assertNotNull(str);
+        }
+    }
+}