1166f407e72b4e935959f0c662aa197dbd3db07d
[yangtools.git] / yang / yang-model-export / src / main / java / org / opendaylight / yangtools / yang / model / export / YangTextSnippetIterator.java
1 /*
2  * Copyright (c) 2018 Pantheon Technologies, s.r.o. and others.  All rights reserved.
3  *
4  * This program and the accompanying materials are made available under the
5  * terms of the Eclipse Public License v1.0 which accompanies this distribution,
6  * and is available at http://www.eclipse.org/legal/epl-v10.html
7  */
8 package org.opendaylight.yangtools.yang.model.export;
9
10 import static java.util.Objects.requireNonNull;
11 import static org.eclipse.jdt.annotation.DefaultLocation.PARAMETER;
12 import static org.eclipse.jdt.annotation.DefaultLocation.RETURN_TYPE;
13
14 import com.google.common.base.CharMatcher;
15 import com.google.common.base.Splitter;
16 import com.google.common.base.Strings;
17 import com.google.common.collect.AbstractIterator;
18 import com.google.common.collect.ImmutableMap;
19 import com.google.common.collect.ImmutableSet;
20 import java.util.ArrayDeque;
21 import java.util.Collection;
22 import java.util.Deque;
23 import java.util.Iterator;
24 import java.util.Map;
25 import java.util.Optional;
26 import java.util.Queue;
27 import java.util.Set;
28 import org.eclipse.jdt.annotation.NonNull;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.opendaylight.yangtools.yang.common.QName;
32 import org.opendaylight.yangtools.yang.common.QNameModule;
33 import org.opendaylight.yangtools.yang.model.api.YangStmtMapping;
34 import org.opendaylight.yangtools.yang.model.api.meta.DeclaredStatement;
35 import org.opendaylight.yangtools.yang.model.api.meta.StatementDefinition;
36
37 /**
38  * This is an iterator over strings needed to assemble a YANG snippet.
39  *
40  * @author Robert Varga
41  */
42 @NonNullByDefault({ PARAMETER, RETURN_TYPE })
43 final class YangTextSnippetIterator extends AbstractIterator<@NonNull String> {
44     // https://tools.ietf.org/html/rfc7950#section-6.1.3
45     //            An unquoted string is any sequence of characters that does not
46     //            contain any space, tab, carriage return, or line feed characters, a
47     //            single or double quote character, a semicolon (";"), braces ("{" or
48     //            "}"), or comment sequences ("//", "/*", or "*/").
49     // Newline is treated separately, so it is not included here
50     private static final CharMatcher NEED_QUOTE_MATCHER = CharMatcher.anyOf(" \t\r'\";{}");
51     private static final CharMatcher DQUOT_MATCHER = CharMatcher.is('"');
52     private static final Splitter NEWLINE_SPLITTER = Splitter.on('\n');
53     private static final ImmutableSet<StatementDefinition> QUOTE_MULTILINE_STATEMENTS = ImmutableSet.of(
54         YangStmtMapping.CONTACT,
55         YangStmtMapping.DESCRIPTION,
56         YangStmtMapping.ERROR_MESSAGE,
57         YangStmtMapping.ORGANIZATION,
58         YangStmtMapping.REFERENCE);
59
60     /*
61      * https://tools.ietf.org/html/rfc6087#section-4.3:
62      *            In general, it is suggested that substatements containing very common
63      *            default values SHOULD NOT be present.  The following substatements
64      *            are commonly used with the default value, which would make the module
65      *            difficult to read if used everywhere they are allowed.
66      */
67     private static final ImmutableMap<StatementDefinition, String> DEFAULT_STATEMENTS =
68             ImmutableMap.<StatementDefinition, String>builder()
69             .put(YangStmtMapping.CONFIG, "true")
70             .put(YangStmtMapping.MANDATORY, "true")
71             .put(YangStmtMapping.MAX_ELEMENTS, "unbounded")
72             .put(YangStmtMapping.MIN_ELEMENTS, "0")
73             .put(YangStmtMapping.ORDERED_BY, "system")
74             .put(YangStmtMapping.REQUIRE_INSTANCE, "true")
75             .put(YangStmtMapping.STATUS, "current")
76             .put(YangStmtMapping.YIN_ELEMENT, "false")
77             .build();
78
79     private static final String INDENT = "  ";
80     private static final int INDENT_STRINGS_SIZE = 16;
81     private static final String[] INDENT_STRINGS = new String[INDENT_STRINGS_SIZE];
82
83     static {
84         for (int i = 0; i < INDENT_STRINGS_SIZE; i++) {
85             INDENT_STRINGS[i] = Strings.repeat(INDENT, i).intern();
86         }
87     }
88
89     private enum Quoting {
90         /**
91          * No quoting necessary.
92          */
93         NONE,
94         /**
95          * Argument is empty, quote an empty string.
96          */
97         EMPTY,
98         /**
99          * Quote on the same line.
100          */
101         SIMPLE,
102         /**
103          * Quote starting on next line.
104          */
105         MULTILINE;
106     }
107
108     /*
109      * We normally have up to 10 strings:
110      *               <indent>
111      *               <prefix>
112      *               ":"
113      *               <name>
114      *               " \n"
115      *               <indent>
116      *               "\""
117      *               <argument>
118      *               "\""
119      *               ";\n"
120      *
121      * But all of this is typically not used:
122      * - statements usually do not have a prefix, saving two items
123      * - arguments are not typically quoted, saving another two
124      *
125      * In case we get into a multi-line argument, we are already splitting strings, so the cost of growing
126      * the queue is negligible
127      */
128     private final Queue<String> strings = new ArrayDeque<>(8);
129     // Let's be modest, 16-level deep constructs are not exactly common.
130     private final Deque<Iterator<? extends DeclaredStatement<?>>> stack = new ArrayDeque<>(8);
131     private final Map<QNameModule, @NonNull String> namespaces;
132     private final Set<StatementDefinition> ignoredStatements;
133     private final boolean omitDefaultStatements;
134
135     YangTextSnippetIterator(final DeclaredStatement<?> stmt, final Map<QNameModule, @NonNull String> namespaces,
136         final Set<StatementDefinition> ignoredStatements, final boolean omitDefaultStatements) {
137         this.namespaces = requireNonNull(namespaces);
138         this.ignoredStatements = requireNonNull(ignoredStatements);
139         this.omitDefaultStatements = omitDefaultStatements;
140         pushStatement(requireNonNull(stmt));
141     }
142
143     @Override
144     protected @NonNull String computeNext() {
145         // We may have some strings stashed, take one out, if that is the case
146         final String nextString = strings.poll();
147         if (nextString != null) {
148             return nextString;
149         }
150
151         final Iterator<? extends DeclaredStatement<?>> it = stack.peek();
152         if (it == null) {
153             endOfData();
154             // Post-end of data, the user will never see this
155             return "";
156         }
157
158         // Try to push next child
159         while (it.hasNext()) {
160             if (pushStatement(it.next())) {
161                 return strings.remove();
162             }
163         }
164
165         // End of children, close the parent statement
166         stack.pop();
167         addIndent();
168         strings.add("}\n");
169         return strings.remove();
170     }
171
172     /**
173      * Push a statement to the stack. A successfully-pushed statement results in strings not being empty.
174      *
175      * @param stmt Statement to push into strings
176      * @return True if the statement was pushed. False if the statement was suppressed.
177      */
178     private boolean pushStatement(final DeclaredStatement<?> stmt) {
179         final StatementDefinition def = stmt.statementDefinition();
180         if (ignoredStatements.contains(def)) {
181             return false;
182         }
183
184         final Collection<? extends DeclaredStatement<?>> children = stmt.declaredSubstatements();
185         if (omitDefaultStatements && children.isEmpty()) {
186             // This statement does not have substatements, check if its value matches the declared default, like
187             // "config true", "mandatory false", etc.
188             final String suppressValue = DEFAULT_STATEMENTS.get(def);
189             if (suppressValue != null && suppressValue.equals(stmt.rawArgument())) {
190                 return false;
191             }
192         }
193
194         // New statement: push indent
195         addIndent();
196
197         // Add statement prefixed with namespace if needed
198         final QName stmtName = def.getStatementName();
199         final Optional<String> prefix = ExportUtils.statementPrefix(namespaces, stmtName);
200         if (prefix.isPresent()) {
201             strings.add(prefix.get());
202             strings.add(":");
203         }
204         strings.add(stmtName.getLocalName());
205
206         // Add argument, quoted and properly indented if need be
207         addArgument(def, stmt.rawArgument());
208
209         if (!children.isEmpty()) {
210             // Open the statement and push child iterator
211             strings.add(" {\n");
212             stack.push(children.iterator());
213         } else {
214             // Close the statement
215             strings.add(";\n");
216         }
217
218         return true;
219     }
220
221     private void addIndent() {
222         int depth = stack.size();
223         while (depth >= INDENT_STRINGS_SIZE) {
224             strings.add(INDENT_STRINGS[INDENT_STRINGS_SIZE - 1]);
225             depth -= INDENT_STRINGS_SIZE;
226         }
227         if (depth > 0) {
228             strings.add(INDENT_STRINGS[depth]);
229         }
230     }
231
232     private void addArgument(final StatementDefinition def, final @Nullable String arg) {
233         if (arg == null) {
234             // No argument, nothing to do
235             return;
236         }
237
238         switch (quoteKind(def, arg)) {
239             case EMPTY:
240                 strings.add(" \"\"");
241                 break;
242             case NONE:
243                 strings.add(" ");
244                 strings.add(arg);
245                 break;
246             case SIMPLE:
247                 strings.add(" \"");
248                 strings.add(DQUOT_MATCHER.replaceFrom(arg, "\\\""));
249                 strings.add("\"");
250                 break;
251             case MULTILINE:
252                 strings.add("\n");
253                 addIndent();
254                 strings.add(INDENT + '\"');
255
256                 final Iterator<String> it = NEWLINE_SPLITTER.split(DQUOT_MATCHER.replaceFrom(arg, "\\\"")).iterator();
257                 final String first = it.next();
258                 if (!first.isEmpty()) {
259                     strings.add(first);
260                 }
261
262                 while (it.hasNext()) {
263                     strings.add("\n");
264                     final String str = it.next();
265                     if (!str.isEmpty()) {
266                         addIndent();
267                         strings.add(INDENT + ' ');
268                         strings.add(str);
269                     }
270                 }
271                 strings.add("\"");
272                 break;
273             default:
274                 throw new IllegalStateException("Illegal quoting for " + def + " argument \"" + arg + "\"");
275         }
276     }
277
278     private static Quoting quoteKind(final StatementDefinition def, final String str) {
279         if (str.isEmpty()) {
280             return Quoting.EMPTY;
281         }
282         if (QUOTE_MULTILINE_STATEMENTS.contains(def) || str.indexOf('\n') != -1) {
283             return Quoting.MULTILINE;
284         }
285         if (NEED_QUOTE_MATCHER.matchesAnyOf(str) || str.contains("//") || str.contains("/*") || str.contains("*/")) {
286             return Quoting.SIMPLE;
287         }
288
289         return Quoting.NONE;
290     }
291 }