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.util.Locale; 022 023/** 024 * Supported wire formats for non-finite {@link Float} / {@link Double} values 025 * ({@link Double#NaN NaN}, {@link Double#POSITIVE_INFINITY +Infinity}, {@link Double#NEGATIVE_INFINITY -Infinity}). 026 * 027 * <p> 028 * Used by {@link MarshallingContext.Builder#floatFormat(FloatFormat)}, 029 * {@link Marshalled#floatFormat()}, 030 * {@link MarshalledProp#floatFormat()}, and 031 * {@link MarshalledConfig#floatFormat()} to control how non-finite 032 * floating-point values are written to text-based wire formats. Finite values are unaffected and ride 033 * the natural bare-numeric-token wire form for the underlying serializer. 034 * 035 * <p> 036 * The default is {@link #NaN_AS_NULL} which is JSON-spec-compliant per RFC 8259 §6 (non-finite floats 037 * are not valid JSON tokens; emitting {@code null} is the closest valid representation). 038 * 039 * <h5 class='topic'>Precedence (highest to lowest)</h5> 040 * <ol> 041 * <li>{@link MarshalledProp#floatFormat() @MarshalledProp(floatFormat=…)} on the bean property. 042 * <li>{@link Marshalled#floatFormat() @Marshalled(floatFormat=…)} on the bean class. 043 * <li>{@link MarshalledConfig#floatFormat() @MarshalledConfig(floatFormat=…)} on 044 * <code><ja>@Rest</ja></code>-annotated classes / methods. 045 * <li>Programmatic {@link MarshallingContext.Builder#floatFormat(FloatFormat)}. 046 * <li>Environment variable <c>MarshallingContext.floatFormat</c>. 047 * <li>The default constant ({@link #NaN_AS_NULL}). 048 * </ol> 049 * 050 * <h5 class='topic'>Parser leniency</h5> 051 * 052 * <p> 053 * Parsers SHALL accept all of the following wire shapes regardless of the parser-side 054 * {@code FloatFormat} setting: 055 * <ul> 056 * <li>Bare numeric tokens including the non-standard {@code NaN} / {@code Infinity} / {@code -Infinity} tokens 057 * (Juneau's lenient parser accepts these by default). 058 * <li>Quoted-string forms — {@code "NaN"}, {@code "Infinity"}, {@code "-Infinity"} (case-insensitive). 059 * <li>{@code null} — reconstructed as {@link Double#NaN} when reading into a primitive {@code double} / {@code float}; 060 * preserved as {@code null} when reading into a boxed {@link Double} / {@link Float}. 061 * </ul> 062 * 063 * <h5 class='topic'>JSON-spec compliance</h5> 064 * 065 * <p> 066 * {@link #NaN_AS_NUMBER} is <b>non-standard JSON</b> per RFC 8259 §6 — JSON does not permit {@code NaN} / 067 * {@code Infinity} / {@code -Infinity} tokens. Juneau's own JSON parser accepts these in lenient mode, 068 * but strict-mode consumers (e.g. some browser {@code JSON.parse} pipelines) will reject them. Use only 069 * when you control both ends of the wire. 070 * 071 * <h5 class='topic'>Binary serializers</h5> 072 * 073 * <p> 074 * Binary serializers (BSON / CBOR / MsgPack / Prototext / Parquet) emit native IEEE-754 floats regardless 075 * of this setting; native non-finite representations exist in every supported binary format (BSON 076 * {@code double} / {@code decimal128}; CBOR's tag 22 / 23 / 24 for special floats; MsgPack {@code float64}). 077 * This setting only affects text-based serializers. 078 */ 079@SuppressWarnings({ 080 "java:S115" // NaN_AS_* enum constants use the IEEE 754 abbreviation 'NaN' which includes lowercase letters; intentional 081}) 082public enum FloatFormat { 083 084 /** Sentinel meaning "no value configured" — falls through to the next-higher precedence level. */ 085 NOT_SET, 086 087 /** 088 * Emit {@code null} for {@code NaN} / {@code ±Infinity} (the default). 089 * 090 * <p> 091 * <b>JSON-spec compliant.</b> Loses the ability to distinguish {@code NaN} from 092 * {@code +Infinity} from {@code -Infinity} on the wire — all three become {@code null}. Round-trips 093 * back to {@link Double#NaN} when reading into a primitive {@code double}; into a boxed {@link Double} 094 * field, {@code null} stays {@code null}. 095 */ 096 NaN_AS_NULL, 097 098 /** 099 * Emit the quoted string {@code "NaN"} / {@code "Infinity"} / {@code "-Infinity"}. 100 * 101 * <p> 102 * <b>Round-trip preserving.</b> The parser unambiguously decodes the string back to the original 103 * non-finite value. 104 */ 105 NaN_AS_STRING, 106 107 /** 108 * Emit the bare {@code NaN} / {@code Infinity} / {@code -Infinity} token. 109 * 110 * <p> 111 * <b>Non-standard JSON</b> per RFC 8259 §6. Accepted by Juneau's lenient parser and V8 / several 112 * Python libs; rejected by strict consumers. Use only when you control both ends of the wire. 113 */ 114 NaN_AS_NUMBER, 115 116 /** 117 * Throw {@link IllegalArgumentException} when a non-finite value is encountered. 118 * 119 * <p> 120 * <b>Fail-fast</b> for data-cleanliness pipelines where a {@code NaN} indicates upstream corruption. 121 */ 122 NaN_AS_ERROR; 123 124 /** 125 * Formats the specified {@code double} using this format. 126 * 127 * <p> 128 * Finite values always return the boxed {@link Double} so text serializers emit the bare numeric 129 * token. Non-finite values are dispatched per the constant: 130 * <ul> 131 * <li>{@link #NaN_AS_NULL} → {@code null}. 132 * <li>{@link #NaN_AS_STRING} → {@code "NaN"} / {@code "Infinity"} / {@code "-Infinity"}. 133 * <li>{@link #NaN_AS_NUMBER} → boxed {@link Double} (non-finite); text serializers emit the bare token. 134 * <li>{@link #NaN_AS_ERROR} → throws {@link IllegalArgumentException}. 135 * </ul> 136 * 137 * <p> 138 * {@link #NOT_SET} / {@code null} format are treated as {@link #NaN_AS_NULL}. 139 * 140 * @param value The value to format. 141 * @param format The configured format. Can be <jk>null</jk> (treated as {@link #NaN_AS_NULL}). 142 * @return The formatted value — a {@link Double} for finite or {@code NaN_AS_NUMBER}, a {@link String} 143 * for {@code NaN_AS_STRING}, or {@code null} for non-finite under {@code NaN_AS_NULL}. 144 * @throws IllegalArgumentException If {@code format} is {@link #NaN_AS_ERROR} and the value is non-finite. 145 */ 146 public static Object format(double value, FloatFormat format) { 147 if (Double.isFinite(value)) 148 return Double.valueOf(value); 149 return formatNonFinite(value, format); 150 } 151 152 /** 153 * Formats the specified {@code float} using this format. 154 * 155 * <p> 156 * Finite values return the boxed {@link Float}. Non-finite values follow the same rules as 157 * {@link #format(double, FloatFormat)} except that {@link #NaN_AS_NUMBER} returns the boxed 158 * {@link Float} (not {@link Double}). 159 * 160 * @param value The value to format. 161 * @param format The configured format. Can be <jk>null</jk> (treated as {@link #NaN_AS_NULL}). 162 * @return The formatted value — a {@link Float} for finite or {@code NaN_AS_NUMBER}, a {@link String} 163 * for {@code NaN_AS_STRING}, or {@code null} for non-finite under {@code NaN_AS_NULL}. 164 * @throws IllegalArgumentException If {@code format} is {@link #NaN_AS_ERROR} and the value is non-finite. 165 */ 166 public static Object format(float value, FloatFormat format) { 167 if (Float.isFinite(value)) 168 return Float.valueOf(value); 169 var fmt = format == null ? NaN_AS_NULL : format; 170 return switch (fmt) { 171 case NOT_SET, NaN_AS_NULL -> null; 172 case NaN_AS_STRING -> nonFiniteTokenFloat(value); 173 case NaN_AS_NUMBER -> Float.valueOf(value); 174 case NaN_AS_ERROR -> throw iaex("Non-finite float value %s rejected by FloatFormat.NaN_AS_ERROR", value); 175 }; 176 } 177 178 private static Object formatNonFinite(double value, FloatFormat format) { 179 var fmt = format == null ? NaN_AS_NULL : format; 180 return switch (fmt) { 181 case NOT_SET, NaN_AS_NULL -> null; 182 case NaN_AS_STRING -> nonFiniteToken(value); 183 case NaN_AS_NUMBER -> Double.valueOf(value); 184 case NaN_AS_ERROR -> throw iaex("Non-finite double value %s rejected by FloatFormat.NaN_AS_ERROR", value); 185 }; 186 } 187 188 private static String nonFiniteToken(double value) { 189 if (Double.isNaN(value)) 190 return "NaN"; 191 return value > 0 ? "Infinity" : "-Infinity"; 192 } 193 194 private static String nonFiniteTokenFloat(float value) { 195 if (Float.isNaN(value)) 196 return "NaN"; 197 return value > 0 ? "Infinity" : "-Infinity"; 198 } 199 200 /** 201 * Parses the specified wire value into a non-finite-aware {@link Double} or {@link Float}. 202 * 203 * <p> 204 * Lenient parsing — accepts bare numeric, quoted-string {@code "NaN"} / {@code "Infinity"} / 205 * {@code "-Infinity"} (case-insensitive), and unquoted {@code NaN} / {@code Infinity} / {@code -Infinity} 206 * tokens regardless of the {@code format} hint. The hint is informational only. 207 * 208 * @param <T> The target type ({@link Double} or {@link Float}). 209 * @param value The wire value. Can be <jk>null</jk> or blank (returns <jk>null</jk>). 210 * @param format The configured format hint (informational only — parsing is format-agnostic). Can be <jk>null</jk> (ignored; parsing is format-agnostic). 211 * @param targetType The desired return type — {@link Double} or {@link Float}. 212 * @return The parsed value, or <jk>null</jk> if {@code value} is <jk>null</jk> or blank. 213 * @throws IllegalArgumentException If the value cannot be parsed or {@code targetType} is unsupported. 214 */ 215 @SuppressWarnings({ 216 "unchecked" // Cast is safe: parameterized by caller. 217 }) 218 public static <T extends Number> T parse(String value, FloatFormat format, Class<T> targetType) { 219 if (value == null) 220 return null; 221 var s = value.trim(); 222 if (s.isEmpty()) 223 return null; 224 // Strip surrounding quotes if present (lenient parsing — quoted-string and bare-numeric both accepted). 225 if (s.length() >= 2 && (s.charAt(0) == '"' || s.charAt(0) == '\'') && s.charAt(s.length() - 1) == s.charAt(0)) 226 s = s.substring(1, s.length() - 1).trim(); 227 try { 228 if (Double.class.equals(targetType) || double.class.equals(targetType)) 229 return (T) Double.valueOf(parseDouble(s)); 230 if (Float.class.equals(targetType) || float.class.equals(targetType)) 231 return (T) Float.valueOf(parseFloat(s)); 232 throw iaex("Unsupported FloatFormat target type: %s", targetType.getName()); 233 } catch (NumberFormatException e) { 234 throw iaex("Invalid float value '%s' for format %s: %s", value, format, e.getMessage()); 235 } 236 } 237 238 private static double parseDouble(String s) { 239 var lower = s.toLowerCase(Locale.ROOT); 240 return switch (lower) { 241 case "nan" -> Double.NaN; 242 case "infinity", "+infinity", "inf", "+inf" -> Double.POSITIVE_INFINITY; 243 case "-infinity", "-inf" -> Double.NEGATIVE_INFINITY; 244 default -> Double.parseDouble(s); 245 }; 246 } 247 248 private static float parseFloat(String s) { 249 var lower = s.toLowerCase(Locale.ROOT); 250 return switch (lower) { 251 case "nan" -> Float.NaN; 252 case "infinity", "+infinity", "inf", "+inf" -> Float.POSITIVE_INFINITY; 253 case "-infinity", "-inf" -> Float.NEGATIVE_INFINITY; 254 default -> Float.parseFloat(s); 255 }; 256 } 257 258 /** 259 * Returns <jk>true</jk> if this format always emits a bare numeric wire token for non-finite values. 260 * 261 * <p> 262 * Only {@link #NaN_AS_NUMBER} returns <jk>true</jk>. {@link #NaN_AS_NULL} may emit {@code null}; 263 * {@link #NaN_AS_STRING} emits a quoted string; {@link #NaN_AS_ERROR} throws. Finite values are 264 * always emitted as bare numeric tokens regardless of this setting. 265 * 266 * @return <jk>true</jk> if this format emits a bare numeric wire token for non-finite values. 267 */ 268 public boolean isNumeric() { 269 return this == NaN_AS_NUMBER; 270 } 271}