Correct double-quoted string whitespace trimming
[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 com.google.common.annotations.VisibleForTesting;
11 import com.google.common.base.CharMatcher;
12 import java.util.Collections;
13 import java.util.List;
14 import java.util.regex.Pattern;
15 import org.antlr.v4.runtime.tree.TerminalNode;
16 import org.opendaylight.yangtools.antlrv4.code.gen.YangStatementParser.ArgumentContext;
17 import org.opendaylight.yangtools.yang.common.YangVersion;
18 import org.opendaylight.yangtools.yang.parser.spi.source.SourceException;
19 import org.opendaylight.yangtools.yang.parser.spi.source.StatementSourceReference;
20
21 final class ArgumentContextUtils {
22     private static final CharMatcher WHITESPACE_MATCHER = CharMatcher.whitespace();
23     private static final CharMatcher ANYQUOTE_MATCHER = CharMatcher.anyOf("'\"");
24     private static final Pattern ESCAPED_DQUOT = Pattern.compile("\\\"", Pattern.LITERAL);
25     private static final Pattern ESCAPED_BACKSLASH = Pattern.compile("\\\\", Pattern.LITERAL);
26     private static final Pattern ESCAPED_LF = Pattern.compile("\\n", Pattern.LITERAL);
27     private static final Pattern ESCAPED_TAB = Pattern.compile("\\t", Pattern.LITERAL);
28
29     private ArgumentContextUtils() {
30         throw new UnsupportedOperationException();
31     }
32
33     static String stringFromStringContext(final ArgumentContext context, final StatementSourceReference ref) {
34         return stringFromStringContext(context, YangVersion.VERSION_1, ref);
35     }
36
37     static String stringFromStringContext(final ArgumentContext context, final YangVersion yangVersion,
38             final StatementSourceReference ref) {
39         final StringBuilder sb = new StringBuilder();
40         List<TerminalNode> strings = context.STRING();
41         if (strings.isEmpty()) {
42             strings = Collections.singletonList(context.IDENTIFIER());
43         }
44         for (final TerminalNode stringNode : strings) {
45             final String str = stringNode.getText();
46             final char firstChar = str.charAt(0);
47             final char lastChar = str.charAt(str.length() - 1);
48             if (firstChar == '"' && lastChar == '"') {
49                 final String innerStr = str.substring(1, str.length() - 1);
50                 /*
51                  * Unescape escaped double quotes, tabs, new line and backslash
52                  * in the inner string and trim the result.
53                  */
54                 checkDoubleQuotedString(innerStr, yangVersion, ref);
55
56                 sb.append(ESCAPED_TAB.matcher(
57                     ESCAPED_LF.matcher(
58                         ESCAPED_BACKSLASH.matcher(
59                             ESCAPED_DQUOT.matcher(
60                                 trimWhitespace(innerStr, stringNode.getSymbol().getCharPositionInLine()))
61                             .replaceAll("\\\""))
62                         .replaceAll("\\\\"))
63                     .replaceAll("\\\n"))
64                     .replaceAll("\\\t"));
65             } else if (firstChar == '\'' && lastChar == '\'') {
66                 /*
67                  * According to RFC6020 a single quote character cannot occur in
68                  * a single-quoted string, even when preceded by a backslash.
69                  */
70                 sb.append(str.substring(1, str.length() - 1));
71             } else {
72                 checkUnquotedString(str, yangVersion, ref);
73                 sb.append(str);
74             }
75         }
76         return sb.toString();
77     }
78
79     private static void checkUnquotedString(final String str, final YangVersion yangVersion,
80             final StatementSourceReference ref) {
81         if (yangVersion == YangVersion.VERSION_1_1) {
82             SourceException.throwIf(ANYQUOTE_MATCHER.matchesAnyOf(str), ref,
83                 "YANG 1.1: unquoted string (%s) contains illegal characters", str);
84         }
85     }
86
87     private static void checkDoubleQuotedString(final String str, final YangVersion yangVersion,
88             final StatementSourceReference ref) {
89         if (yangVersion == YangVersion.VERSION_1_1) {
90             for (int i = 0; i < str.length() - 1; i++) {
91                 if (str.charAt(i) == '\\') {
92                     switch (str.charAt(i + 1)) {
93                         case 'n':
94                         case 't':
95                         case '\\':
96                         case '\"':
97                             i++;
98                             break;
99                         default:
100                             throw new SourceException(ref, "YANG 1.1: illegal double quoted string (%s). In double "
101                                     + "quoted string the backslash must be followed by one of the following character "
102                                     + "[n,t,\",\\], but was '%s'.", str, str.charAt(i + 1));
103                     }
104                 }
105             }
106         }
107     }
108
109     @VisibleForTesting
110     static String trimWhitespace(final String str, final int dquot) {
111         int brk = str.indexOf('\n');
112         if (brk == -1) {
113             // No need to trim whitespace
114             return str;
115         }
116
117         // Okay, we may need to do some trimming, set up a builder and append the first segment
118         final int length = str.length();
119         final StringBuilder sb = new StringBuilder(length);
120
121         // Append first segment, which needs only tail-trimming
122         sb.append(str, 0, trimTrailing(str, 0, brk)).append('\n');
123
124         // With that out of the way, setup our iteration state. The string segment we are looking at is
125         // str.substring(start, end), which is guaranteed not to include any line breaks, i.e. end <= brk unless we are
126         // at the last segment.
127         int start = brk + 1;
128         brk = str.indexOf('\n', start);
129
130         // Loop over inner strings
131         while (brk != -1) {
132             final int end = brk != -1 ? brk : length;
133             trimLeadingAndAppend(sb, dquot, str, start, trimTrailing(str, start, end)).append('\n');
134             start = end + 1;
135             brk = str.indexOf('\n', start);
136         }
137
138         return trimLeadingAndAppend(sb, dquot, str, start, length).toString();
139     }
140
141     private static StringBuilder trimLeadingAndAppend(final StringBuilder sb, final int dquot, final String str,
142             final int start, final int end) {
143         int offset = start;
144         int pos = 0;
145
146         while (pos <= dquot) {
147             if (offset == end) {
148                 // We ran out of data, nothing to append
149                 return sb;
150             }
151
152             final char ch = str.charAt(offset);
153             if (ch == '\t') {
154                 // tabs are to be treated as 8 spaces
155                 pos += 8;
156             } else if (WHITESPACE_MATCHER.matches(ch)) {
157                 pos++;
158             } else {
159                 break;
160             }
161
162             offset++;
163         }
164
165         // We have expanded beyond double quotes, push equivalent spaces
166         while (pos - 1 > dquot) {
167             sb.append(' ');
168             pos--;
169         }
170
171         return sb.append(str, offset, end);
172     }
173
174     private static int trimTrailing(final String str, final int start, final int end) {
175         int ret = end;
176         while (ret > start) {
177             final int prev = ret - 1;
178             if (!WHITESPACE_MATCHER.matches(str.charAt(prev))) {
179                 break;
180             }
181             ret = prev;
182         }
183         return ret;
184     }
185 }