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