001/* 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017package org.apache.juneau.marshall; 018 019import static org.apache.juneau.commons.utils.Shorts.*; 020 021import java.math.*; 022 023/** 024 * Supported wire formats for {@link BigInteger} and {@link BigDecimal} values. 025 * 026 * <p> 027 * Used by {@link MarshallingContext.Builder#bigNumberFormat(BigNumberFormat)}, 028 * {@link Marshalled#bigNumberFormat()}, 029 * {@link MarshalledProp#bigNumberFormat()}, and 030 * {@link MarshalledConfig#bigNumberFormat()} to control how 031 * {@link BigInteger} / {@link BigDecimal} values are written to text-based wire formats. 032 * 033 * <p> 034 * The default is {@link #NUMBER} which preserves the historical wire output (a bare numeric token). 035 * 036 * <h5 class='topic'>JS-interop motivation</h5> 037 * 038 * <p> 039 * JavaScript's {@code Number.MAX_SAFE_INTEGER} is {@code 2^53 − 1} (= {@code 9007199254740991}). 040 * Values outside that range silently lose precision when parsed by a JavaScript client. {@link #STRING} 041 * forces a quoted-string representation that round-trips losslessly. {@link #AUTO} picks {@link #NUMBER} 042 * for values that are JS-safe and {@link #STRING} for everything else — the typical "safe default" for 043 * services that may be consumed by both JVM and browser clients. 044 * 045 * <h5 class='topic'>Precedence (highest to lowest)</h5> 046 * <ol> 047 * <li>{@link MarshalledProp#bigNumberFormat() @MarshalledProp(bigNumberFormat=…)} on the bean property. 048 * <li>{@link Marshalled#bigNumberFormat() @Marshalled(bigNumberFormat=…)} on the bean class. 049 * <li>{@link MarshalledConfig#bigNumberFormat() @MarshalledConfig(bigNumberFormat=…)} on 050 * <code><ja>@Rest</ja></code>-annotated classes / methods. 051 * <li>Programmatic {@link MarshallingContext.Builder#bigNumberFormat(BigNumberFormat)}. 052 * <li>Environment variable <c>MarshallingContext.bigNumberFormat</c>. 053 * <li>The default constant ({@link #NUMBER}). 054 * </ol> 055 * 056 * <h5 class='topic'>Parser leniency</h5> 057 * 058 * <p> 059 * Parsers SHALL accept both bare numeric and quoted-string input regardless of the parser-side 060 * {@code BigNumberFormat} setting. The configured constant is informational only. 061 * 062 * <h5 class='topic'>Binary serializers</h5> 063 * 064 * <p> 065 * Binary serializers (BSON / CBOR / MsgPack / Prototext) emit native numeric types regardless of the 066 * configured {@code BigNumberFormat}; this setting only affects text-based serializers. BSON has 067 * {@code decimal128} for exact decimal values; CBOR / MsgPack handle big-integer values via 068 * tag 2 / tag 3 with string fallback. 069 */ 070public enum BigNumberFormat { 071 072 /** Sentinel meaning "no value configured" — falls through to the next-higher precedence level. */ 073 NOT_SET, 074 075 /** 076 * Bare numeric token (the default). 077 * 078 * <h5 class='figure'>Example:</h5> 079 * <p class='bcode'> 080 * 12345678901234567890 081 * </p> 082 * 083 * <p> 084 * This is the historical wire output and the natural form for server-to-server JVM clients. 085 */ 086 NUMBER, 087 088 /** 089 * Quoted-string form for lossless precision. 090 * 091 * <h5 class='figure'>Example:</h5> 092 * <p class='bcode'> 093 * "12345678901234567890" 094 * </p> 095 * 096 * <p> 097 * Round-trips losslessly through any JSON / YAML / etc. parser, including JavaScript clients which 098 * would otherwise truncate to a 64-bit double. 099 */ 100 STRING, 101 102 /** 103 * Hybrid — emit {@link #NUMBER} when the value is JS-safe (fits in {@code ±(2^53 − 1)}), otherwise 104 * emit {@link #STRING}. 105 * 106 * <p> 107 * For {@link BigDecimal} values, any non-zero scale forces {@link #STRING} (a fractional value cannot 108 * be safely represented in a JS double regardless of magnitude). 109 * 110 * <p> 111 * The "safe default" for services that may be consumed by both JVM and browser clients. 112 */ 113 AUTO; 114 115 /** {@link Number#MAX_SAFE_INTEGER} equivalent — largest integer that round-trips through a JS double. */ 116 private static final long JS_MAX_SAFE_INTEGER = 9007199254740991L; 117 private static final BigInteger BIG_JS_MAX_SAFE = BigInteger.valueOf(JS_MAX_SAFE_INTEGER); 118 private static final BigInteger BIG_JS_MIN_SAFE = BigInteger.valueOf(-JS_MAX_SAFE_INTEGER); 119 120 /** 121 * Formats the specified {@link BigInteger} using this format. 122 * 123 * <p> 124 * Returns either a {@link Number} (when {@link #NUMBER} or {@link #AUTO} produces a JS-safe value) or 125 * a {@link String} (when {@link #STRING} or {@link #AUTO} produces an out-of-range value). Callers 126 * should branch on the runtime type: 127 * <ul> 128 * <li>{@link Number} → emit as a bare numeric wire token. 129 * <li>{@link String} → emit as a quoted string. 130 * </ul> 131 * 132 * @param value The value to format. Can be <jk>null</jk> (returns <jk>null</jk>). 133 * @param format The configured format. Can be <jk>null</jk> (treated as {@link #NUMBER}). 134 * @return The formatted value (a {@link BigInteger} for numeric output, a {@link String} for string output), 135 * or <jk>null</jk> if {@code value} is <jk>null</jk>. 136 */ 137 public static Object format(BigInteger value, BigNumberFormat format) { 138 if (value == null) 139 return null; 140 var fmt = format == null ? NUMBER : format; 141 return switch (fmt) { 142 case NOT_SET, NUMBER -> value; 143 case STRING -> value.toString(); 144 case AUTO -> isJsSafe(value) ? (Object) value : value.toString(); 145 }; 146 } 147 148 /** 149 * Formats the specified {@link BigDecimal} using this format. 150 * 151 * <p> 152 * Returns either a {@link Number} (when {@link #NUMBER} or {@link #AUTO} produces a JS-safe value) or 153 * a {@link String} (when {@link #STRING} or {@link #AUTO} produces an out-of-range value). 154 * 155 * <p> 156 * For {@link #AUTO}: any non-zero-scale {@link BigDecimal} (i.e. has a fractional part) forces 157 * {@link #STRING} since fractional values cannot be safely represented in a JS double regardless of 158 * magnitude. Zero-scale values are tested against the integer {@code ±(2^53 − 1)} bound. 159 * 160 * @param value The value to format. Can be <jk>null</jk> (returns <jk>null</jk>). 161 * @param format The configured format. Can be <jk>null</jk> (treated as {@link #NUMBER}). 162 * @return The formatted value (a {@link BigDecimal} for numeric output, a {@link String} for string output), 163 * or <jk>null</jk> if {@code value} is <jk>null</jk>. 164 */ 165 public static Object format(BigDecimal value, BigNumberFormat format) { 166 if (value == null) 167 return null; 168 var fmt = format == null ? NUMBER : format; 169 return switch (fmt) { 170 case NOT_SET, NUMBER -> value; 171 case STRING -> value.toPlainString(); 172 case AUTO -> { 173 if (value.scale() > 0) 174 yield value.toPlainString(); 175 yield isJsSafe(value.toBigInteger()) ? (Object) value : value.toPlainString(); 176 } 177 }; 178 } 179 180 /** 181 * Parses the specified wire value into a {@link BigInteger} or {@link BigDecimal}. 182 * 183 * <p> 184 * Accepts both bare numeric and quoted-string input regardless of the {@code format} hint. The hint is 185 * informational only. 186 * 187 * @param <T> The target type ({@link BigInteger} or {@link BigDecimal}). 188 * @param value The wire value. Can be <jk>null</jk> or blank (returns <jk>null</jk>). 189 * @param format The configured format (informational only — parsing is format-agnostic). Can be <jk>null</jk> (ignored; parsing is format-agnostic). 190 * @param targetType The desired return type — {@link BigInteger} or {@link BigDecimal}. 191 * @return The parsed value, or <jk>null</jk> if {@code value} is <jk>null</jk> or blank. 192 * @throws IllegalArgumentException If the value cannot be parsed as a number or {@code targetType} is unsupported. 193 */ 194 @SuppressWarnings({ 195 "unchecked" // Cast is safe: parameterized by caller. 196 }) 197 public static <T extends Number> T parse(String value, BigNumberFormat format, Class<T> targetType) { 198 if (value == null) 199 return null; 200 var s = value.trim(); 201 if (s.isEmpty()) 202 return null; 203 // Strip surrounding quotes if present (lenient parsing — quoted-string and bare-numeric both accepted). 204 if (s.length() >= 2 && (s.charAt(0) == '"' || s.charAt(0) == '\'') && s.charAt(s.length() - 1) == s.charAt(0)) 205 s = s.substring(1, s.length() - 1).trim(); 206 try { 207 if (BigInteger.class.equals(targetType)) 208 return (T) new BigInteger(s); 209 if (BigDecimal.class.equals(targetType)) 210 return (T) new BigDecimal(s); 211 throw iaex("Unsupported BigNumberFormat target type: %s", targetType.getName()); 212 } catch (NumberFormatException e) { 213 throw iaex("Invalid big-number value '%s' for format %s: %s", value, format, e.getMessage()); 214 } 215 } 216 217 /** 218 * Returns <jk>true</jk> if this format always emits a numeric wire value. 219 * 220 * <p> 221 * {@link #NUMBER} returns <jk>true</jk>; {@link #STRING} returns <jk>false</jk>. {@link #AUTO} 222 * returns <jk>false</jk> conservatively — its {@code format(...)} return type varies per value, so 223 * serializers should branch on the actual runtime type returned by 224 * {@link #format(BigInteger, BigNumberFormat)} / {@link #format(BigDecimal, BigNumberFormat)} rather 225 * than this hint. 226 * 227 * @return <jk>true</jk> if this format emits a bare numeric wire value. 228 */ 229 public boolean isNumeric() { 230 return this == NUMBER; 231 } 232 233 private static boolean isJsSafe(BigInteger value) { 234 return value.compareTo(BIG_JS_MIN_SAFE) >= 0 && value.compareTo(BIG_JS_MAX_SAFE) <= 0; 235 } 236}