View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.jexl3.parser;
18  
19  import java.io.Serializable;
20  import java.math.BigDecimal;
21  import java.math.BigInteger;
22  import java.text.DecimalFormat;
23  import java.text.DecimalFormatSymbols;
24  import java.util.Locale;
25  
26  import org.apache.commons.jexl3.JexlArithmetic;
27  import org.apache.commons.jexl3.JexlEngine;
28  
29  /**
30   * Parses number literals.
31   */
32  public final class NumberParser implements Serializable {
33  
34      /**
35       */
36      private static final long serialVersionUID = 1L;
37  
38  
39      /**
40       * Returns the maximum digit count allowed for a BigInteger literal in the given base.
41       * When a JEXL engine with a bounded MathContext is active on the current thread, the
42       * engine's precision is converted to an equivalent bit limit, then to max digits for the base.
43       * Otherwise MAX_BIGINTEGER_DIGITS applies.
44       * At runtime, JexlArithmetic.checkBigIntegerPrecision() enforces the bit-length limit (f014, f013).
45       */
46      private static int maxBigIntegerDigits(final int base) {
47          final JexlEngine engine = JexlEngine.getThreadEngine();
48          if (engine != null) {
49              final long precision = engine.getArithmetic().getMathContext().getPrecision();
50              if (precision > 0 && precision < Integer.MAX_VALUE) {
51                  // Use the same bit formula as checkBigIntegerPrecision: precision * 10/3 + 1 bits
52                  // For a given base B with log2(B) bits per digit, max_digits = max_bits / log2(B)
53                  final int maxBits = (int) (precision * 10 / 3 + 1);
54                  final int bitsPerDigit;
55                  if (base == 16) {
56                      bitsPerDigit = 4;  // log2(16) = 4
57                  } else if (base == 8) {
58                      bitsPerDigit = 3;  // log2(8) = 3
59                  } else {
60                      // base 10: log2(10) ≈ 3.32, approximate as 10/3
61                      return (maxBits * 3) / 10;
62                  }
63                  return maxBits / bitsPerDigit;
64              }
65          }
66          return JexlArithmetic.MAX_BIGINTEGER_DIGITS;
67      }
68  
69      /** JEXL locale-neutral big decimal format. */
70      static final DecimalFormat BIGDF = new DecimalFormat("0.0b", new DecimalFormatSymbols(Locale.ROOT));
71      private static boolean isNegative(final Token token) {
72          return token != null && "-".equals(token.image);
73      }
74      static Number parseDouble(final Token negative, final Token s) {
75          return new NumberParser().assignReal(isNegative(negative), s.image).getLiteralValue();
76      }
77  
78      static Number parseInteger(final Token negative, final Token s) {
79          return new NumberParser().assignNatural(isNegative(negative), s.image).getLiteralValue();
80      }
81  
82      public NumberParser() {
83          this(null);
84      }
85  
86      public NumberParser(final Number number) {
87          if (number != null) {
88              this.literal = number;
89              this.clazz = number.getClass();
90          } else {
91              this.literal = null;
92              this.clazz = null;
93          }
94      }
95  
96      /** The type literal value. */
97      private Number literal;
98  
99      /** The expected class. */
100     private Class<? extends Number> clazz;
101 
102     /**
103      * Sets this node as a natural literal.
104      * Originally from OGNL.
105      *
106      * @param negative whether the natural should be negative
107      * @param natural The natural as string
108      * @return this parser instance
109      */
110     NumberParser assignNatural(final boolean negative, final String natural) {
111         String s = natural;
112         Number result;
113         Class<? extends Number> rclass;
114         // determine the base
115         final int base;
116         if (s.charAt(0) == '0') {
117             if (s.length() > 1 && (s.charAt(1) == 'x' || s.charAt(1) == 'X')) {
118                 base = 16;
119                 s = s.substring(2); // Trim the 0x off the front
120             } else {
121                 base = 8;
122             }
123         } else {
124             base = 10;
125         }
126         final int maxDigits = maxBigIntegerDigits(base);
127         // switch on suffix if any
128         final int last = s.length() - 1;
129         switch (s.charAt(last)) {
130             case 'l':
131             case 'L': {
132                 rclass = Long.class;
133                 final long l = Long.parseLong(s.substring(0, last), base);
134                 result = negative? -l : l;
135                 break;
136             }
137             case 'h':
138             case 'H': {
139                 rclass = BigInteger.class;
140                 if (last > maxDigits) {
141                     throw new NumberFormatException(
142                       "BigInteger literal too long: " + last
143                       + " > " + maxDigits);
144                 }
145                 final BigInteger bi = new BigInteger(s.substring(0, last), base);
146                 result = negative? bi.negate() : bi;
147                 break;
148             }
149             default: {
150                 // preferred literal class is integer
151                 rclass = Integer.class;
152                 try {
153                     final int i = Integer.parseInt(s, base);
154                     result = negative? -i : i;
155                 } catch (final NumberFormatException take2) {
156                     try {
157                         final long l = Long.parseLong(s, base);
158                         result = negative? -l : l;
159                     } catch (final NumberFormatException take3) {
160                         if (s.length() > maxDigits) {
161                             throw new NumberFormatException(
162                               "BigInteger literal too long: " + s.length()
163                               + " > " + maxDigits);
164                         }
165                         final BigInteger bi = new BigInteger(s, base);
166                         result = negative? bi.negate() : bi;
167                     }
168                 }
169             }
170         }
171         literal = result;
172         clazz = rclass;
173         return this;
174     }
175 
176     /**
177      * Sets this node as an (optionally) signed natural literal.
178      * Originally from OGNL.
179      *
180      * @param str The natural as string
181      * @return this parser instance
182      */
183     NumberParser assignNatural(final String str) {
184         String s;
185         // determine negative sign if any, ignore +
186         final boolean negative;
187         switch (str.charAt(0)) {
188             case '-':
189                 negative = true;
190                 s = str.substring(1);
191                 break;
192             case '+':
193                 negative = false;
194                 s = str.substring(1);
195                 break;
196             default:
197                 negative = false;
198                 s = str;
199         }
200         return assignNatural(negative, s);
201     }
202 
203     /**
204      * Sets this node as a real literal.
205      * Originally from OGNL.
206      *
207      * @param negative whether the real should be negative
208      * @param s The real as string
209      * @return this parser instance
210      */
211     NumberParser assignReal(final boolean negative, final String s) {
212         Number result;
213         Class<? extends Number> rclass;
214         if ("#NaN".equals(s) || "NaN".equals(s)) {
215             result = Double.NaN;
216             rclass = Double.class;
217         } else {
218             final int last = s.length() - 1;
219             switch (s.charAt(last)) {
220                 case 'b':
221                 case 'B': {
222                     rclass = BigDecimal.class;
223                     final BigDecimal bd = new BigDecimal(s.substring(0, last));
224                     result = negative? bd.negate() : bd;
225                     break;
226                 }
227                 case 'f':
228                 case 'F': {
229                     rclass = Float.class;
230                     final float f4 = Float.parseFloat(s.substring(0, last));
231                     result = negative? -f4 : f4;
232                     break;
233                 }
234                 case 'd':
235                 case 'D':
236                     rclass = Double.class;
237                     final double f8 = Double.parseDouble(s.substring(0, last));
238                     result = negative? -f8 : f8;
239                     break;
240                 default: {
241                     // preferred literal class is double
242                     rclass = Double.class;
243                     try {
244                         final double d = Double.parseDouble(s);
245                         result = negative? -d : d;
246                     } catch (final NumberFormatException take3) {
247                         final BigDecimal bd = new BigDecimal(s);
248                         result = negative? bd.negate() : bd;
249                     }
250                     break;
251                 }
252             }
253         }
254         literal = result;
255         clazz = rclass;
256         return this;
257     }
258 
259     /**
260      * Sets this node as an (optionally) signed real literal.
261      * Originally from OGNL.
262      *
263      * @param str The real as string
264      * @return this parser instance
265      */
266     NumberParser assignReal(final String str) {
267         String s;
268         // determine negative sign if any, ignore +
269         final boolean negative;
270         switch (str.charAt(0)) {
271             case '-':
272                 negative = true;
273                 s = str.substring(1);
274                 break;
275             case '+':
276                 negative = false;
277                 s = str.substring(1);
278                 break;
279             default:
280                 negative = false;
281                 s = str;
282         }
283         return assignReal(negative, s);
284     }
285 
286     Class<? extends Number> getLiteralClass() {
287         return clazz;
288     }
289 
290     Number getLiteralValue() {
291         return literal;
292     }
293 
294     boolean isInteger() {
295         return Integer.class.equals(clazz);
296     }
297 
298     @Override
299     public String toString() {
300         if (literal == null || clazz == null || Double.isNaN(literal.doubleValue())) {
301             return "NaN";
302         }
303         if (BigDecimal.class.equals(clazz)) {
304             synchronized (BIGDF) {
305                 return BIGDF.format(literal);
306             }
307         }
308         final StringBuilder strb = new StringBuilder(literal.toString());
309         if (Float.class.equals(clazz)) {
310             strb.append('f');
311         } else if (Double.class.equals(clazz)) {
312             strb.append('d');
313         } else if (BigInteger.class.equals(clazz)) {
314             strb.append('h');
315         } else if (Long.class.equals(clazz)) {
316             strb.append('l');
317         }
318         return strb.toString();
319     }
320 
321 }