From: Robert Varga Date: Sat, 3 Feb 2018 19:33:57 +0000 (+0100) Subject: Add YangTextSnippet to yang-model-export X-Git-Tag: v2.0.2~43 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?a=commitdiff_plain;h=9f11d4713a6f3faa2b5dc7c70958ea022f053d5c;p=yangtools.git Add YangTextSnippet to yang-model-export 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 --- diff --git a/yang/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/stmt/ModuleEffectiveStatement.java b/yang/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/stmt/ModuleEffectiveStatement.java index 2feac3d08f..8a05f8711d 100644 --- a/yang/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/stmt/ModuleEffectiveStatement.java +++ b/yang/yang-model-api/src/main/java/org/opendaylight/yangtools/yang/model/api/stmt/ModuleEffectiveStatement.java @@ -19,7 +19,8 @@ public interface ModuleEffectiveStatement extends EffectiveStatement { + abstract class PrefixToEffectiveModuleNamespace + implements IdentifierNamespace { private PrefixToEffectiveModuleNamespace() { // This class should never be subclassed } @@ -29,7 +30,7 @@ public interface ModuleEffectiveStatement extends EffectiveStatement { + abstract class QNameModuleToPrefixNamespace implements IdentifierNamespace { 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 index 0000000000..6993381e77 --- /dev/null +++ b/yang/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippet.java @@ -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. + * + *

+ * 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 mapper; + private final DeclaredStatement statement; + private final boolean omitDefaultStatements; + + private YangTextSnippet(final DeclaredStatement statement, + final Map 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 index 0000000000..5830f18622 --- /dev/null +++ b/yang/yang-model-export/src/main/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippetIterator.java @@ -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 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 DEFAULT_STATEMENTS = + ImmutableMap.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: + * + * + * ":" + * + * " \n" + * + * "\"" + * + * "\"" + * ";\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 strings = new ArrayDeque<>(8); + // Let's be modest, 16-level deep constructs are not exactly common. + private final Deque>> stack = new ArrayDeque<>(8); + private final Map namespaces; + private final Set ignoredStatements; + private final boolean omitDefaultStatements; + + YangTextSnippetIterator(final DeclaredStatement stmt, final Map namespaces, + final Set 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> 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> 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 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 index 0000000000..7ae29ed4c6 --- /dev/null +++ b/yang/yang-model-export/src/test/java/org/opendaylight/yangtools/yang/model/export/YangTextSnippetTest.java @@ -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); + } + } +}