001/* 002 * Copyright 2015-2020 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2015-2020 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2015-2020 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.util.json; 037 038 039 040import java.math.BigDecimal; 041 042import com.unboundid.util.Debug; 043import com.unboundid.util.NotMutable; 044import com.unboundid.util.StaticUtils; 045import com.unboundid.util.ThreadSafety; 046import com.unboundid.util.ThreadSafetyLevel; 047 048import static com.unboundid.util.json.JSONMessages.*; 049 050 051 052/** 053 * This class provides an implementation of a JSON value that represents a 054 * base-ten numeric value of arbitrary size. It may or may not be a 055 * floating-point value (including a decimal point with numbers to the right of 056 * it), and it may or may not be expressed using scientific notation. The 057 * numeric value will be represented internally as a {@code BigDecimal}. 058 * <BR><BR> 059 * The string representation of a JSON number consists of the following 060 * elements, in the following order: 061 * <OL> 062 * <LI> 063 * An optional minus sign to indicate that the value is negative. If this 064 * is absent, then the number will be positive. Positive numbers must not 065 * be prefixed with a plus sign. 066 * </LI> 067 * <LI> 068 * One or more numeric digits to specify the whole number portion of the 069 * value. There must not be any unnecessary leading zeroes, so the first 070 * digit may be zero only if it is the only digit in the whole number 071 * portion of the value. 072 * </LI> 073 * <LI> 074 * An optional decimal point followed by at least one numeric digit to 075 * indicate the fractional portion of the value. Trailing zeroes are 076 * allowed in the fractional component. 077 * </LI> 078 * <LI> 079 * An optional 'e' or 'E' character, followed by an optional '+' or '-' 080 * character and at least one numeric digit to indicate that the value is 081 * expressed in scientific notation and the number before the uppercase or 082 * lowercase E should be multiplied by the specified positive or negative 083 * power of ten. 084 * </LI> 085 * </OL> 086 * It is possible for the same number to have multiple equivalent string 087 * representations. For example, all of the following valid string 088 * representations of JSON numbers represent the same numeric value: 089 * <UL> 090 * <LI>12345</LI> 091 * <LI>12345.0</LI> 092 * <LI>1.2345e4</LI> 093 * <LI>1.2345e+4</LI> 094 * </UL> 095 * JSON numbers must not be enclosed in quotation marks. 096 * <BR><BR> 097 * If a JSON number is created from its string representation, then that 098 * string representation will be returned from the {@link #toString()} method 099 * (or appended to the provided buffer for the {@link #toString(StringBuilder)} 100 * method). If a JSON number is created from a {@code long} or {@code double} 101 * value, then the Java string representation of that value (as obtained from 102 * the {@code String.valueOf} method) will be used as the string representation 103 * for the number. If a JSON number is created from a {@code BigDecimal} value, 104 * then the Java string representation will be obtained via that value's 105 * {@code toPlainString} method. 106 * <BR><BR> 107 * The normalized representation of a JSON number is a canonical string 108 * representation for that number. That is, all equivalent JSON number values 109 * will have the same normalized representation. The normalized representation 110 * will never use scientific notation, will never have trailing zeroes in the 111 * fractional component, and will never have a fractional component if that 112 * fractional component would be zero. For example, for the 113 * logically-equivalent values "12345", "12345.0", "1.2345e4", and "1.2345e+4", 114 * the normalized representation will be "12345". For the logically-equivalent 115 * values "9876.5", "9876.50", and "9.8765e3", the normalized representation 116 * will be "9876.5". 117 */ 118@NotMutable() 119@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 120public final class JSONNumber 121 extends JSONValue 122{ 123 /** 124 * The serial version UID for this serializable class. 125 */ 126 private static final long serialVersionUID = -9194944952299318254L; 127 128 129 130 // The numeric value for this object. 131 private final BigDecimal value; 132 133 // The normalized representation of the value. 134 private final BigDecimal normalizedValue; 135 136 // The string representation for this object. 137 private final String stringRepresentation; 138 139 140 141 /** 142 * Creates a new JSON number with the provided value. 143 * 144 * @param value The value for this JSON number. 145 */ 146 public JSONNumber(final long value) 147 { 148 this.value = new BigDecimal(value); 149 normalizedValue = this.value; 150 stringRepresentation = String.valueOf(value); 151 } 152 153 154 155 /** 156 * Creates a new JSON number with the provided value. 157 * 158 * @param value The value for this JSON number. 159 */ 160 public JSONNumber(final double value) 161 { 162 this.value = new BigDecimal(value); 163 normalizedValue = this.value; 164 stringRepresentation = String.valueOf(value); 165 } 166 167 168 169 /** 170 * Creates a new JSON number with the provided value. 171 * 172 * @param value The value for this JSON number. It must not be 173 * {@code null}. 174 */ 175 public JSONNumber(final BigDecimal value) 176 { 177 this.value = value; 178 stringRepresentation = value.toPlainString(); 179 180 // There isn't a simple way to get a good normalized value from a 181 // BigDecimal. If it represents an integer but has a decimal point followed 182 // by some zeroes, then the only way we can strip them off is to convert it 183 // from a BigDecimal to a BigInteger and back. If it represents a 184 // floating-point value that has unnecessary zeros then we have to call the 185 // stripTrailingZeroes method. 186 BigDecimal minimalValue; 187 try 188 { 189 minimalValue = new BigDecimal(value.toBigIntegerExact()); 190 } 191 catch (final Exception e) 192 { 193 // This is fine -- it just means that the value does not represent an 194 // integer. 195 minimalValue = value.stripTrailingZeros(); 196 } 197 normalizedValue = minimalValue; 198 } 199 200 201 202 /** 203 * Creates a new JSON number from the provided string representation. 204 * 205 * @param stringRepresentation The string representation to parse as a JSON 206 * number. It must not be {@code null}. 207 * 208 * @throws JSONException If the provided string cannot be parsed as a valid 209 * JSON number. 210 */ 211 public JSONNumber(final String stringRepresentation) 212 throws JSONException 213 { 214 this.stringRepresentation = stringRepresentation; 215 216 217 // Make sure that the provided string represents a valid JSON number. This 218 // is a little more strict than what BigDecimal accepts. First, make sure 219 // it's not an empty string. 220 final char[] chars = stringRepresentation.toCharArray(); 221 if (chars.length == 0) 222 { 223 throw new JSONException(ERR_NUMBER_EMPTY_STRING.get()); 224 } 225 226 227 // Make sure that the last character is a digit. All valid string 228 // representations of JSON numbers must end with a digit, and validating 229 // that now allows us to do less error handling in subsequent checks. 230 if (! isDigit(chars[chars.length-1])) 231 { 232 throw new JSONException(ERR_NUMBER_LAST_CHAR_NOT_DIGIT.get( 233 stringRepresentation)); 234 } 235 236 237 // If the value starts with a minus sign, then skip over it. 238 int pos = 0; 239 if (chars[0] == '-') 240 { 241 pos++; 242 } 243 244 245 // Make sure that the first character (after the potential minus sign) is a 246 // digit. If it's a zero, then make sure it's not followed by another 247 // digit. 248 if (! isDigit(chars[pos])) 249 { 250 throw new JSONException(ERR_NUMBER_ILLEGAL_CHAR.get(stringRepresentation, 251 pos)); 252 } 253 254 if (chars[pos++] == '0') 255 { 256 if ((chars.length > pos) && isDigit(chars[pos])) 257 { 258 throw new JSONException(ERR_NUMBER_ILLEGAL_LEADING_ZERO.get( 259 stringRepresentation)); 260 } 261 } 262 263 264 // Parse the rest of the string. Make sure that it satisfies all of the 265 // following constraints: 266 // - There can be at most one decimal point. If there is a decimal point, 267 // it must be followed by at least one digit. 268 // - There can be at most one uppercase or lowercase 'E'. If there is an 269 // 'E', then it must be followed by at least one digit, or it must be 270 // followed by a plus or minus sign and at least one digit. 271 // - If there are both a decimal point and an 'E', then the decimal point 272 // must come before the 'E'. 273 // - The only other characters allowed are digits. 274 boolean decimalFound = false; 275 boolean eFound = false; 276 for ( ; pos < chars.length; pos++) 277 { 278 final char c = chars[pos]; 279 if (c == '.') 280 { 281 if (decimalFound) 282 { 283 throw new JSONException(ERR_NUMBER_MULTIPLE_DECIMAL_POINTS.get( 284 stringRepresentation)); 285 } 286 else 287 { 288 decimalFound = true; 289 } 290 291 if (eFound) 292 { 293 throw new JSONException(ERR_NUMBER_DECIMAL_IN_EXPONENT.get( 294 stringRepresentation)); 295 } 296 297 if (! isDigit(chars[pos+1])) 298 { 299 throw new JSONException(ERR_NUMBER_DECIMAL_NOT_FOLLOWED_BY_DIGIT.get( 300 stringRepresentation)); 301 } 302 } 303 else if ((c == 'e') || (c == 'E')) 304 { 305 if (eFound) 306 { 307 throw new JSONException(ERR_NUMBER_MULTIPLE_EXPONENTS.get( 308 stringRepresentation)); 309 } 310 else 311 { 312 eFound = true; 313 } 314 315 if ((chars[pos+1] == '-') || (chars[pos+1] == '+')) 316 { 317 if (! isDigit(chars[pos+2])) 318 { 319 throw new JSONException( 320 ERR_NUMBER_EXPONENT_NOT_FOLLOWED_BY_DIGIT.get( 321 stringRepresentation)); 322 } 323 324 // Increment the counter to skip over the sign. 325 pos++; 326 } 327 else if (! isDigit(chars[pos+1])) 328 { 329 throw new JSONException(ERR_NUMBER_EXPONENT_NOT_FOLLOWED_BY_DIGIT.get( 330 stringRepresentation)); 331 } 332 } 333 else if (! isDigit(chars[pos])) 334 { 335 throw new JSONException(ERR_NUMBER_ILLEGAL_CHAR.get( 336 stringRepresentation, pos)); 337 } 338 } 339 340 341 // If we've gotten here, then we know the string represents a valid JSON 342 // number. BigDecimal should be able to parse all valid JSON numbers. 343 try 344 { 345 value = new BigDecimal(stringRepresentation); 346 } 347 catch (final Exception e) 348 { 349 Debug.debugException(e); 350 351 // This should never happen if all of the validation above is correct, but 352 // handle it just in case. 353 throw new JSONException( 354 ERR_NUMBER_CANNOT_PARSE.get(stringRepresentation, 355 StaticUtils.getExceptionMessage(e)), 356 e); 357 } 358 359 // There isn't a simple way to get a good normalized value from a 360 // BigDecimal. If it represents an integer but has a decimal point followed 361 // by some zeroes, then the only way we can strip them off is to convert it 362 // from a BigDecimal to a BigInteger and back. If it represents a 363 // floating-point value that has unnecessary zeros then we have to call the 364 // stripTrailingZeroes method. 365 BigDecimal minimalValue; 366 try 367 { 368 minimalValue = new BigDecimal(value.toBigIntegerExact()); 369 } 370 catch (final Exception e) 371 { 372 // This is fine -- it just means that the value does not represent an 373 // integer. 374 minimalValue = value.stripTrailingZeros(); 375 } 376 normalizedValue = minimalValue; 377 } 378 379 380 381 /** 382 * Indicates whether the specified character represents a digit. 383 * 384 * @param c The character for which to make the determination. 385 * 386 * @return {@code true} if the specified character represents a digit, or 387 * {@code false} if not. 388 */ 389 private static boolean isDigit(final char c) 390 { 391 switch (c) 392 { 393 case '0': 394 case '1': 395 case '2': 396 case '3': 397 case '4': 398 case '5': 399 case '6': 400 case '7': 401 case '8': 402 case '9': 403 return true; 404 default: 405 return false; 406 } 407 } 408 409 410 411 /** 412 * Retrieves the value of this JSON number as a {@code BigDecimal}. 413 * 414 * @return The value of this JSON number as a {@code BigDecimal}. 415 */ 416 public BigDecimal getValue() 417 { 418 return value; 419 } 420 421 422 423 /** 424 * {@inheritDoc} 425 */ 426 @Override() 427 public int hashCode() 428 { 429 return normalizedValue.hashCode(); 430 } 431 432 433 434 /** 435 * {@inheritDoc} 436 */ 437 @Override() 438 public boolean equals(final Object o) 439 { 440 if (o == this) 441 { 442 return true; 443 } 444 445 if (o instanceof JSONNumber) 446 { 447 // NOTE: BigDecimal.equals probably doesn't do what you want, nor what 448 // anyone would normally expect. If you want to determine if two 449 // BigDecimal values are the same, then use compareTo. 450 final JSONNumber n = (JSONNumber) o; 451 return (value.compareTo(n.value) == 0); 452 } 453 454 return false; 455 } 456 457 458 459 /** 460 * {@inheritDoc} 461 */ 462 @Override() 463 public boolean equals(final JSONValue v, final boolean ignoreFieldNameCase, 464 final boolean ignoreValueCase, 465 final boolean ignoreArrayOrder) 466 { 467 return ((v instanceof JSONNumber) && 468 (value.compareTo(((JSONNumber) v).value) == 0)); 469 } 470 471 472 473 /** 474 * Retrieves a string representation of this number as it should appear in a 475 * JSON object. If the object containing this number was decoded from a 476 * string, then this method will use the same string representation as in that 477 * original object. Otherwise, the string representation will be constructed. 478 * 479 * @return A string representation of this number as it should appear in a 480 * JSON object. 481 */ 482 @Override() 483 public String toString() 484 { 485 return stringRepresentation; 486 } 487 488 489 490 /** 491 * Appends a string representation of this number as it should appear in a 492 * JSON object to the provided buffer. If the object containing this number 493 * was decoded from a string, then this method will use the same string 494 * representation as in that original object. Otherwise, the string 495 * representation will be constructed. 496 * 497 * @param buffer The buffer to which the information should be appended. 498 */ 499 @Override() 500 public void toString(final StringBuilder buffer) 501 { 502 buffer.append(stringRepresentation); 503 } 504 505 506 507 /** 508 * Retrieves a single-line string representation of this number as it should 509 * appear in a JSON object. If the object containing this number was decoded 510 * from a string, then this method will use the same string representation as 511 * in that original object. Otherwise, the string representation will be 512 * constructed. 513 * 514 * @return A single-line string representation of this number as it should 515 * appear in a JSON object. 516 */ 517 @Override() 518 public String toSingleLineString() 519 { 520 return stringRepresentation; 521 } 522 523 524 525 /** 526 * Appends a single-line string representation of this number as it should 527 * appear in a JSON object to the provided buffer. If the object containing 528 * this number was decoded from a string, then this method will use the same 529 * string representation as in that original object. Otherwise, the string 530 * representation will be constructed. 531 * 532 * @param buffer The buffer to which the information should be appended. 533 */ 534 @Override() 535 public void toSingleLineString(final StringBuilder buffer) 536 { 537 buffer.append(stringRepresentation); 538 } 539 540 541 542 /** 543 * Retrieves a normalized string representation of this number as it should 544 * appear in a JSON object. The normalized representation will not use 545 * exponentiation, will not include a decimal point if the value can be 546 * represented as an integer, and will not include any unnecessary trailing 547 * zeroes if it can only be represented as a floating-point value. 548 * 549 * @return A normalized string representation of this number as it should 550 * appear in a JSON object. 551 */ 552 @Override() 553 public String toNormalizedString() 554 { 555 return normalizedValue.toPlainString(); 556 } 557 558 559 560 /** 561 * Appends a normalized string representation of this number as it should 562 * appear in a JSON object to the provided buffer. The normalized 563 * representation will not use exponentiation, will not include a decimal 564 * point if the value can be represented as an integer, and will not include 565 * any unnecessary trailing zeroes if it can only be represented as a 566 * floating-point value. 567 * 568 * @param buffer The buffer to which the information should be appended. 569 */ 570 @Override() 571 public void toNormalizedString(final StringBuilder buffer) 572 { 573 buffer.append(normalizedValue.toPlainString()); 574 } 575 576 577 578 /** 579 * Retrieves a normalized string representation of this number as it should 580 * appear in a JSON object. The normalized representation will not use 581 * exponentiation, will not include a decimal point if the value can be 582 * represented as an integer, and will not include any unnecessary trailing 583 * zeroes if it can only be represented as a floating-point value. 584 * 585 * @param ignoreFieldNameCase Indicates whether field names should be 586 * treated in a case-sensitive (if {@code false}) 587 * or case-insensitive (if {@code true}) manner. 588 * @param ignoreValueCase Indicates whether string field values should 589 * be treated in a case-sensitive (if 590 * {@code false}) or case-insensitive (if 591 * {@code true}) manner. 592 * @param ignoreArrayOrder Indicates whether the order of elements in an 593 * array should be considered significant (if 594 * {@code false}) or insignificant (if 595 * {@code true}). 596 * 597 * @return A normalized string representation of this number as it should 598 * appear in a JSON object. 599 */ 600 @Override() 601 public String toNormalizedString(final boolean ignoreFieldNameCase, 602 final boolean ignoreValueCase, 603 final boolean ignoreArrayOrder) 604 { 605 return normalizedValue.toPlainString(); 606 } 607 608 609 610 /** 611 * Appends a normalized string representation of this number as it should 612 * appear in a JSON object to the provided buffer. The normalized 613 * representation will not use exponentiation, will not include a decimal 614 * point if the value can be represented as an integer, and will not include 615 * any unnecessary trailing zeroes if it can only be represented as a 616 * floating-point value. 617 * 618 * @param buffer The buffer to which the information should be 619 * appended. 620 * @param ignoreFieldNameCase Indicates whether field names should be 621 * treated in a case-sensitive (if {@code false}) 622 * or case-insensitive (if {@code true}) manner. 623 * @param ignoreValueCase Indicates whether string field values should 624 * be treated in a case-sensitive (if 625 * {@code false}) or case-insensitive (if 626 * {@code true}) manner. 627 * @param ignoreArrayOrder Indicates whether the order of elements in an 628 * array should be considered significant (if 629 * {@code false}) or insignificant (if 630 * {@code true}). 631 */ 632 @Override() 633 public void toNormalizedString(final StringBuilder buffer, 634 final boolean ignoreFieldNameCase, 635 final boolean ignoreValueCase, 636 final boolean ignoreArrayOrder) 637 { 638 buffer.append(normalizedValue.toPlainString()); 639 } 640 641 642 643 /** 644 * {@inheritDoc} 645 */ 646 @Override() 647 public void appendToJSONBuffer(final JSONBuffer buffer) 648 { 649 buffer.appendNumber(stringRepresentation); 650 } 651 652 653 654 /** 655 * {@inheritDoc} 656 */ 657 @Override() 658 public void appendToJSONBuffer(final String fieldName, 659 final JSONBuffer buffer) 660 { 661 buffer.appendNumber(fieldName, stringRepresentation); 662 } 663}