View Javadoc
1   /*
2    * Redistribution and use of this software and associated documentation ("Software"), with or
3    * without modification, are permitted provided that the following conditions are met:
4    *
5    * 1. Redistributions of source code must retain copyright statements and notices. Redistributions
6    * must also contain a copy of this document.
7    *
8    * 2. Redistributions in binary form must reproduce the above copyright notice, this list of
9    * conditions and the following disclaimer in the documentation and/or other materials provided with
10   * the distribution.
11   *
12   * 3. The name "Exolab" must not be used to endorse or promote products derived from this Software
13   * without prior written permission of Intalio, Inc. For written permission, please contact
14   * info@exolab.org.
15   *
16   * 4. Products derived from this Software may not be called "Exolab" nor may "Exolab" appear in
17   * their names without prior written permission of Intalio, Inc. Exolab is a registered trademark of
18   * Intalio, Inc.
19   *
20   * 5. Due credit should be given to the Exolab Project (http://www.exolab.org/).
21   *
22   * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR
23   * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
24   * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTALIO, INC. OR ITS
25   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26   * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27   * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
28   * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY
29   * WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30   *
31   * Copyright 2000-2003 (C) Intalio, Inc. All Rights Reserved.
32   *
33   * $Id$ Date Author Changes 10/31/2000 Arnaud Blandin Created
34   */
35  package org.exolab.castor.xml.validators;
36  
37  import java.lang.reflect.InvocationTargetException;
38  import java.lang.reflect.Method;
39  import java.math.BigDecimal;
40  
41  import org.exolab.castor.xml.TypeValidator;
42  import org.exolab.castor.xml.ValidationContext;
43  import org.exolab.castor.xml.ValidationException;
44  
45  /**
46   * The Decimal Validation class. This class handles validation for the <code>BigDecimal</code> type.
47   *
48   * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
49   * @author <a href="mailto:edward.kuns@aspect.com">Edward Kuns</a>
50   * @version $Revision$ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
51   */
52  public class DecimalValidator extends PatternValidator implements TypeValidator {
53  
54    /** Reference to the Method BigDecimal.toPlainString(), only in JDK 5 or later. */
55    private static Method _bdMethodToPlainString = null;
56  
57    static {
58      try {
59        _bdMethodToPlainString = BigDecimal.class.getMethod("toPlainString", (Class[]) null);
60      } catch (NoSuchMethodException e) {
61        // If it does not exist, we're in Java 1.4.2 or earlier
62      }
63    }
64  
65    /** Fixed value of this short. (Not used if null.) */
66    private BigDecimal _fixed = null;
67    /** Minimum value (inclusive or exclusive) for this BigDecimal. (Not used if null.) */
68    private BigDecimal _min = null;
69    /** Maximum value (inclusive or exclusive) for this BigDecimal. (Not used if null.) */
70    private BigDecimal _max = null;
71    /** Maximum number of significant digits in this BigDecimal. (Not applied if < 0.) */
72    private int _totalDigits = -1;
73    /** Maximum number of fractional digits in this BigDecimal. (Not applied if < 0.) */
74    private int _fractionDigits = -1;
75    /** If true, the minimum value is an <b>exclusive</b> value. */
76    private boolean _hasMinExclusive = false;
77    /** If true, the maximum value is an <b>exclusive</b> value. */
78    private boolean _hasMaxExclusive = false;
79  
80    /**
81     * Creates a new DecimalValidator with no restrictions.
82     */
83    public DecimalValidator() {
84      super();
85    } // -- decimalValidator
86  
87    /**
88     * Clears the fixed value for this BigIntegerValidator.
89     */
90    public void clearFixed() {
91      _fixed = null;
92    } // -- clearFixed
93  
94    /**
95     * Clears the maximum value for this DecimalValidator.
96     */
97    public void clearMax() {
98      _max = null;
99      _hasMaxExclusive = false;
100   } // -- clearMax
101 
102   /**
103    * Clears the minimum value for this DecimalValidator.
104    */
105   public void clearMin() {
106     _min = null;
107     _hasMinExclusive = false;
108   } // -- clearMin
109 
110   /**
111    * Returns the configured fixed value for BigDecimal validation. Returns null if no fixed value
112    * has been configured.
113    *
114    * @return the fixed value to validate against.
115    */
116   public BigDecimal getFixed() {
117     return _fixed;
118   } // -- getFixed
119 
120   /**
121    * Returns the configured inclusive maximum value for BigDecimal validation. Returns null if no
122    * inclusive maximum has been configured.
123    *
124    * @return the maximum inclusive value to validate against.
125    */
126   public BigDecimal getMaxInclusive() {
127     return (_hasMaxExclusive) ? null : _max;
128   } // -- getMaxInclusive
129 
130   /**
131    * Returns the configured exclusive maximum value for BigDecimal validation. Returns null if no
132    * exclusive maximum has been configured.
133    *
134    * @return the maximum exclusive value to validate against.
135    */
136   public BigDecimal getMaxExclusive() {
137     return (_hasMaxExclusive) ? _max : null;
138   } // -- getMaxInclusive
139 
140   /**
141    * Returns the configured inclusive minimum value for BigDecimal validation. Returns null if no
142    * inclusive minimum has been configured.
143    *
144    * @return the minimum inclusive value to validate against.
145    */
146   public BigDecimal getMinInclusive() {
147     return (_hasMinExclusive) ? null : _min;
148   } // -- getMinInclusive
149 
150   /**
151    * Returns the configured exclusive minimum value for BigDecimal validation. Returns null if no
152    * exclusive minimum has been configured.
153    *
154    * @return the minimum exclusive value to validate against.
155    */
156   public BigDecimal getMinExclusive() {
157     return (_hasMinExclusive) ? _min : null;
158   } // -- getMinInclusive
159 
160   /**
161    * Returns true if a fixed value to validate against has been set.
162    *
163    * @return true if a fixed value has been set.
164    */
165   public boolean hasFixed() {
166     return (_fixed != null);
167   } // -- hasFixed
168 
169   /**
170    * Sets the fixed value for BigDecimal validation.
171    * <p>
172    * NOTE: If maximum and/or minimum values have been set and the fixed value is not within that
173    * max/min range, then no BigDecimal will pass validation. This is as according to the XML Schema
174    * spec.
175    *
176    * @param fixedValue the fixed value that a BigDecimal validated with this validator must be equal
177    *        to.
178    */
179   public void setFixed(final BigDecimal fixedValue) {
180     _fixed = fixedValue;
181   } // -- setMinExclusive
182 
183   /**
184    * Sets the minimum (exclusive) value for BigDecimal validation. To pass validation, a BigDecimal
185    * must be greater than this value.
186    *
187    * @param minValue the minimum (exclusive) value for BigDecimal validation.
188    */
189   public void setMinExclusive(final BigDecimal minValue) {
190     if (minValue == null) {
191       throw new IllegalArgumentException("argument 'minValue' must not be null.");
192     }
193     _min = minValue;
194     _hasMinExclusive = true;
195   } // -- setMinExclusive
196 
197   /**
198    * Sets the minimum (inclusive) value for BigDecimal validation. To pass validation, a BigDecimal
199    * must be greater than or equal to this value.
200    *
201    * @param minValue the minimum (inclusive) value for BigDecimal validation.
202    */
203   public void setMinInclusive(final BigDecimal minValue) {
204     _min = minValue;
205     _hasMinExclusive = false;
206   } // -- setMinInclusive
207 
208   /**
209    * Sets the maximum (exclusive) value for BigDecimal validation. To pass validation, a BigDecimal
210    * must be less than this value.
211    *
212    * @param maxValue the maximum (exclusive) value for BigDecimal validation.
213    */
214   public void setMaxExclusive(final BigDecimal maxValue) {
215     if (maxValue == null) {
216       throw new IllegalArgumentException("argument 'maxValue' must not be null.");
217     }
218     _max = maxValue;
219     _hasMaxExclusive = true;
220   } // -- setMaxExclusive
221 
222   /**
223    * Sets the maximum (inclusive) value for BigDecimal validation. To pass validation, a BigDecimal
224    * must be less than or equal to this value.
225    *
226    * @param maxValue the maximum (inclusive) value for BigDecimal validation.
227    */
228   public void setMaxInclusive(final BigDecimal maxValue) {
229     _max = maxValue;
230     _hasMaxExclusive = false;
231   } // -- setMaxInclusive
232 
233   /**
234    * Sets the maximum number of digits for BigDecimal validation. To pass validation, a BigDecimal
235    * must have this many digits or fewer. Leading zeros are not counted. Trailing zeros after the
236    * decimal point are not counted.
237    *
238    * @param totalDig the maximum (inclusive) number of digits for BigDecimal validation. (must be >
239    *        0)
240    */
241   public void setTotalDigits(final int totalDig) {
242     if (totalDig <= 0) {
243       throw new IllegalArgumentException(
244           "DecimalValidator: the totalDigits facet must be positive");
245     }
246     _totalDigits = totalDig;
247   }
248 
249   /**
250    * Sets the maximum number of fraction digits for BigDecimal validation. To pass validation, a
251    * BigDecimal must have this many digits or fewer following the decimal point. Trailing zeros
252    * after the decimal point are not counted.
253    *
254    * @param fractionDig the maximum (inclusive) number of fraction digits for BigDecimal validation.
255    *        (must be > 0)
256    */
257   public void setFractionDigits(final int fractionDig) {
258     if (fractionDig < 0) {
259       throw new IllegalArgumentException(
260           "DecimalValidator: the fractionDigits facet must be positive");
261     }
262     _fractionDigits = fractionDig;
263   }
264 
265   /**
266    * Validates the given Object.
267    *
268    * @param bd the BigDecimal to validate
269    * @param context the ValidationContext
270    * @throws ValidationException if the object fails validation.
271    */
272   public void validate(final BigDecimal bd, final ValidationContext context)
273       throws ValidationException {
274     if (_fixed != null && !bd.equals(_fixed)) {
275       String err = "BigDecimal " + bd + " is not equal to the fixed value: " + _fixed;
276       throw new ValidationException(err);
277     }
278 
279     if (_min != null) {
280       if (bd.compareTo(_min) == -1) {
281         String err = "BigDecimal " + bd + " is less than the minimum allowed value: " + _min;
282         throw new ValidationException(err);
283       } else if ((bd.compareTo(_min) == 0) && (_hasMinExclusive)) {
284         String err =
285             "BigDecimal " + bd + " cannot be equal to the minimum exclusive value: " + _min;
286         throw new ValidationException(err);
287       }
288     }
289 
290     if (_max != null) {
291       if (bd.compareTo(_max) == 1) {
292         String err = "BigDecimal " + bd + " is greater than the maximum allowed value: " + _max;
293         throw new ValidationException(err);
294       } else if ((bd.compareTo(_max) == 0) && (_hasMaxExclusive)) {
295         String err =
296             "BigDecimal " + bd + " cannot be equal to the maximum exclusive value: " + _max;
297         throw new ValidationException(err);
298       }
299     }
300 
301     // For digit counting, we are not supposed to count leading or trailing zeros
302     BigDecimal clean = stripTrailingZeros(bd);
303 
304     if (_totalDigits != -1) {
305       String temp = toStringForBigDecimal(clean);
306       int length = temp.length();
307       if (temp.indexOf('-') == 0) {
308         --length;
309       }
310       if (temp.indexOf('.') != -1) {
311         --length;
312       }
313       if (length > _totalDigits) {
314         String err = "BigDecimal " + bd + " has too many significant digits -- must have "
315             + _totalDigits + " or fewer";
316         throw new ValidationException(err);
317       }
318       temp = null;
319     }
320 
321     if (_fractionDigits != -1 && clean.scale() > _fractionDigits) {
322       String err = "BigDecimal " + bd + " has too many fraction digits -- must have "
323           + _fractionDigits + " fraction digits or fewer";
324       throw new ValidationException(err);
325     }
326 
327     if (hasPattern()) {
328       super.validate(toStringForBigDecimal(bd), context);
329     }
330   } // -- validate
331 
332   /**
333    * Validates the given Object.
334    *
335    * @param object the Object to validate
336    * @throws ValidationException if the object fails validation.
337    */
338   public void validate(final Object object) throws ValidationException {
339     validate(object, (ValidationContext) null);
340   } // -- validate
341 
342   /**
343    * Validates the given Object.
344    *
345    * @param object the Object to validate
346    * @param context the ValidationContext
347    * @throws ValidationException if the object fails validation.
348    */
349   public void validate(final Object object, final ValidationContext context)
350       throws ValidationException {
351     if (object == null) {
352       String err = "decimalValidator cannot validate a null object.";
353       throw new ValidationException(err);
354     }
355 
356     BigDecimal value = null;
357     if (object instanceof BigDecimal) {
358       value = (BigDecimal) object;
359     } else {
360       try {
361         value = new java.math.BigDecimal(object.toString());
362       } catch (Exception ex) {
363         String err = "Expecting a decimal, received instead: " + object.getClass().getName();
364         throw new ValidationException(err);
365       }
366     }
367     validate(value, context);
368   } // -- validate
369 
370   /**
371    * Because Sun broke API compatibility between Java 1.4 and Java 5, we have to do this the hard
372    * way.
373    *
374    * @param bd the BigDecimal to toString() on.
375    * @return what <i>should be</i> toString() for BigDecimal
376    * @see <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6364896"> This Sun bug report
377    *      calling for better documentation of this change</a>
378    */
379   private String toStringForBigDecimal(final BigDecimal bd) {
380     if (_bdMethodToPlainString != null) {
381       try {
382         // For Java 1.5 or later, use toPlainString() to get what we want
383         return (String) _bdMethodToPlainString.invoke(bd, (Object[]) null);
384       } catch (IllegalAccessException e) {
385         // Cannot occur, so just fall through to toString()
386       } catch (InvocationTargetException e) {
387         // Cannot occur, so just fall through to toString()
388       }
389     }
390 
391     // For Java 1.4.2 or earlier, use toString() to get what we want
392     return bd.toString();
393   }
394 
395   /**
396    * Trims trailing zeros from the provided BigDecimal. Since BigDecimals are immutable, the value
397    * passed in is not changed.
398    * <p>
399    * The JDK 5 API provides a method to do this, but earlier releases of Java do not. Rather than
400    * reflectively use this method for early releases and the API-provided one for releases after
401    * Java 5, we'll just use this method for all Java releases.
402    *
403    * @param bd the BigDecimal to trim
404    * @return a new BigDecimal with trailing zeros removed
405    */
406   private BigDecimal stripTrailingZeros(final BigDecimal bd) {
407     BigDecimal ret = null;
408     try {
409       for (int i = bd.scale(); i >= 0; i--) {
410         ret = bd.setScale(i);
411       }
412     } catch (ArithmeticException e) {
413       // We've removed all trailing zeros
414     }
415     return (ret == null) ? bd : ret;
416   }
417 
418 } // -- decimalValidator