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