--- /dev/null
+/*
+ * 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);
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}