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