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