001/* 002 * ModeShape (http://www.modeshape.org) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.modeshape.schematic.internal.document; 017 018import static org.modeshape.schematic.document.Json.ReservedField.BASE_64; 019import static org.modeshape.schematic.document.Json.ReservedField.BINARY_TYPE; 020import static org.modeshape.schematic.document.Json.ReservedField.CODE; 021import static org.modeshape.schematic.document.Json.ReservedField.DATE; 022import static org.modeshape.schematic.document.Json.ReservedField.INCREMENT; 023import static org.modeshape.schematic.document.Json.ReservedField.OBJECT_ID; 024import static org.modeshape.schematic.document.Json.ReservedField.REGEX_OPTIONS; 025import static org.modeshape.schematic.document.Json.ReservedField.REGEX_PATTERN; 026import static org.modeshape.schematic.document.Json.ReservedField.SCOPE; 027import static org.modeshape.schematic.document.Json.ReservedField.TIMESTAMP; 028import static org.modeshape.schematic.document.Json.ReservedField.UUID; 029import java.io.IOException; 030import java.io.InputStream; 031import java.io.InputStreamReader; 032import java.io.Reader; 033import java.io.StringReader; 034import java.net.URL; 035import java.nio.charset.Charset; 036import java.text.CharacterIterator; 037import java.text.ParseException; 038import java.text.StringCharacterIterator; 039import java.util.Date; 040import java.util.Iterator; 041import java.util.LinkedList; 042import java.util.List; 043import java.util.concurrent.atomic.AtomicBoolean; 044import org.modeshape.schematic.Base64; 045import org.modeshape.schematic.annotation.Immutable; 046import org.modeshape.schematic.annotation.NotThreadSafe; 047import org.modeshape.schematic.annotation.ThreadSafe; 048import org.modeshape.schematic.document.Bson; 049import org.modeshape.schematic.document.Document; 050import org.modeshape.schematic.document.DocumentSequence; 051import org.modeshape.schematic.document.Json; 052import org.modeshape.schematic.document.Null; 053import org.modeshape.schematic.document.ParsingException; 054 055/** 056 * A class that reads the <a href="http://www.json.org/">JSON</a> data format and constructs an in-memory <a 057 * href="http://bsonspec.org/">BSON</a> representation. 058 * <p> 059 * This reader is capable of optionally introspecting string values to look for certain string patterns that are commonly used to 060 * represent dates. In introspection is not done by default, but when it is used it looks for the following patterns: 061 * <ul> 062 * <li>a string literal date of the form <code>"<i>yyyy</i>-<i>MM</i>-<i>dd</i>T<i>HH</i>:<i>mm</i>:<i>ss</i>"</code> where 063 * <code>T</code> is a literal character</li> 064 * <li>a string literal date of the form <code>"<i>yyyy</i>-<i>MM</i>-<i>dd</i>T<i>HH</i>:<i>mm</i>:<i>ss</i>Z"</code> where 065 * <code>T</code> and <code>Z</code> are literal characters</li> 066 * <li>a string literal date of the form 067 * <code>"<i>yyyy</i>-<i>MM</i>-<i>dd</i>T<i>HH</i>:<i>mm</i>:<i>ss</i>GMT+<i>00</i>:<i>00</i>"</code> where <code>T</code>, and 068 * <code>GMT</code> are literal characters</li> 069 * <li>a string literal date of the form <code>"/Date(<i>millisOrIso</i>)/"</code></li> 070 * <li>a string literal date of the form <code>"\/Date(<i>millisOrIso</i>)\/"</code></li> 071 * </ul> 072 * Note that in the date forms listed above, <code><i>millisOrIso</i></code> is either a long value representing the number of 073 * milliseconds since epoch or a string literal in ISO-8601 format representing a date and time. 074 * </p> 075 * <p> 076 * This reader also accepts non-string values that are function calls of the form 077 * 078 * <pre> 079 * new <i>functionName</i>(<i>parameters</i>) 080 * </pre> 081 * 082 * where <code><i>parameters</i></code> consists of one or more JSON values (including nested functions). If the function call 083 * cannot be parsed and executed, the string literal form of the function call is kept. 084 * </p> 085 * 086 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc. 087 * @since 5.1 088 */ 089@ThreadSafe 090@Immutable 091public class JsonReader { 092 093 protected static final DocumentValueFactory VALUE_FACTORY = new DefaultDocumentValueFactory(); 094 protected static final ValueMatcher SIMPLE_VALUE_MATCHER = new SimpleValueMatcher(VALUE_FACTORY); 095 protected static final ValueMatcher DATE_VALUE_MATCHER = new DateValueMatcher(VALUE_FACTORY); 096 097 public static final boolean DEFAULT_INTROSPECT = true; 098 099 /** 100 * Read the JSON representation from supplied URL and construct the {@link Document} representation, using the 101 * {@link Charset#defaultCharset() default character set}. 102 * 103 * @param url the URL to the JSON document; may not be null and must be resolvable 104 * @return the in-memory {@link Document} representation 105 * @throws ParsingException if there was a problem reading from the URL 106 */ 107 public Document read( URL url ) throws ParsingException { 108 try { 109 return read(url.openStream(), DEFAULT_INTROSPECT); 110 } catch (IOException e) { 111 throw new ParsingException(e.getMessage(), e, 0, 0); 112 } 113 } 114 115 /** 116 * Read the JSON representation from supplied input stream and construct the {@link Document} representation, using the 117 * {@link Charset#defaultCharset() default character set}. 118 * 119 * @param stream the input stream; may not be null 120 * @return the in-memory {@link Document} representation 121 * @throws ParsingException if there was a problem reading from the stream 122 */ 123 public Document read( InputStream stream ) throws ParsingException { 124 return read(stream, DEFAULT_INTROSPECT); 125 } 126 127 /** 128 * Read the JSON representation from supplied input stream and construct the {@link Document} representation, using the 129 * supplied {@link Charset character set}. 130 * 131 * @param stream the input stream; may not be null 132 * @param charset the character set that should be used; may not be null 133 * @return the in-memory {@link Document} representation 134 * @throws ParsingException if there was a problem reading from the stream 135 */ 136 public Document read( InputStream stream, 137 Charset charset ) throws ParsingException { 138 return read(stream, charset, DEFAULT_INTROSPECT); 139 } 140 141 /** 142 * Read the JSON representation from supplied input stream and construct the {@link Document} representation. 143 * 144 * @param reader the IO reader; may not be null 145 * @return the in-memory {@link Document} representation 146 * @throws ParsingException if there was a problem reading from the stream 147 */ 148 public Document read( Reader reader ) throws ParsingException { 149 return read(reader, DEFAULT_INTROSPECT); 150 } 151 152 /** 153 * Read the JSON representation from supplied string and construct the {@link Document} representation. 154 * 155 * @param json the JSON representation; may not be null 156 * @return the in-memory {@link Document} representation 157 * @throws ParsingException if there was a problem reading from the stream 158 */ 159 public Document read( String json ) throws ParsingException { 160 return read(json, DEFAULT_INTROSPECT); 161 } 162 163 /** 164 * Read the JSON representation from supplied input stream and construct the {@link Document} representation, using the 165 * {@link Charset#defaultCharset() default character set}. 166 * 167 * @param stream the input stream; may not be null 168 * @param introspectStringValues true if the string values should be examined for common patterns, or false otherwise 169 * @return the in-memory {@link Document} representation 170 * @throws ParsingException if there was a problem reading from the stream 171 */ 172 public Document read( InputStream stream, 173 boolean introspectStringValues ) throws ParsingException { 174 return read(stream, Json.UTF8, introspectStringValues); 175 } 176 177 /** 178 * Read the JSON representation from supplied input stream and construct the {@link Document} representation, using the 179 * supplied {@link Charset character set}. 180 * 181 * @param stream the input stream; may not be null 182 * @param charset the character set that should be used; may not be null 183 * @param introspectStringValues true if the string values should be examined for common patterns, or false otherwise 184 * @return the in-memory {@link Document} representation 185 * @throws ParsingException if there was a problem reading from the stream 186 */ 187 public Document read( InputStream stream, 188 Charset charset, 189 boolean introspectStringValues ) throws ParsingException { 190 return read(new InputStreamReader(stream, charset), introspectStringValues); 191 } 192 193 /** 194 * Read the JSON representation from supplied input stream and construct the {@link Document} representation. 195 * 196 * @param reader the IO reader; may not be null 197 * @param introspectStringValues true if the string values should be examined for common patterns, or false otherwise 198 * @return the in-memory {@link Document} representation 199 * @throws ParsingException if there was a problem reading from the stream 200 */ 201 public Document read( Reader reader, 202 boolean introspectStringValues ) throws ParsingException { 203 // Create an object so that this reader is thread safe ... 204 ValueMatcher matcher = introspectStringValues ? DATE_VALUE_MATCHER : SIMPLE_VALUE_MATCHER; 205 return new Parser(new Tokenizer(reader), VALUE_FACTORY, matcher).parseDocument(); 206 } 207 208 /** 209 * Read the JSON representation from supplied string and construct the {@link Document} representation. 210 * 211 * @param json the JSON representation; may not be null 212 * @param introspectStringValues true if the string values should be examined for common patterns, or false otherwise 213 * @return the in-memory {@link Document} representation 214 * @throws ParsingException if there was a problem reading from the stream 215 */ 216 public Document read( String json, 217 boolean introspectStringValues ) throws ParsingException { 218 return read(new StringReader(json), introspectStringValues); 219 } 220 221 /** 222 * Return a {@link DocumentSequence} that can be used to pull multiple documents from the stream. 223 * 224 * @param stream the input stream; may not be null 225 * @return the sequence that can be used to get one or more Document instances from a single input 226 */ 227 public DocumentSequence readMultiple( InputStream stream ) { 228 return readMultiple(stream, DEFAULT_INTROSPECT); 229 } 230 231 /** 232 * Return a {@link DocumentSequence} that can be used to pull multiple documents from the stream. 233 * 234 * @param stream the input stream; may not be null 235 * @param introspectStringValues true if the string values should be examined for common patterns, or false otherwise 236 * @return the sequence that can be used to get one or more Document instances from a single input 237 */ 238 public DocumentSequence readMultiple( InputStream stream, 239 boolean introspectStringValues ) { 240 return readMultiple(new InputStreamReader(stream, Json.UTF8), introspectStringValues); 241 } 242 243 /** 244 * Return a {@link DocumentSequence} that can be used to pull multiple documents from the stream. 245 * 246 * @param reader the IO reader; may not be null 247 * @return the sequence that can be used to get one or more Document instances from a single input 248 */ 249 public DocumentSequence readMultiple( Reader reader ) { 250 return readMultiple(reader, DEFAULT_INTROSPECT); 251 } 252 253 /** 254 * Return a {@link DocumentSequence} that can be used to pull multiple documents from the stream. 255 * 256 * @param reader the IO reader; may not be null 257 * @param introspectStringValues true if the string values should be examined for common patterns, or false otherwise 258 * @return the sequence that can be used to get one or more Document instances from a single input 259 */ 260 public DocumentSequence readMultiple( Reader reader, 261 boolean introspectStringValues ) { 262 // Create an object so that this reader is thread safe ... 263 final Tokenizer tokenizer = new Tokenizer(reader); 264 ValueMatcher matcher = introspectStringValues ? DATE_VALUE_MATCHER : SIMPLE_VALUE_MATCHER; 265 final Parser parser = new Parser(tokenizer, VALUE_FACTORY, matcher); 266 return () -> { 267 if (tokenizer.isFinished()) return null; 268 Document doc = parser.parseDocument(false); 269 // System.out.println(Json.writePretty(doc)); 270 return doc; 271 }; 272 } 273 274 /** 275 * Parse the number represented by the supplied (unquoted) JSON field value. 276 * 277 * @param value the string representation of the value 278 * @return the number, or null if the value could not be parsed 279 */ 280 public static Number parseNumber( String value ) { 281 // Try to parse as a number ... 282 char c = value.charAt(0); 283 284 if ((c >= '0' && c <= '9') || c == '.' || c == '-' || c == '+') { 285 // It's definitely a number ... 286 if (c == '0' && value.length() > 2) { 287 // it might be a hex number that starts with '0x' 288 char two = value.charAt(1); 289 if (two == 'x' || two == 'X') { 290 try { 291 // Parse the remainder of the hex number ... 292 return Integer.parseInt(value.substring(2), 16); 293 } catch (NumberFormatException e) { 294 // Ignore and continue ... 295 } 296 } 297 } 298 // Try parsing as a double ... 299 try { 300 if ((value.indexOf('.') > -1) || (value.indexOf('E') > -1) || (value.indexOf('e') > -1)) { 301 return Double.parseDouble(value); 302 } 303 Long longObj = new Long(value); 304 long longValue = longObj.longValue(); 305 int intValue = longObj.intValue(); 306 if (longValue == intValue) { 307 // Then it's just an integer ... 308 return new Integer(intValue); 309 } 310 return longObj; 311 } catch (NumberFormatException e) { 312 // ignore ... 313 } 314 } 315 return null; 316 } 317 318 /** 319 * The component that parses a tokenized JSON stream. 320 * 321 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc. 322 * @since 5.1 323 */ 324 @NotThreadSafe 325 public static class Parser { 326 327 private final DocumentValueFactory values; 328 private final Tokenizer tokens; 329 private final ValueMatcher valueMatcher; 330 331 /** 332 * Create a new JsonReader that uses the supplied {@link Tokenizer} instance. 333 * 334 * @param tokenizer the tokenizer that tokenizes the stream of JSON content; may not be null 335 * @param values the factory for creating value objects; may not be null 336 * @param valueMatcher the component that looks for patterns within string values to create alternative objects; may not 337 * be null 338 */ 339 public Parser( Tokenizer tokenizer, 340 DocumentValueFactory values, 341 ValueMatcher valueMatcher ) { 342 this.tokens = tokenizer; 343 this.values = values; 344 this.valueMatcher = valueMatcher; 345 } 346 347 /** 348 * Parse the stream for the next JSON document. 349 * 350 * @return the document, or null if there are no more documents 351 * @throws ParsingException if there is a problem parsing the value 352 */ 353 public Document parseDocument() throws ParsingException { 354 return parseDocument(null, true); 355 } 356 357 /** 358 * Parse the stream for the next JSON document. 359 * 360 * @param failIfNotValidDocument true if this method should throw an exception if the stream does not contain a valid 361 * document, or false if null should be returned if there is no valid document on the stream 362 * @return the document, or null if there are no more documents 363 * @throws ParsingException if there is a problem parsing the value 364 */ 365 public Document parseDocument( boolean failIfNotValidDocument ) throws ParsingException { 366 return parseDocument(null, failIfNotValidDocument); 367 } 368 369 protected BasicDocument newDocument() { 370 return new BasicDocument(); 371 } 372 373 /** 374 * Parse the stream for the next JSON document. 375 * 376 * @param hasReservedFieldNames the flag that should be set if this document contains field names that are reserved 377 * @param failIfNotValidDocument true if this method should throw an exception if the stream does not contain a valid 378 * document, or false if null should be returned if there is no valid document on the stream 379 * @return the document, or null if there are no more documents 380 * @throws ParsingException if there is a problem parsing the value 381 */ 382 protected Document parseDocument( AtomicBoolean hasReservedFieldNames, 383 boolean failIfNotValidDocument ) throws ParsingException { 384 if (tokens.nextUsefulChar() != '{') { 385 if (failIfNotValidDocument) { 386 throw tokens.error("JSON documents must begin with a '{' character"); 387 } 388 // otherwise just return ... 389 return null; 390 } 391 BasicDocument doc = newDocument(); 392 do { 393 String fieldName = null; 394 // Peek at the next character on the stream ... 395 switch (tokens.peek()) { 396 case 0: 397 throw tokens.error("JSON documents must end with a '}' character"); 398 case '}': 399 tokens.next(); 400 return doc; 401 default: 402 // This should be a field name, so read it ... 403 fieldName = tokens.nextString(); 404 break; 405 } 406 // Now look for any of the following delimiters: ':', "->", or "=>" 407 tokens.nextFieldDelim(); 408 409 // Now look for a value ... 410 Object value = parseValue(); 411 doc.put(fieldName, value); 412 413 // Determine if this field is a reserved 414 if (hasReservedFieldNames != null && isReservedFieldName(fieldName)) { 415 hasReservedFieldNames.set(true); 416 } 417 418 // Look for the delimiter between fields ... 419 if (tokens.nextDocumentDelim()) return doc; 420 } while (true); 421 } 422 423 protected final boolean isReservedFieldName( String fieldName ) { 424 return fieldName.length() != 0 && fieldName.charAt(0) == '$'; 425 } 426 427 /** 428 * Parse the JSON array on the stream, beginning with the '[' character until the ']' character, which is consumed. 429 * 430 * @return the array representation; never null but possibly an empty array 431 * @throws ParsingException if there is a problem parsing the value 432 */ 433 public BasicArray parseArray() throws ParsingException { 434 if (tokens.nextUsefulChar() != '[') { 435 throw tokens.error("JSON arrays must begin with a '[' character"); 436 } 437 BasicArray array = new BasicArray(); 438 boolean expectValueSeparator = false; 439 do { 440 // Peek at the next character on the stream ... 441 char c = tokens.peek(); 442 switch (c) { 443 case 0: 444 throw tokens.error("JSON arrays must end with a ']' character"); 445 case ']': 446 tokens.next(); 447 return array; 448 case ',': 449 tokens.next(); 450 expectValueSeparator = false; 451 break; 452 default: 453 if (expectValueSeparator) { 454 throw tokens.error("Invalid character in JSON array: '" + c + "' at line " + tokens.lineNumber() + 455 " column " + tokens.columnNumber()); 456 } 457 // This should be a value .. 458 Object value = parseValue(); 459 array.addValue(value); 460 expectValueSeparator = true; 461 break; 462 } 463 } while (true); 464 } 465 466 /** 467 * Parse the stream for the next field value, which can be one of the following values: 468 * <ul> 469 * <li>a nested document</li> 470 * <li>an array of values</li> 471 * <li>a string literal, surrounded by single-quote characters</li> 472 * <li>a string literal, surrounded by double-quote characters</li> 473 * <li>a string literal date of the form <code>"<i>yyyy</i>-<i>MM</i>-<i>dd</i>T<i>HH</i>:<i>mm</i>:<i>ss</i>"</code> 474 * where <code>T</code> is a literal character</li> 475 * <li>a string literal date of the form <code>"<i>yyyy</i>-<i>MM</i>-<i>dd</i>T<i>HH</i>:<i>mm</i>:<i>ss</i>Z"</code> 476 * where <code>T</code> and <code>Z</code> are literal characters</li> 477 * <li>a string literal date of the form 478 * <code>"<i>yyyy</i>-<i>MM</i>-<i>dd</i>T<i>HH</i>:<i>mm</i>:<i>ss</i>GMT+<i>00</i>:<i>00</i>"</code> where 479 * <code>T</code>, and <code>GMT</code> are literal characters</li> 480 * <li>a string literal date of the form <code>"/Date(<i>millisOrIso</i>)/"</code></li> 481 * <li>a string literal date of the form <code>"\/Date(<i>millisOrIso</i>)\/"</code></li> 482 * <li>a date literal of the form <code>new Date(<i>millisOrIso</i>)</code></li> 483 * <li>a date literal of the form <code>Date(<i>millisOrIso</i>)</code></li> 484 * <li>a function of the form <code>new <i>functionName</i>(<i>parameters</i>)</code> where <code><i>parameters</i></code> 485 * consists of one or more values as parsed by this method 486 * </ul> 487 * Note that in the date forms listed above, <code><i>millisOrIso</i></code> is either a long value representing the 488 * number of milliseconds since epoch or a string literal in ISO-8601 format representing a date and time. 489 * 490 * @return the field value 491 * @throws ParsingException if there is a problem parsing the value 492 */ 493 public Object parseValue() throws ParsingException { 494 char c = tokens.peek(); 495 switch (c) { 496 case 0: 497 // There's nothing left ... 498 return null; 499 case '{': 500 // Nested object ... 501 AtomicBoolean hasReservedFieldNames = new AtomicBoolean(); 502 Document doc = parseDocument(hasReservedFieldNames, true); 503 if (!hasReservedFieldNames.get()) { 504 return doc; 505 } 506 // Convert the doc with reserved field names ... 507 return processDocumentWithReservedFieldNames(doc); 508 case '[': 509 // Nested array ... 510 return parseArray(); 511 case '"': 512 case '\'': 513 String literal = tokens.nextString(); 514 Object value = valueMatcher.parseValue(literal); 515 return value != null ? value : literal; 516 case 'd': 517 case 'n': 518 String newToken = tokens.nextWord(); // read the 'new' token 519 if ("new".equalsIgnoreCase(newToken) || "date".equalsIgnoreCase(newToken)) { 520 return parseFunction(); 521 } 522 break; 523 } 524 525 // Looks like it's a number, so try that ... 526 String number = tokens.nextNumber(); 527 return number != null ? parseValue(number, tokens.lineNumber(), tokens.columnNumber()) : number; 528 } 529 530 /** 531 * Parse the value given by the supplied string located at the supplied line and column numbers. This method looks for 532 * known constant values, then attempts to parse the value as a number, and then calls 533 * {@link #parseUnknownValue(String, int, int)}. 534 * 535 * @param value the string representation of the value 536 * @param lineNumber the line number for the beginning of the value 537 * @param columnNumber the column number for the beginning of the value 538 * @return the value 539 * @throws ParsingException if there is a problem parsing the value 540 */ 541 public Object parseValue( String value, 542 int lineNumber, 543 int columnNumber ) throws ParsingException { 544 if (value.length() == 0) return value; 545 if ("true".equalsIgnoreCase(value)) return Boolean.TRUE; 546 if ("false".equalsIgnoreCase(value)) return Boolean.FALSE; 547 if ("null".equalsIgnoreCase(value)) return Null.getInstance(); 548 549 // Try to parse as a number ... 550 Number number = parseNumber(value); 551 if (number != null) return number; 552 553 return parseUnknownValue(value, lineNumber, columnNumber); 554 } 555 556 /** 557 * Parse the number represented by the supplied value. This method is called by the {@link #parseValue(String, int, int)} 558 * method. 559 * 560 * @param value the string representation of the value 561 * @return the number, or null if the value could not be parsed 562 */ 563 protected Number parseNumber( String value ) { 564 return JsonReader.parseNumber(value); 565 } 566 567 /** 568 * Override this method if custom value types are expected. 569 * 570 * @param value the string representation of the value 571 * @param lineNumber the line number at which the value starts 572 * @param columnNumber the column number at which the value starts 573 * @return the value 574 * @throws ParsingException if there is a problem parsing the value 575 */ 576 protected Object parseUnknownValue( String value, 577 int lineNumber, 578 int columnNumber ) throws ParsingException { 579 return value; 580 } 581 582 /** 583 * Parse a function call on the stream. The 'new' keyword has already been processed. 584 * 585 * @return the result of the evaluation of the function 586 * @throws ParsingException if there is a problem parsing the value 587 */ 588 public Object parseFunction() throws ParsingException { 589 // Parse the function name ... 590 int line = tokens.lineNumber(); 591 int col = tokens.columnNumber(); 592 String functionName = tokens.nextString(); 593 FunctionCall function = new FunctionCall(functionName, line, col); 594 595 // Read the open parenthesis .. 596 char c = tokens.nextUsefulChar(); 597 if (c != '(') { 598 throw tokens.error("Expected '(' after function name \"" + functionName + "\" and at line " + tokens.lineNumber() 599 + ", column " + tokens.columnNumber()); 600 } 601 602 // Read the parameters ... 603 do { 604 line = tokens.lineNumber(); 605 col = tokens.columnNumber(); 606 Object parameter = parseValue(); 607 if (parameter == null) { 608 break; 609 } 610 function.add(parameter, line, col); 611 } while (true); 612 613 // Now evaluate the function ... 614 Object value = evaluateFunction(function); 615 return value != null ? value : evaluateUnknownFunction(function); 616 } 617 618 /** 619 * Method that is called to evaluate the supplied function. This method may be overridden by subclasses to handle custom 620 * functions. 621 * 622 * @param function the function definition 623 * @return the value that resulted from evaluating the function, or null if the function call could not be evaluated 624 * @throws ParsingException if there is a problem parsing the value 625 */ 626 public Object evaluateFunction( FunctionCall function ) throws ParsingException { 627 int numParams = function.size(); 628 if ("date".equalsIgnoreCase(function.getFunctionName())) { 629 if (numParams > 0) { 630 // The parameter should be a long or a timestamp ... 631 FunctionParameter param1 = function.get(0); 632 Object value = param1.getValue(); 633 if (value instanceof Long) { 634 Long millis = (Long)value; 635 return values.createDate(millis.longValue()); 636 } 637 if (value instanceof Integer) { 638 Integer millis = (Integer)value; 639 return values.createDate(millis.longValue()); 640 } 641 if (value instanceof String) { 642 String valueStr = (String)value; 643 try { 644 return values.createDate(valueStr); 645 } catch (ParseException e) { 646 // Not a valid date ... 647 throw tokens.error("Expecting the \"new Date(...)\" parameter to be a valid number of milliseconds or ISO date string, but found \"" 648 + param1.getValue() 649 + "\" at line " 650 + param1.getLineNumber() 651 + ", column " 652 + param1.getColumnNumber()); 653 } 654 } 655 } 656 // Not a valid date ... 657 throw tokens.error("The date function requires one parameter at line " + function.getLineNumber() + ", column " 658 + function.getColumnNumber()); 659 } 660 return null; 661 } 662 663 /** 664 * Method that is called when the function call described by the parameter could not be evaluated. By default, the string 665 * representation of the function is returned. 666 * 667 * @param function the function definition 668 * @return the value that resulted from evaluating the function 669 * @throws ParsingException if there is a problem parsing the value 670 */ 671 protected Object evaluateUnknownFunction( FunctionCall function ) throws ParsingException { 672 return function.toString(); 673 } 674 675 @SuppressWarnings( "deprecation" ) 676 protected Object processDocumentWithReservedFieldNames( Document doc ) { 677 if (doc == null) return null; 678 Object value = null; 679 int numFields = doc.size(); 680 if (numFields == 0) return doc; 681 try { 682 if (numFields == 1) { 683 if (!Null.matches(value = doc.get(OBJECT_ID))) { 684 String bytesInBase16 = value.toString(); 685 return values.createObjectId(bytesInBase16); 686 } 687 if (!Null.matches(value = doc.get(DATE))) { 688 if (value instanceof Date) { 689 return value; 690 } 691 String isoDate = value.toString(); 692 try { 693 return values.createDate(isoDate); 694 } catch (ParseException e) { 695 Long millis = Long.parseLong(isoDate); 696 return values.createDate(millis); 697 } 698 } 699 if (!Null.matches(value = doc.get(REGEX_PATTERN))) { 700 String pattern = value.toString(); 701 return values.createRegex(pattern, null); 702 } 703 if (!Null.matches(value = doc.get(UUID))) { 704 return values.createUuid(value.toString()); 705 } 706 if (!Null.matches(value = doc.get(CODE))) { 707 String code = value.toString(); 708 return values.createCode(code); 709 } 710 } else if (numFields == 2) { 711 if (!Null.matches(value = doc.get(TIMESTAMP))) { 712 int time = doc.getInteger(TIMESTAMP); 713 int inc = doc.getInteger(INCREMENT); 714 return values.createTimestamp(time, inc); 715 } 716 if (!Null.matches(value = doc.get(REGEX_PATTERN))) { 717 String pattern = value.toString(); 718 String options = doc.getString(REGEX_OPTIONS); 719 return values.createRegex(pattern, options); 720 } 721 if (!Null.matches(value = doc.get(CODE))) { 722 String code = value.toString(); 723 Document scope = doc.getDocument(SCOPE); 724 return scope != null ? values.createCode(code, scope) : values.createCode(code); 725 } 726 if (!Null.matches(value = doc.get(BINARY_TYPE))) { 727 char c = value.toString().charAt(0); 728 byte type = 0x00; 729 switch (c) { 730 case '0': 731 type = Bson.BinaryType.GENERAL; 732 break; 733 case '1': 734 type = Bson.BinaryType.FUNCTION; 735 break; 736 case '2': 737 type = Bson.BinaryType.BINARY; 738 break; 739 case '3': 740 type = Bson.BinaryType.UUID; 741 break; 742 case '5': 743 type = Bson.BinaryType.MD5; 744 break; 745 case '8': 746 c = value.toString().charAt(1); 747 if (c == '0') { 748 type = Bson.BinaryType.USER_DEFINED; 749 } 750 break; 751 } 752 String data = doc.getString(BASE_64); 753 return values.createBinary(type, Base64.decode(data)); 754 } 755 } 756 } catch (Throwable e) { 757 // ignore 758 } 759 return doc; 760 } 761 762 protected static class FunctionCall implements Iterable<FunctionParameter> { 763 private final String functionName; 764 private final List<FunctionParameter> parameters = new LinkedList<>(); 765 private final int lineNumber; 766 private final int columnNumber; 767 768 public FunctionCall( String functionName, 769 int lineNumber, 770 int columnNumber ) { 771 this.functionName = functionName; 772 this.lineNumber = lineNumber; 773 this.columnNumber = columnNumber; 774 } 775 776 public String getFunctionName() { 777 return functionName; 778 } 779 780 public void add( Object parameter, 781 int lineNumber, 782 int columnNumber ) { 783 this.parameters.add(new FunctionParameter(parameter, lineNumber, columnNumber)); 784 } 785 786 @Override 787 public Iterator<FunctionParameter> iterator() { 788 return parameters.iterator(); 789 } 790 791 public FunctionParameter get( int index ) { 792 return parameters.get(index); 793 } 794 795 public int size() { 796 return parameters.size(); 797 } 798 799 public int getLineNumber() { 800 return lineNumber; 801 } 802 803 public int getColumnNumber() { 804 return columnNumber; 805 } 806 807 @Override 808 public String toString() { 809 StringBuilder sb = new StringBuilder(functionName); 810 sb.append('('); 811 boolean first = true; 812 for (FunctionParameter parameter : parameters) { 813 if (first) { 814 first = false; 815 } else { 816 sb.append(','); 817 } 818 sb.append(parameter.getValue()); 819 } 820 sb.append(')'); 821 return sb.toString(); 822 } 823 } 824 825 @Immutable 826 protected static class FunctionParameter { 827 private final Object value; 828 private final int lineNumber; 829 private final int columnNumber; 830 831 public FunctionParameter( Object value, 832 int lineNumber, 833 int columnNumber ) { 834 this.value = value; 835 this.lineNumber = lineNumber; 836 this.columnNumber = columnNumber; 837 } 838 839 public Object getValue() { 840 return value; 841 } 842 843 public int getLineNumber() { 844 return lineNumber; 845 } 846 847 public int getColumnNumber() { 848 return columnNumber; 849 } 850 851 @Override 852 public String toString() { 853 return Json.write(value); 854 } 855 } 856 } 857 858 /** 859 * The component that matches a string value for certain patterns. If the value matches a known pattern, it return the 860 * appropriate value object; otherwise, the supplied string value is returned. 861 * 862 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc. 863 * @since 5.1 864 */ 865 @NotThreadSafe 866 public static interface ValueMatcher { 867 868 /** 869 * Parse the value given by the supplied string into an appropriate value object. This method looks for specific patterns 870 * of Date strings; if no known pattern is found, it just returns the supplied value. 871 * 872 * @param value the string representation of the value 873 * @return the value 874 */ 875 public Object parseValue( String value ); 876 } 877 878 /** 879 * The component that matches a string value for certain patterns. If the value matches a known pattern, it return the 880 * appropriate value object; otherwise, the supplied string value is returned. 881 * 882 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc. 883 * @since 5.1 884 */ 885 @NotThreadSafe 886 public static class SimpleValueMatcher implements ValueMatcher { 887 888 protected final DocumentValueFactory values; 889 890 /** 891 * Create a new matcher that uses the supplied {@link DocumentValueFactory} instance. 892 * 893 * @param values the factory for creating value objects; may not be null 894 */ 895 public SimpleValueMatcher( DocumentValueFactory values ) { 896 this.values = values; 897 } 898 899 /** 900 * Parse the value given by the supplied string into an appropriate value object. This method looks for specific patterns 901 * of Date strings; if no known pattern is found, it just returns the supplied value. 902 * 903 * @param value the string representation of the value 904 * @return the value 905 */ 906 @Override 907 public Object parseValue( String value ) { 908 return value; 909 } 910 } 911 912 /** 913 * The component that parses a tokenized JSON stream and attempts to evaluate literal values such as dates 914 * 915 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc. 916 * @since 5.1 917 */ 918 @NotThreadSafe 919 public static class DateValueMatcher extends SimpleValueMatcher { 920 921 /** 922 * Create a new matcher that uses the supplied {@link DocumentValueFactory} instance. 923 * 924 * @param values the factory for creating value objects; may not be null 925 */ 926 public DateValueMatcher( DocumentValueFactory values ) { 927 super(values); 928 } 929 930 /** 931 * Parse the value given by the supplied string into an appropriate value object. This method looks for specific patterns 932 * of Date strings; if no known pattern is found, it just returns the supplied value. 933 * 934 * @param value the string representation of the value 935 * @return the value 936 */ 937 @Override 938 public Object parseValue( String value ) { 939 if (value != null) { 940 if (value.length() > 2) { 941 Date date = parseDateFromLiteral(value); 942 if (date != null) { 943 return date; 944 } 945 } 946 // Unescape escaped characters ... 947 value = unescapeValue(value); 948 } 949 return value; 950 } 951 952 protected String unescapeValue( String value ) { 953 if (value == null || value.length() == 0) return value; 954 955 StringBuilder sb = new StringBuilder(value.length()); 956 CharacterIterator iter = new StringCharacterIterator(value); 957 for (char c = iter.first(); c != CharacterIterator.DONE; c = iter.next()) { 958 switch (c) { 959 case '\\': 960 // The character might be an escape sequence, so output the backslash ... 961 char next = iter.next(); 962 switch (next) { 963 case CharacterIterator.DONE: 964 // This was the last character, so we're done ... 965 sb.append(c); 966 break; 967 case '/': // optional 968 case '\b': 969 case '\f': 970 case '\n': 971 case '\r': 972 case '\t': 973 // This is an escaped sequence ... 974 sb.append(next); 975 break; 976 case 'u': 977 // This is an unicode escape sequence, so we already output one of them ... 978 char first = iter.next(); 979 char second = iter.next(); 980 char third = iter.next(); 981 char fourth = iter.next(); 982 String string = "" + first + second + third + fourth; 983 try { 984 char uni = (char)Integer.parseInt(string, 16); 985 sb.append(uni); 986 } catch (NumberFormatException e) { 987 // this is not a valid unicode escape sequence so just append it as is 988 sb.append("\\u").append(string); 989 continue; 990 } 991 break; 992 default: 993 // It's not an escape sequence that we care about. We've already written the backslash, 994 // so just write the character ... 995 sb.append(c); 996 sb.append(next); 997 } 998 break; 999 default: 1000 // Unicode escapes are handled above ... 1001 sb.append(c); 1002 break; 1003 } 1004 } 1005 return sb.toString(); 1006 } 1007 1008 /** 1009 * Parse the date represented by the supplied value. This method is called by the {@link #parseValue(String)} method. This 1010 * method checks the following formats: 1011 * <ul> 1012 * <li>a string literal date of the form <code>"<i>yyyy</i>-<i>MM</i>-<i>dd</i>T<i>HH</i>:<i>mm</i>:<i>ss</i>"</code> 1013 * where <code>T</code> is a literal character</li> 1014 * <li>a string literal date of the form <code>"<i>yyyy</i>-<i>MM</i>-<i>dd</i>T<i>HH</i>:<i>mm</i>:<i>ss</i>Z"</code> 1015 * where <code>T</code> and <code>Z</code> are literal characters</li> 1016 * <li>a string literal date of the form 1017 * <code>"<i>yyyy</i>-<i>MM</i>-<i>dd</i>T<i>HH</i>:<i>mm</i>:<i>ss</i>GMT+<i>00</i>:<i>00</i>"</code> where 1018 * <code>T</code>, and <code>GMT</code> are literal characters</li> 1019 * <li>a string literal date of the form <code>"/Date(<i>millisOrIso</i>)/"</code></li> 1020 * <li>a string literal date of the form <code>"\/Date(<i>millisOrIso</i>)\/"</code></li> 1021 * </ul> 1022 * <p> 1023 * Note that this method does not handle the <code>new Date(...)</code> or <code>Date(...)</code> representations, as 1024 * that's handled elsewhere. 1025 * </p> 1026 * 1027 * @param value the string representation of the value; never null and never empty 1028 * @return the number, or null if the value could not be parsed 1029 */ 1030 protected Date parseDateFromLiteral( String value ) { 1031 char f = value.charAt(0); 1032 if (Character.isDigit(f)) { 1033 // Try as simply an ISO-8601 formatted date ... 1034 return evaluateDate(value); 1035 } 1036 if (value.startsWith("\\/Date(") && value.endsWith(")\\/")) { 1037 String millisOrIso = value.substring(7, value.length() - 3).trim(); 1038 return evaluateDate(millisOrIso); 1039 } 1040 if (value.startsWith("/Date(") && value.endsWith(")/")) { 1041 String millisOrIso = value.substring(6, value.length() - 2).trim(); 1042 return evaluateDate(millisOrIso); 1043 } 1044 return null; 1045 } 1046 1047 protected Date evaluateDate( String millisOrIso ) { 1048 try { 1049 return values.createDate(millisOrIso); 1050 } catch (ParseException e) { 1051 // not an ISO-8601 format ... 1052 } 1053 return null; 1054 } 1055 1056 } 1057 1058 /** 1059 * The component that tokenizes a stream of JSON content. 1060 * 1061 * @author Randall Hauch <rhauch@redhat.com> (C) 2011 Red Hat Inc. 1062 * @since 5.1 1063 */ 1064 @NotThreadSafe 1065 public static class Tokenizer { 1066 1067 private final Reader reader; 1068 private int lineNumber; 1069 private int columnNumber; 1070 private boolean finished; 1071 private boolean hasPrevious; 1072 private char previous; 1073 private StringBuilder stringBuilder = new StringBuilder(128); 1074 1075 /** 1076 * Create a new tokenizer that uses the supplied {@link Reader Java IO Reader} instance. 1077 * 1078 * @param reader the reader for accessing the JSON content; may not be null 1079 */ 1080 public Tokenizer( Reader reader ) { 1081 this.reader = reader; 1082 } 1083 1084 public boolean isFinished() { 1085 return finished; 1086 } 1087 1088 protected char next() throws ParsingException { 1089 char c = 0; 1090 if (hasPrevious) { 1091 hasPrevious = false; 1092 c = previous; 1093 } else { 1094 try { 1095 int x = reader.read(); 1096 if (x <= 0) { 1097 // We've reached the end of the stream ... 1098 finished = true; 1099 c = 0; 1100 } else { 1101 c = (char)x; 1102 } 1103 } catch (IOException e) { 1104 throw error("Error reading at line " + lineNumber + ", column " + columnNumber + ": " 1105 + e.getLocalizedMessage(), 1106 e); 1107 } 1108 } 1109 // It's a valid character, but we have to advance our line & column counts ... 1110 if (previous == '\r') { 1111 ++lineNumber; 1112 columnNumber = (c == '\n' ? 0 : 1); 1113 } else if (c == '\n') { 1114 ++lineNumber; 1115 columnNumber = 0; 1116 } else { 1117 ++columnNumber; 1118 } 1119 previous = c; 1120 return c; 1121 } 1122 1123 public String next( int characterCount ) throws ParsingException { 1124 StringBuilder sb = stringBuilder(); 1125 for (int i = 0; i != characterCount; ++i) { 1126 sb.append(next()); 1127 } 1128 return complete(sb); 1129 } 1130 1131 protected final StringBuilder stringBuilder() { 1132 StringBuilder stringBuilder = this.stringBuilder != null ? this.stringBuilder : new StringBuilder(128); 1133 this.stringBuilder = null; 1134 stringBuilder.delete(0, stringBuilder.length()); 1135 return stringBuilder; 1136 } 1137 1138 protected final String complete( StringBuilder sb ) { 1139 assert sb != null; 1140 assert stringBuilder == null; 1141 stringBuilder = sb; 1142 return sb.toString(); 1143 } 1144 1145 public char nextUsefulChar() throws ParsingException { 1146 boolean withinComment = false; 1147 do { 1148 char next = next(); 1149 if (next == '/') { 1150 char afterNext = next(); 1151 if (afterNext != '/') { 1152 throw error("Invalid character '" + afterNext + "' (expected comment //)"); 1153 } 1154 withinComment = true; 1155 continue; 1156 } 1157 boolean isLineSeparator = (next == '\n') || (next == '\r'); 1158 if (isLineSeparator && withinComment) { 1159 withinComment = false; 1160 } 1161 if (next == 0 || (!withinComment && next != ' ' && next != '\t' && !isLineSeparator)) return next; 1162 } while (true); 1163 } 1164 1165 public char peek() throws ParsingException { 1166 if (hasPrevious) { 1167 return previous; 1168 } 1169 char next = nextUsefulChar(); 1170 hasPrevious = true; 1171 previous = next; 1172 --columnNumber; 1173 return next; 1174 } 1175 1176 /** 1177 * Read the next quoted string from the stream, where stream begins with the a single-quote or double-quote character and 1178 * the string ends with the same quote character. 1179 * 1180 * @return the next string; never null 1181 * @throws ParsingException 1182 */ 1183 public String nextString() throws ParsingException { 1184 char c = nextUsefulChar(); 1185 switch (c) { 1186 case '"': 1187 case '\'': 1188 return nextString(c); 1189 } 1190 throw error("Expecting a field name at line " + lineNumber + ", column " + columnNumber 1191 + ". Check for a missing comma."); 1192 } 1193 1194 public String nextString( char endQuote ) throws ParsingException { 1195 StringBuilder sb = stringBuilder(); 1196 char c = 0; 1197 do { 1198 c = next(); 1199 switch (c) { 1200 case 0: 1201 case '\n': 1202 case '\r': 1203 // The string was not properly terminated ... 1204 throw error("The string was not terminated before the end of line or end of document, at line " 1205 + lineNumber + ", column " + columnNumber); 1206 case '\\': 1207 // Escape sequence ... 1208 c = next(); 1209 switch (c) { 1210 case '\'': // single quote 1211 case '"': // double quote 1212 case '\\': // reverse solidus 1213 case '/': // forward solidus 1214 break; 1215 case 'b': 1216 c = '\b'; 1217 break; 1218 case 'f': 1219 c = '\f'; 1220 break; 1221 case 'n': 1222 c = '\n'; 1223 break; 1224 case 'r': 1225 c = '\r'; 1226 break; 1227 case 't': 1228 c = '\t'; 1229 break; 1230 case 'u': 1231 // Unicode sequence made of exactly 4 hex characters ... 1232 char[] hex = new char[4]; 1233 hex[0] = next(); 1234 hex[1] = next(); 1235 hex[2] = next(); 1236 hex[3] = next(); 1237 String code = new String(hex, 0, 4); 1238 try { 1239 c = (char)Integer.parseInt(code, 16); // hex 1240 } catch (NumberFormatException e) { 1241 // this is not a valid unicode escape sequence so just append it as is 1242 sb.append('\\').append('u').append(code); 1243 continue; 1244 } 1245 break; 1246 default: 1247 // No other characters are valid escaped sequences, so this is actually just a backslash 1248 // followed by the current character c. So append the backslash ... 1249 sb.append('\\'); 1250 // then the character ... 1251 break; 1252 } 1253 sb.append(c); 1254 break; 1255 default: 1256 // Just a regular character (or the end quote) ... 1257 if (c == endQuote) { 1258 // This is the only way to successfully exit this method! 1259 return complete(sb); 1260 } 1261 // just a regular character ... 1262 sb.append(c); 1263 } 1264 } while (true); 1265 } 1266 1267 public void nextFieldDelim() throws ParsingException { 1268 try { 1269 switch (nextUsefulChar()) { 1270 case ':': 1271 case '=': 1272 if (peek() == '>') { 1273 next(); // consume the '>' 1274 } 1275 break; 1276 } 1277 } catch (ParsingException e) { 1278 throw error("Expecting a field delimiter (either ':', '=' or '=>') at line " + lineNumber + ", column " 1279 + columnNumber); 1280 } 1281 } 1282 1283 /** 1284 * Consume the next document delimiter (either a ',' or a ';'), and return whether the end-of-document character (e.g., 1285 * '}') has been consumed. This will correctly handle repeated delimiters, which are technically incorrect. 1286 * 1287 * @return true if a '}' has been consumed, or false otherwise 1288 * @throws ParsingException if the document delimiter could not be read 1289 */ 1290 public boolean nextDocumentDelim() throws ParsingException { 1291 switch (nextUsefulChar()) { 1292 case ';': // handle ';' delimiters, too! 1293 case ',': 1294 switch (peek()) { 1295 case ':': 1296 case ',': 1297 // There are multiple delimiters in a row. Strictly speaking, this is invalid but we 1298 // can easily handle it anyway ... 1299 return nextDocumentDelim(); 1300 case '}': 1301 // The comma was before '}' - this is not strictly well-formed, but we'll handle it 1302 next(); 1303 return true; 1304 } 1305 return false; 1306 case '}': 1307 return true; 1308 } 1309 return false; 1310 } 1311 1312 /** 1313 * Return a string containing the next number on the stream. 1314 * 1315 * @return the next number as a string, or null if there is no content on the stream 1316 * @throws ParsingException if the number could not be read 1317 */ 1318 public String nextNumber() throws ParsingException { 1319 char c = peek(); 1320 if (c == 0) { 1321 return null; 1322 } 1323 StringBuilder sb = stringBuilder(); 1324 while (c > ' ' && "{}[]:\"=#/\\',;".indexOf(c) <= -1) { 1325 if (c == 0) { 1326 break; 1327 } 1328 sb.append(next()); 1329 c = peek(); 1330 } 1331 return complete(sb); 1332 } 1333 1334 /** 1335 * Return a string containing the next alpha-numeric word on the stream. 1336 * 1337 * @return the next word as a string 1338 * @throws ParsingException if the number could not be read 1339 */ 1340 public String nextWord() throws ParsingException { 1341 char c = peek(); 1342 StringBuilder sb = stringBuilder(); 1343 while (Character.isLetterOrDigit(c)) { 1344 sb.append(next()); 1345 c = peek(); 1346 } 1347 return complete(sb); 1348 } 1349 1350 public ParsingException error( String message ) { 1351 return new ParsingException(message, lineNumber, columnNumber); 1352 } 1353 1354 public ParsingException error( String message, 1355 Throwable t ) { 1356 return new ParsingException(message, t, lineNumber, columnNumber); 1357 } 1358 1359 public int lineNumber() { 1360 return lineNumber; 1361 } 1362 1363 public int columnNumber() { 1364 return columnNumber; 1365 } 1366 } 1367}