001package Torello.JSON;
002
003import javax.json.*;
004import java.math.BigDecimal;
005import java.math.BigInteger;
006import java.util.function.Function;
007
008import static javax.json.JsonValue.ValueType.*;
009import static Torello.JSON.JFlag.*;
010import static Torello.JSON.RJInternal.*;
011
012/**
013 * Builds on the J2EE Standard Release JSON Parsing Tools by providing additional
014 * help with converting JSON Data into the Best-Fit
015 * <B STYLE='color: red'><CODE>java.lang.Number</CODE></B>
016 * 
017 * <EMBED CLASS='external-html' DATA-FILE-ID=ALL_CLASSES_NOTE>
018 * <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUMBER_JSON>
019 * 
020 * @see Json
021 * @see JsonObject
022 * @see JsonArray
023 */
024@Torello.JavaDoc.Annotations.StaticFunctional
025public class ReadNumberJSON
026{
027    // This is a static class.  Has no program state.
028    private ReadNumberJSON() { }
029
030
031    // ********************************************************************************************
032    // ********************************************************************************************
033    // Number from JsonArray
034    // ********************************************************************************************
035    // ********************************************************************************************
036
037
038    /**
039     * Retrieve a {@link JsonArray} element, and transform it to a {@code java.lang.Number}.
040     * <EMBED CLASS=defs DATA-JTYPE=JsonNumber DATA-TYPE='java.lang.Number'>
041     * <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_NOTE>
042     * 
043     * @param ja            Any instance of {@link JsonArray}
044     * @param index         A valid index into {@code 'ja'}
045     * @param throwOnNull   Asks that an exception throw if Json-Null is encountered
046     * @return              <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_RET_JA>
047     * 
048     * @throws IndexOutOfBoundsException If {@code 'index'} is out of the bounds of {@code 'ja'}
049     * @throws JsonTypeArrException      <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_JTAEX>
050     * @throws JsonNullArrException      <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_JNAEX>
051     * 
052     * @see JsonValue#getValueType()
053     * @see #convertToNumber(JsonNumber) 
054     */
055    public static Number get(final JsonArray ja, final int index, final boolean throwOnNull)
056    {
057        // This will throw an IndexOutOfBoundsException if the index is out of bounds.
058        JsonValue jv = ja.get(index);
059
060        switch (jv.getValueType())
061        {
062            case NULL:
063
064                // This is simple-stuff (not rocket-science).  "Type Mapping" Code has to worry
065                // about what the meaning of "null" should be.
066
067                if (throwOnNull) throw new JsonNullArrException(ja, index, NUMBER, Number.class);
068                else return null;
069
070            case NUMBER: return convertToNumber((JsonNumber) jv);
071
072            // The JsonValue at the specified array-index does not contain a JsonString.
073            default: throw new JsonTypeArrException(ja, index, NUMBER, jv, Number.class);
074        }
075    }
076
077    /**
078     * Retrieve a {@link JsonArray} element, and transform it to a {@code java.lang.Number}.
079     * <EMBED CLASS=defs DATA-TYPE='java.lang.Number' DATA-JTYPE=JsonNumber>
080     * <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_NOTE>
081     * 
082     * @param ja            Any instance of {@link JsonArray}
083     * @param index         The array index containing the element to retrieve.
084     * @param FLAGS         Return-value / exception-throw constants defined in {@link JFlag}
085     * @param defaultValue  <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_DEF_VAL>
086     * @return  Best fit boxed number, or null.
087     * 
088     * @throws IndexOutOfBoundsException <EMBED CLASS='external-html' DATA-FILE-ID=IOOBEX>
089     * @throws JsonNullArrException      <EMBED CLASS='external-html' DATA-FILE-ID=JNAEX>
090     * @throws JsonTypeArrException      <EMBED CLASS='external-html' DATA-FILE-ID=JTAEX>
091     * 
092     * @see ReadBoxedJSON#GET(JsonArray, int, int, Number, Class, Function, Function)
093     * @see #convertToNumber(JsonNumber)
094     */
095    public static Number get(
096            final JsonArray ja,
097            final int       index,
098            final int       FLAGS,
099            final Number    defaultValue
100        )
101    {
102        return ReadBoxedJSON.GET
103            (ja, index, FLAGS, defaultValue, Number.class, ReadNumberJSON::convertToNumber, null);
104    }
105
106    /**
107     * Retrieve a {@link JsonArray} element containing a {@link JsonString}, and transform it to a
108     * {@code java.lang.Number}, with either a user-provided parser, or the standard java parser
109     * 
110     * <EMBED CLASS=defs DATA-TYPE='java.lang.Number' DATA-JTYPE=JsonString
111     *  DATA-PARSER='new BigDecimal'>
112     * 
113     * <BR /><BR />If {@code 'parser'} is passed null, the default parser is used, which is just
114     * the {@code java.math.BigDecimal} constructor which accepts a {@code String}.  What is done
115     * with the parsed {@code BigDecimal} instance is explained below.
116     * 
117     * <BR /><BR /><EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_PARSE_DESC>
118     * 
119     * @param ja             Any instance of {@link JsonArray}
120     * @param index          The array index containing the {@link JsonString} element to retrieve.
121     * @param FLAGS          Return-value / exception-throw constants defined in {@link JFlag}
122     * @param defaultValue   <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_DEF_VAL>
123     * @param optionalParser <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_PARSER>
124     * @return               <EMBED CLASS='external-html' DATA-FILE-ID=PARSE_BOXED_RET_JA>
125     * 
126     * @throws IndexOutOfBoundsException <EMBED CLASS='external-html' DATA-FILE-ID=IOOBEX>
127     * @throws JsonStrParseArrException  <EMBED CLASS='external-html' DATA-FILE-ID=JSPAEX>
128     * @throws JsonNullArrException      <EMBED CLASS='external-html' DATA-FILE-ID=JNAEX>
129     * @throws JsonTypeArrException      <EMBED CLASS='external-html' DATA-FILE-ID=JTAEX>
130     * 
131     * @see ParseBoxedJSON#PARSE(JsonObject, String, int, Number, Class, Function, Function, Function)
132     * @see #convertToNumber(String)
133     */
134    public static Number parse(
135            final JsonArray                 ja, 
136            final int                       index,
137            final int                       FLAGS,
138            final Number                    defaultValue,
139            final Function<String, Number>  optionalParser
140        )
141    {
142        return ParseBoxedJSON.PARSE(
143            ja, index, FLAGS, defaultValue, Number.class, optionalParser,
144            ReadNumberJSON::convertToNumber, null  /* Not Needed, convertToNumber won't throw */
145        );
146    }
147
148
149    // ********************************************************************************************
150    // ********************************************************************************************
151    // Number from JsonObject
152    // ********************************************************************************************
153    // ********************************************************************************************
154
155
156    /**
157     * Extract a {@link JsonObject} property, and transform it to a {@code java.lang.Number}.
158     * <EMBED CLASS=defs DATA-TYPE='java.lang.Number' DATA-JTYPE=JsonNumber>
159     * <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_NOTE>
160     * 
161     * @param jo            Any instance of {@link JsonObject}.
162     * @param propertyName  Any property name contained by {@code 'jo'}
163     * @param isOptional    <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_IS_OPTIONAL>
164     * @param throwOnNull   <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_THROW_ON_JO>
165     * @return              <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_RET_JO>
166     * 
167     * @throws JsonPropMissingException <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_JPMEX>
168     * @throws JsonTypeObjException     <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_JTOEX>
169     * @throws JsonNullObjException     <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_JNOEX>
170     * 
171     * @see JsonValue#getValueType()
172     * @see #convertToNumber(JsonNumber)
173     */
174    public static Number get(
175            final JsonObject    jo,
176            final String        propertyName,
177            final boolean       isOptional,
178            final boolean       throwOnNull
179        )
180    {
181        if (! jo.containsKey(propertyName))
182        {
183            if (isOptional) return null;
184            else throw new JsonPropMissingException(jo, propertyName, NUMBER, Number.class);
185        }
186
187        JsonValue jv = jo.get(propertyName);
188
189        switch (jv.getValueType())
190        {
191            case NULL:
192                // This is simple-stuff (not rocket-science).  "Type Mapping" Code has to worry
193                // about what the meaning of "null" should be.
194
195                if (throwOnNull) throw new JsonNullObjException
196                    (jo, propertyName, NUMBER, Number.class);
197
198                else return null;
199
200            case NUMBER: return convertToNumber((JsonNumber) jv);
201
202            // The JsonObject propertydoes not contain a JsonNumber.
203            default: throw new JsonTypeObjException
204                (jo, propertyName, NUMBER, jv, Number.class);
205        }
206    }
207
208    /**
209     * Retrieve a {@link JsonObject} property, and transform it to a {@code java.lang.Number}.
210     * <EMBED CLASS=defs DATA-TYPE='java.lang.Number' DATA-JTYPE=JsonNumber>
211     * <EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_NOTE>
212     * 
213     * @param jo            Any instance of {@link JsonObject}
214     * @param propertyName  Any property name contained by {@code 'jo'}
215     * @param FLAGS         Return-value / exception-throw constants defined in {@link JFlag}
216     * @param defaultValue  <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_DEF_VAL>
217     * @return Best fit boxed number, or null.
218     * 
219     * @throws JsonPropMissingException <EMBED CLASS='external-html' DATA-FILE-ID=JPMEX>
220     * @throws JsonNullObjException     <EMBED CLASS='external-html' DATA-FILE-ID=JNOEX>
221     * @throws JsonTypeObjException     <EMBED CLASS='external-html' DATA-FILE-ID=JTOEX>
222     * 
223     * @see ReadBoxedJSON#GET(JsonObject, String, int, Number, Class, Function, Function)
224     * @see #convertToNumber(JsonNumber)
225     */
226    public static Number get(
227            final JsonObject    jo,
228            final String        propertyName,
229            final int           FLAGS,
230            final Number        defaultValue
231        )
232    {
233        return ReadBoxedJSON.GET(
234            jo, propertyName, FLAGS, defaultValue, Number.class,
235            ReadNumberJSON::convertToNumber,
236            null
237        );
238    }
239
240    /**
241     * Retrieve a {@link JsonObject} property containing a {@link JsonString}, and transform it to
242     * a {@code java.lang.Number}, with either a user-provided parser, or the standard java
243     * parser
244     * 
245     * <EMBED CLASS=defs DATA-TYPE='java.lang.Number' DATA-JTYPE=JsonString
246     *  DATA-PARSER='new BigDecimal'>
247     * 
248     * <BR /><BR />If {@code 'parser'} is passed null, the default parser is used, which is just
249     * the {@code java.math.BigDecimal} constructor which accepts a {@code String}.  What is done
250     * with the parsed {@code BigDecimal} instance is explained below.
251     * 
252     * <BR /><BR /><EMBED CLASS='external-html' DATA-FILE-ID=READ_NUM_PARSE_DESC>
253     * 
254     * @param jo            Any instance of {@link JsonObject}
255     * @param propertyName  Any property name contained by {@code 'jo'}
256     * @param FLAGS         Return-value / exception-throw constants defined in {@link JFlag}
257     * @param defaultValue  <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_DEF_VAL>
258     * @param optionalParser <EMBED CLASS='external-html' DATA-FILE-ID=COMMON_PARSER>
259     * @return              <EMBED CLASS='external-html' DATA-FILE-ID=PARSE_BOXED_RET_JO>
260     * 
261     * @throws JsonPropMissingException <EMBED CLASS='external-html' DATA-FILE-ID=JPMEX>
262     * @throws JsonStrParseObjException <EMBED CLASS='external-html' DATA-FILE-ID=JSPOEX>
263     * @throws JsonNullObjException     <EMBED CLASS='external-html' DATA-FILE-ID=JNOEX>
264     * @throws JsonTypeObjException     <EMBED CLASS='external-html' DATA-FILE-ID=JTOEX>
265     * 
266     * @see ParseBoxedJSON#PARSE(JsonObject, String, int, Number, Class, Function, Function, Function)
267     * @see #convertToNumber(String)
268     */
269    public static Number parse(
270            final JsonObject                jo,
271            final String                    propertyName,
272            final int                       FLAGS,
273            final Number                    defaultValue,
274            final Function<String, Number>  optionalParser
275        )
276    {
277        return ParseBoxedJSON.PARSE(
278            jo, propertyName, FLAGS, defaultValue, Number.class, optionalParser,
279            ReadNumberJSON::convertToNumber,
280            null /* Not Needed, convertToNumber won't throw */
281        );
282    }
283
284
285    // ********************************************************************************************
286    // ********************************************************************************************
287    // Number from JsonString Parse  **OR**  from JsonNumber
288    // ********************************************************************************************
289    // ********************************************************************************************
290
291
292    /**
293     * <EMBED CLASS=defs DATA-TYPE=Number DATA-PARSER='new BigDecimal'>
294     * <EMBED CLASS='external-html' DATA-FILE-ID=RORP_BOXED_WF_JA>
295     * 
296     * @param ja             Any {@link JsonArray}
297     * @param i              Any index into the {@code JsonArray}
298     * @param FLAGS          Return-value / exception-throw constants defined in {@link JFlag}
299     * @param defaultValue   <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_DEFVAL>
300     * @param optionalParser <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_PARSER>
301     * @return               <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_RET_JA>
302     * 
303     * @throws IndexOutOfBoundsException <EMBED CLASS='external-html' DATA-FILE-ID=IOOBEX>
304     * @throws JsonStrParseArrException  <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_JSPAEX>
305     * @throws JsonNullArrException      <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_JNAEX>
306     * @throws JsonTypeArrException      <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_JTAEX>
307     * 
308     * @see #get(JsonArray, int, int, Number)
309     * @see #parse(JsonArray, int, int, Number, Function)
310     */
311    public static Number get(
312            final JsonArray             ja,
313            final int                   i,
314            final int                   FLAGS,
315            final Number                defaultValue,
316            Function<String, Number>    optionalParser
317        )
318    {
319        JsonValue.ValueType t;
320
321        return ((i >= ja.size()) || ((t=ja.get(i).getValueType()) == NUMBER) || (t != STRING))
322            ? get(ja, i, FLAGS, defaultValue)
323            : parse(ja, i, FLAGS, defaultValue, optionalParser);
324    }
325
326    /**
327     * <EMBED CLASS=defs DATA-TYPE=Number DATA-PARSER='new BigDecimal'>
328     * <EMBED CLASS='external-html' DATA-FILE-ID=RORP_BOXED_WF_JO>
329     * 
330     * @param jo             Any {@link JsonObject}
331     * @param propertyName   Any of the properties defined in the {@code JsonObject}
332     * @param FLAGS          Return-value / exception-throw constants defined in {@link JFlag}
333     * @param defaultValue   <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_DEFVAL>
334     * @param optionalParser <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_PARSER>
335     * @return               <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_RET_JO>
336     * 
337     * @throws JsonPropMissingException <EMBED CLASS='external-html' DATA-FILE-ID=JPMEX>
338     * @throws JsonStrParseObjException <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_JSPOEX>
339     * @throws JsonNullObjException     <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_JNOEX>
340     * @throws JsonTypeObjException     <EMBED CLASS='external-html' DATA-FILE-ID=ROPB_JTOEX>
341     * 
342     * @see #get(JsonObject, String, int, Number)
343     * @see #parse(JsonObject, String, int, Number, Function)
344     */
345    public static Number get(
346            final JsonObject                jo,
347            final String                    propertyName,
348            final int                       FLAGS,
349            final Number                    defaultValue,
350            final Function<String, Number>  optionalParser
351        )
352    {
353        JsonValue.ValueType t;
354
355        return (    (! jo.containsKey(propertyName))
356                ||  ((t = jo.get(propertyName).getValueType()) == NUMBER)
357                ||  (t != STRING)
358            )
359            ? get(jo, propertyName, FLAGS, defaultValue)
360            : parse(jo, propertyName, FLAGS, defaultValue, optionalParser);
361    }
362
363
364    // ********************************************************************************************
365    // ********************************************************************************************
366    // Internal Use Only Helpers
367    // ********************************************************************************************
368    // ********************************************************************************************
369
370
371    /**
372     * Converts any {@link JsonNumber} into one of the inheriting subclasses of Java class
373     * {@code Number}
374     * @param jn Any {@link JsonNumber}
375     * @return The most appropriate intance of {@code java.lang.Number}
376     * @see #get(JsonObject, String, int, Number)
377     * @see ]#get(JsonArray, int, int, Number)
378     * @see JsonNumber#isIntegral()
379     * @see JsonNumber#bigIntegerValue()
380     * @see JsonNumber#bigDecimalValue()
381     */
382    protected static Number convertToNumber(final JsonNumber jn)
383    {
384        if (jn.isIntegral())
385        {
386            BigInteger bi = jn.bigIntegerValue();
387            int        l  = bi.bitLength();
388
389            if (l <= 32) return Integer.valueOf(bi.intValue());
390            if (l <= 64) return Long.valueOf(bi.longValue());
391
392            return bi;
393        }
394
395        else
396        {
397            BigDecimal bd = jn.bigDecimalValue();
398
399            // This probably isn't the most efficient thing I've ever written, but I do not
400            // have the energy to stare at java.math.BigDecimal at the moment.  The JavaDoc for
401            // this JSON => Java-Type Conversion is quite intricate.  I will figure this out at
402            // at later date.
403
404            float f = bd.floatValue();
405
406            if (    (! Float.isInfinite(f))
407                &&  (BigDecimal.valueOf(f).compareTo(bd) == 0)
408            )
409                return (Float) f;
410
411            double d = bd.doubleValue();
412
413            if (    (! Double.isInfinite(d))
414                &&  (BigDecimal.valueOf(d).compareTo(bd) == 0)
415            )
416                return (Double) d;
417
418            return bd;
419        }
420    }
421
422    /**
423     * Converts any {@code java.lang.String} into one of the inheriting subclasses of Java class
424     * {@code Number}
425     * @param s Any {@code String}
426     * @return The most appropriate instance of {@code java.lang.Number}
427     * @throws NumberFormatException If the input {@code String} isn't properly formatted as a
428     * number.
429     * @see ReadNumberJSON#parse(JsonObject, String, int, Number, Function)
430     * @see ReadNumberJSON#parse(JsonArray, int, int, Number, Function)
431     */
432    protected static Number convertToNumber(final String s)
433    { return convertToNumber(new BigDecimal(s.trim())); }
434
435    /**
436     * Converts any {@code java.math.BigDecimal} into one of the inheriting subclasses of
437     * {@code Number}.
438     * @param bd Any {@code BigDecimal}
439     * @return The most appropriate instance of {@code java.lang.Number}
440     */
441    protected static Number convertToNumber(final BigDecimal bd)
442    {
443        if (bd.scale() == 0)
444        {
445            BigInteger bi = bd.toBigInteger();
446            int        l  = bi.bitLength();
447
448            if (l <= 32) return Integer.valueOf(bi.intValue());
449            if (l <= 64) return Long.valueOf(bi.longValue());
450            return bi;
451        }
452
453        else
454        {
455            // This probably isn't the most efficient thing I've ever written, but I do not
456            // have the energy to stare at java.math.BigDecimal at the moment.  The JavaDoc for
457            // this JSON => Java-Type Conversion is quite intricate.  I will figure this out at
458            // at later date.
459
460            float f = bd.floatValue();
461
462            if (    (! Float.isInfinite(f))
463                &&  (BigDecimal.valueOf(f).compareTo(bd) == 0)
464            )
465                return (Float) f;
466
467            double d = bd.doubleValue();
468
469            if (    (! Double.isInfinite(d))
470                &&  (BigDecimal.valueOf(d).compareTo(bd) == 0)
471            )
472                return (Double) d;
473
474            return bd;
475        }
476    }
477
478}