Turn ArgumentContextUtils into an abstract class
[yangtools.git] / yang / yang-parser-rfc7950 / src / main / java / org / opendaylight / yangtools / yang / parser / rfc7950 / repo / ArgumentContextUtils.java
1 /*
2  * Copyright (c) 2015 Cisco Systems, Inc. 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.parser.rfc7950.repo;
9
10 import static com.google.common.base.Verify.verify;
11
12 import com.google.common.annotations.VisibleForTesting;
13 import com.google.common.base.CharMatcher;
14 import com.google.common.base.VerifyException;
15 import java.util.regex.Pattern;
16 import org.antlr.v4.runtime.tree.ParseTree;
17 import org.antlr.v4.runtime.tree.TerminalNode;
18 import org.eclipse.jdt.annotation.NonNull;
19 import org.opendaylight.yangtools.yang.common.YangVersion;
20 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser;
21 import org.opendaylight.yangtools.yang.parser.antlr.YangStatementParser.ArgumentContext;
22 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
23 import org.opendaylight.yangtools.yang.parser.spi.source.StatementSourceReference;
24
25 /**
26  * Utilities for dealing with YANG statement argument strings, encapsulated in ANTLR grammar's ArgumentContext.
27  */
28 abstract class ArgumentContextUtils {
29     /**
30      * YANG 1.0 version of strings, which were not completely clarified in
31      * <a href="https://tools.ietf.org/html/rfc6020#section-6.1.3">RFC6020</a>.
32      */
33     private static final class RFC6020 extends ArgumentContextUtils {
34         private static final @NonNull RFC6020 INSTANCE = new RFC6020();
35
36         @Override
37         void checkDoubleQuoted(final String str, final StatementSourceReference ref) {
38             // No-op
39         }
40
41         @Override
42         void checkUnquoted(final String str, final StatementSourceReference ref) {
43             // No-op
44         }
45     }
46
47     /**
48      * YANG 1.1 version of strings, which were clarified in
49      * <a href="https://tools.ietf.org/html/rfc7950#section-6.1.3">RFC7950</a>.
50      */
51     // NOTE: the differences clarified lead to a proper ability to delegate this to ANTLR lexer, but that does not
52     //       understand versions and needs to work with both.
53     private static final class RFC7950 extends ArgumentContextUtils {
54         private static final CharMatcher ANYQUOTE_MATCHER = CharMatcher.anyOf("'\"");
55         private static final @NonNull RFC7950 INSTANCE = new RFC7950();
56
57         @Override
58         void checkDoubleQuoted(final String str, final StatementSourceReference ref) {
59             // FIXME: YANGTOOLS-1079: we should forward backslash to this method, so that it does not start from the
60             //                        start from the start of the string. Furthermore this logic should operate on spans
61             //                        of characters -- i.e. the check for backslash should be a search instead -- as
62             //                        String knows how to do that and can do it more efficiently than this loop.
63             for (int i = 0; i < str.length() - 1; i++) {
64                 if (str.charAt(i) == '\\') {
65                     switch (str.charAt(i + 1)) {
66                         case 'n':
67                         case 't':
68                         case '\\':
69                         case '\"':
70                             i++;
71                             break;
72                         default:
73                             throw new SourceException(ref, "YANG 1.1: illegal double quoted string (%s). In double "
74                                     + "quoted string the backslash must be followed by one of the following character "
75                                     + "[n,t,\",\\], but was '%s'.", str, str.charAt(i + 1));
76                     }
77                 }
78             }
79         }
80
81         @Override
82         void checkUnquoted(final String str, final StatementSourceReference ref) {
83             SourceException.throwIf(ANYQUOTE_MATCHER.matchesAnyOf(str), ref,
84                 "YANG 1.1: unquoted string (%s) contains illegal characters", str);
85         }
86     }
87
88     private static final CharMatcher WHITESPACE_MATCHER = CharMatcher.whitespace();
89     private static final Pattern ESCAPED_DQUOT = Pattern.compile("\\\"", Pattern.LITERAL);
90     private static final Pattern ESCAPED_BACKSLASH = Pattern.compile("\\\\", Pattern.LITERAL);
91     private static final Pattern ESCAPED_LF = Pattern.compile("\\n", Pattern.LITERAL);
92     private static final Pattern ESCAPED_TAB = Pattern.compile("\\t", Pattern.LITERAL);
93
94     private ArgumentContextUtils() {
95         // Hidden on purpose
96     }
97
98     static @NonNull ArgumentContextUtils forVersion(final YangVersion version) {
99         switch (version) {
100             case VERSION_1:
101                 return RFC6020.INSTANCE;
102             case VERSION_1_1:
103                 return RFC7950.INSTANCE;
104             default:
105                 throw new IllegalStateException("Unhandled version " + version);
106         }
107     }
108
109     // TODO: teach the only caller about versions, or provide common-enough idioms for its use case
110     static @NonNull ArgumentContextUtils rfc6020() {
111         return RFC6020.INSTANCE;
112     }
113
114     /*
115      * NOTE: this method we do not use convenience methods provided by generated parser code, but instead are making
116      *       based on the grammar assumptions. While this is more verbose, it cuts out a number of unnecessary code,
117      *       such as intermediate List allocation et al.
118      */
119     final @NonNull String stringFromStringContext(final ArgumentContext context, final StatementSourceReference ref) {
120         // Get first child, which we fully expect to exist and be a lexer token
121         final ParseTree firstChild = context.getChild(0);
122         verify(firstChild instanceof TerminalNode, "Unexpected shape of %s", context);
123         final TerminalNode firstNode = (TerminalNode) firstChild;
124         final int firstType = firstNode.getSymbol().getType();
125         switch (firstType) {
126             case YangStatementParser.IDENTIFIER:
127                 // Simple case, there is a simple string, which cannot contain anything that we would need to process.
128                 return firstNode.getText();
129             case YangStatementParser.STRING:
130                 // Complex case, defer to a separate method
131                 return concatStrings(context, ref);
132             default:
133                 throw new VerifyException("Unexpected first symbol in " + context);
134         }
135     }
136
137     private String concatStrings(final ArgumentContext context, final StatementSourceReference ref) {
138         /*
139          * We have multiple fragments. Just search the tree. This code is equivalent to
140          *
141          *    context.STRING().forEach(stringNode -> appendString(sb, stringNode, ref))
142          *
143          * except we minimize allocations which that would do.
144          */
145         final StringBuilder sb = new StringBuilder();
146         for (ParseTree child : context.children) {
147             verify(child instanceof TerminalNode, "Unexpected fragment component %s", child);
148             final TerminalNode childNode = (TerminalNode) child;
149             switch (childNode.getSymbol().getType()) {
150                 case YangStatementParser.SEP:
151                     // Ignore whitespace
152                     break;
153                 case YangStatementParser.PLUS:
154                     // Operator, which we are handling by concat
155                     break;
156                 case YangStatementParser.STRING:
157                     // a lexer string, could be pretty much anything
158                     // FIXME: YANGTOOLS-1079: appendString() is a dispatch based on quotes, which we should be able to
159                     //                        defer to lexer for a dedicated type. That would expand the switch table
160                     //                        here, but since we have it anyway, it would be nice to have the quoting
161                     //                        distinction already taken care of. The performance difference will need to
162                     //                        be benchmarked, though.
163                     appendString(sb, childNode, ref);
164                     break;
165                 default:
166                     throw new VerifyException("Unexpected symbol in " + childNode);
167             }
168         }
169         return sb.toString();
170     }
171
172     private void appendString(final StringBuilder sb, final TerminalNode stringNode,
173             final StatementSourceReference ref) {
174         final String str = stringNode.getText();
175         final char firstChar = str.charAt(0);
176         final char lastChar = str.charAt(str.length() - 1);
177         if (firstChar == '"' && lastChar == '"') {
178             sb.append(normalizeDoubleQuoted(str.substring(1, str.length() - 1),
179                 stringNode.getSymbol().getCharPositionInLine(), ref));
180         } else if (firstChar == '\'' && lastChar == '\'') {
181             /*
182              * According to RFC6020 a single quote character cannot occur in a single-quoted string, even when preceded
183              * by a backslash.
184              */
185             sb.append(str, 1, str.length() - 1);
186         } else {
187             checkUnquoted(str, ref);
188             sb.append(str);
189         }
190     }
191
192     private String normalizeDoubleQuoted(final String str, final int dquot, final StatementSourceReference ref) {
193         // Whitespace normalization happens irrespective of further handling and has no effect on the result
194         final String stripped = trimWhitespace(str, dquot);
195
196         // Now we need to perform some amount of unescaping. This serves as a pre-check before we dispatch
197         // validation and processing (which will reuse the work we have done)
198         final int backslash = stripped.indexOf('\\');
199         return backslash == -1 ? stripped : unescape(stripped, backslash, ref);
200     }
201
202     /*
203      * NOTE: Enforcement and transformation logic done by these methods should logically reside in the lexer and ANTLR
204      *       account the for it with lexer modes. We do not want to force a re-lexing phase in the parser just because
205      *       we decided to let ANTLR do the work.
206      */
207     // FIXME: YANGTOOLS-1079: Re-evaluate above comment once our integration surface with lexer has been decided
208     abstract void checkDoubleQuoted(String str, StatementSourceReference ref);
209
210     abstract void checkUnquoted(String str, StatementSourceReference ref);
211
212     /*
213      * Unescape escaped double quotes, tabs, new line and backslash in the inner string and trim the result.
214      */
215     private String unescape(final String str, final int backslash, final StatementSourceReference ref) {
216         checkDoubleQuoted(str, ref);
217
218         // FIXME: YANGTOOLS-1079: given we the leading backslash, it would be more efficient to walk the string and
219         //                        unescape in one go
220         return ESCAPED_TAB.matcher(
221                     ESCAPED_LF.matcher(
222                         ESCAPED_BACKSLASH.matcher(
223                             ESCAPED_DQUOT.matcher(str).replaceAll("\\\""))
224                         .replaceAll("\\\\"))
225                     .replaceAll("\\\n"))
226                .replaceAll("\\\t");
227     }
228
229     @VisibleForTesting
230     static String trimWhitespace(final String str, final int dquot) {
231         final int firstBrk = str.indexOf('\n');
232         if (firstBrk == -1) {
233             return str;
234         }
235
236         // Okay, we may need to do some trimming, set up a builder and append the first segment
237         final int length = str.length();
238         final StringBuilder sb = new StringBuilder(length);
239
240         // Append first segment, which needs only tail-trimming
241         sb.append(str, 0, trimTrailing(str, 0, firstBrk)).append('\n');
242
243         // With that out of the way, setup our iteration state. The string segment we are looking at is
244         // str.substring(start, end), which is guaranteed not to include any line breaks, i.e. end <= brk unless we are
245         // at the last segment.
246         int start = firstBrk + 1;
247         int brk = str.indexOf('\n', start);
248
249         // Loop over inner strings
250         while (brk != -1) {
251             trimLeadingAndAppend(sb, dquot, str, start, trimTrailing(str, start, brk)).append('\n');
252             start = brk + 1;
253             brk = str.indexOf('\n', start);
254         }
255
256         return trimLeadingAndAppend(sb, dquot, str, start, length).toString();
257     }
258
259     private static StringBuilder trimLeadingAndAppend(final StringBuilder sb, final int dquot, final String str,
260             final int start, final int end) {
261         int offset = start;
262         int pos = 0;
263
264         while (pos <= dquot) {
265             if (offset == end) {
266                 // We ran out of data, nothing to append
267                 return sb;
268             }
269
270             final char ch = str.charAt(offset);
271             if (ch == '\t') {
272                 // tabs are to be treated as 8 spaces
273                 pos += 8;
274             } else if (WHITESPACE_MATCHER.matches(ch)) {
275                 pos++;
276             } else {
277                 break;
278             }
279
280             offset++;
281         }
282
283         // We have expanded beyond double quotes, push equivalent spaces
284         while (pos - 1 > dquot) {
285             sb.append(' ');
286             pos--;
287         }
288
289         return sb.append(str, offset, end);
290     }
291
292     private static int trimTrailing(final String str, final int start, final int end) {
293         int ret = end;
294         while (ret > start) {
295             final int prev = ret - 1;
296             if (!WHITESPACE_MATCHER.matches(str.charAt(prev))) {
297                 break;
298             }
299             ret = prev;
300         }
301         return ret;
302     }
303 }