1 /*
2  * Copyright (c) 2018, 2020, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.
8  *
9  * This code is distributed in the hope that it will be useful, but WITHOUT
10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12  * version 2 for more details (a copy is included in the LICENSE file that
13  * accompanied this code).
14  *
15  * You should have received a copy of the GNU General Public License version
16  * 2 along with this work; if not, write to the Free Software Foundation,
17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18  *
19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20  * or visit www.oracle.com if you need additional information or have any
21  * questions.
22  */
23 package build.tools.pandocfilter.json;
24 
25 import java.util.*;
26 
27 class JSONParser {
28     private int pos = 0;
29     private String input;
30 
JSONParser()31     JSONParser() {
32     }
33 
failure(String message)34     private IllegalStateException failure(String message) {
35         return new IllegalStateException(String.format("[%d]: %s : %s", pos, message, input));
36     }
37 
current()38     private char current() {
39         return input.charAt(pos);
40     }
41 
advance()42     private void advance() {
43         pos++;
44     }
45 
hasInput()46     private boolean hasInput() {
47         return pos < input.length();
48     }
49 
expectMoreInput(String message)50     private void expectMoreInput(String message) {
51         if (!hasInput()) {
52             throw failure(message);
53         }
54     }
55 
next(String message)56     private char next(String message) {
57         advance();
58         if (!hasInput()) {
59             throw failure(message);
60         }
61         return current();
62     }
63 
64 
expect(char c)65     private void expect(char c) {
66         var msg = String.format("Expected character %c", c);
67 
68         var n = next(msg);
69         if (n != c) {
70             throw failure(msg);
71         }
72     }
73 
assume(char c, String message)74     private void assume(char c, String message) {
75         expectMoreInput(message);
76         if (current() != c) {
77             throw failure(message);
78         }
79     }
80 
parseBoolean()81     private JSONBoolean parseBoolean() {
82         if (current() == 't') {
83             expect('r');
84             expect('u');
85             expect('e');
86             advance();
87             return new JSONBoolean(true);
88         }
89 
90         if (current() == 'f') {
91             expect('a');
92             expect('l');
93             expect('s');
94             expect('e');
95             advance();
96             return new JSONBoolean(false);
97         }
98 
99         throw failure("a boolean can only be 'true' or 'false'");
100     }
101 
parseNumber()102     private JSONValue parseNumber() {
103         var isInteger = true;
104         var builder = new StringBuilder();
105 
106         if (current() == '-') {
107             builder.append(current());
108             advance();
109             expectMoreInput("a number cannot consist of only '-'");
110         }
111 
112         if (current() == '0') {
113             builder.append(current());
114             advance();
115 
116             if (hasInput() && current() == '.') {
117                 isInteger = false;
118                 builder.append(current());
119                 advance();
120 
121                 expectMoreInput("a number cannot end with '.'");
122 
123                 if (!isDigit(current())) {
124                     throw failure("must be at least one digit after '.'");
125                 }
126 
127                 while (hasInput() && isDigit(current())) {
128                     builder.append(current());
129                     advance();
130                 }
131             }
132         } else {
133             while (hasInput() && isDigit(current())) {
134                 builder.append(current());
135                 advance();
136             }
137 
138             if (hasInput() && current() == '.') {
139                 isInteger = false;
140                 builder.append(current());
141                 advance();
142 
143                 expectMoreInput("a number cannot end with '.'");
144 
145                 if (!isDigit(current())) {
146                     throw failure("must be at least one digit after '.'");
147                 }
148 
149                 while (hasInput() && isDigit(current())) {
150                     builder.append(current());
151                     advance();
152                 }
153             }
154         }
155 
156         if (hasInput() && (current() == 'e' || current() == 'E')) {
157             isInteger = false;
158 
159             builder.append(current());
160             advance();
161             expectMoreInput("a number cannot end with 'e' or 'E'");
162 
163             if (current() == '+' || current() == '-') {
164                 builder.append(current());
165                 advance();
166             }
167 
168             if (!isDigit(current())) {
169                 throw failure("a digit must follow {'e','E'}{'+','-'}");
170             }
171 
172             while (hasInput() && isDigit(current())) {
173                 builder.append(current());
174                 advance();
175             }
176         }
177 
178         var value = builder.toString();
179         return isInteger ? new JSONNumber(Long.parseLong(value)) :
180                            new JSONDecimal(Double.parseDouble(value));
181 
182     }
183 
parseString()184     private JSONString parseString() {
185         var missingEndChar = "string is not terminated with '\"'";
186         var builder = new StringBuilder();
187         for (var c = next(missingEndChar); c != '"'; c = next(missingEndChar)) {
188             if (c == '\\') {
189                 var n = next(missingEndChar);
190                 switch (n) {
191                     case '"':
192                         builder.append("\"");
193                         break;
194                     case '\\':
195                         builder.append("\\");
196                         break;
197                     case '/':
198                         builder.append("/");
199                         break;
200                     case 'b':
201                         builder.append("\b");
202                         break;
203                     case 'f':
204                         builder.append("\f");
205                         break;
206                     case 'n':
207                         builder.append("\n");
208                         break;
209                     case 'r':
210                         builder.append("\r");
211                         break;
212                     case 't':
213                         builder.append("\t");
214                         break;
215                     case 'u':
216                         var u1 = next(missingEndChar);
217                         var u2 = next(missingEndChar);
218                         var u3 = next(missingEndChar);
219                         var u4 = next(missingEndChar);
220                         var cp = Integer.parseInt(String.format("%c%c%c%c", u1, u2, u3, u4), 16);
221                         builder.append(new String(new int[]{cp}, 0, 1));
222                         break;
223                     default:
224                         throw failure(String.format("Unexpected escaped character '%c'", n));
225                 }
226             } else {
227                 builder.append(c);
228             }
229         }
230 
231         advance(); // step beyond closing "
232         return new JSONString(builder.toString());
233     }
234 
parseArray()235     private JSONArray parseArray() {
236         var error = "array is not terminated with ']'";
237         var list = new ArrayList<JSONValue>();
238 
239         advance(); // step beyond opening '['
240         consumeWhitespace();
241         expectMoreInput(error);
242 
243         while (current() != ']') {
244             var val = parseValue();
245             list.add(val);
246 
247             expectMoreInput(error);
248             if (current() == ',') {
249                 advance();
250             }
251             expectMoreInput(error);
252         }
253 
254         advance(); // step beyond closing ']'
255         return new JSONArray(list.toArray(new JSONValue[0]));
256     }
257 
parseNull()258     public JSONNull parseNull() {
259         expect('u');
260         expect('l');
261         expect('l');
262         advance();
263         return new JSONNull();
264     }
265 
parseObject()266     public JSONObject parseObject() {
267         var error = "object is not terminated with '}'";
268         var map = new HashMap<String, JSONValue>();
269 
270         advance(); // step beyond opening '{'
271         consumeWhitespace();
272         expectMoreInput(error);
273 
274         while (current() != '}') {
275             var key = parseValue();
276             if (!(key instanceof JSONString)) {
277                 throw failure("a field must of type string");
278             }
279 
280             if (!hasInput() || current() != ':') {
281                 throw failure("a field must be followed by ':'");
282             }
283             advance(); // skip ':'
284 
285             var val = parseValue();
286             map.put(key.asString(), val);
287 
288             expectMoreInput(error);
289             if (current() == ',') {
290                 advance();
291             }
292             expectMoreInput(error);
293         }
294 
295         advance(); // step beyond '}'
296         return new JSONObject(map);
297     }
298 
isDigit(char c)299     private boolean isDigit(char c) {
300         return c == '0' ||
301                c == '1' ||
302                c == '2' ||
303                c == '3' ||
304                c == '4' ||
305                c == '5' ||
306                c == '6' ||
307                c == '7' ||
308                c == '8' ||
309                c == '9';
310     }
311 
isStartOfNumber(char c)312     private boolean isStartOfNumber(char c) {
313         return isDigit(c) || c == '-';
314     }
315 
isStartOfString(char c)316     private boolean isStartOfString(char c) {
317         return c == '"';
318     }
319 
isStartOfBoolean(char c)320     private boolean isStartOfBoolean(char c) {
321         return c == 't' || c == 'f';
322     }
323 
isStartOfArray(char c)324     private boolean isStartOfArray(char c) {
325         return c == '[';
326     }
327 
isStartOfNull(char c)328     private boolean isStartOfNull(char c) {
329         return c == 'n';
330     }
331 
isWhitespace(char c)332     private boolean isWhitespace(char c) {
333         return c == '\r' ||
334                c == '\n' ||
335                c == '\t' ||
336                c == ' ';
337     }
338 
isStartOfObject(char c)339     private boolean isStartOfObject(char c) {
340         return c == '{';
341     }
342 
consumeWhitespace()343     private void consumeWhitespace() {
344         while (hasInput() && isWhitespace(current())) {
345             advance();
346         }
347     }
348 
parseValue()349     public JSONValue parseValue() {
350         JSONValue ret = null;
351 
352         consumeWhitespace();
353         if (hasInput()) {
354             var c = current();
355 
356             if (isStartOfNumber(c)) {
357                 ret = parseNumber();
358             } else if (isStartOfString(c)) {
359                 ret = parseString();
360             } else if (isStartOfBoolean(c)) {
361                 ret = parseBoolean();
362             } else if (isStartOfArray(c)) {
363                 ret = parseArray();
364             } else if (isStartOfNull(c)) {
365                 ret = parseNull();
366             } else if (isStartOfObject(c)) {
367                 ret = parseObject();
368             } else {
369                 throw failure("not a valid start of a JSON value");
370             }
371         }
372         consumeWhitespace();
373 
374         return ret;
375     }
376 
parse(String s)377     public JSONValue parse(String s) {
378         if (s == null || s.equals("")) {
379             return null;
380         }
381 
382         pos = 0;
383         input = s;
384 
385         var result = parseValue();
386         if (hasInput()) {
387             throw failure("can only have one top-level JSON value");
388         }
389         return result;
390     }
391 }
392