1 /*
2 * Copyright 2006-2016 The JGUIraffe Team.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License")
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package net.sf.jguiraffe.transform;
17
18 import java.lang.reflect.ParameterizedType;
19 import java.text.NumberFormat;
20 import java.text.ParseException;
21 import java.text.ParsePosition;
22 import java.util.Locale;
23
24 import org.apache.commons.configuration.Configuration;
25 import org.apache.commons.configuration.MapConfiguration;
26
27 /**
28 * <p>
29 * An abstract base class for transformers and validators for numbers.
30 * </p>
31 * <p>
32 * This base class already provides the major part of functionality for
33 * validating numeric input and transforming strings to number objects. Concrete
34 * sub classes are responsible for the creation of a
35 * <code>java.text.NumberFormat</code> object that is used for parsing the
36 * user input. The class also supports certain semantic checks, especially
37 * whether an entered number lies in a specified interval.
38 * </p>
39 * <p>
40 * This class makes use of Java generics to be independent on a concrete number
41 * type. The returned (transformed) object will be of this type, and also the
42 * specified minimum or maximum values must use this type.
43 * </p>
44 * <p>
45 * The following properties are supported by this class:
46 * </p>
47 * <table border="1">
48 * <tr>
49 * <th>Property</th>
50 * <th>Description</th>
51 * <th>Default</th>
52 * </tr>
53 * <tr>
54 * <td valign="top">minimum</td>
55 * <td>Here the minimum value can be defined. Entered numbers are checked to be
56 * greater or equal than this number. If this property is undefined, no minimum
57 * checks will be performed.</td>
58 * <td valign="top">undefined</td>
59 * </tr>
60 * <tr>
61 * <td valign="top">maximum</td>
62 * <td>Here the maximum value can be defined. Entered numbers are checked to be
63 * less or equal than this number. If this property is undefined, no maximum
64 * checks will be performed.</td>
65 * <td valign="top">undefined</td>
66 * </tr>
67 * </table>
68 * </p>
69 * <p>
70 * Validation of user input can fail for multiple reasons. The following table
71 * lists the possible error messages: <table border="1">
72 * <tr>
73 * <th>Message key</th>
74 * <th>Description</th>
75 * <th>Parameters</th>
76 * </tr>
77 * <tr>
78 * <td valign="top"><code>{@value ValidationMessageConstants#ERR_INVALID_NUMBER}</code></td>
79 * <td>The passed in string cannot be parsed to a number object.</td>
80 * <td valign="top">{0} = the input string</td>
81 * </tr>
82 * <tr>
83 * <td valign="top"><code>{@value ValidationMessageConstants#ERR_NUMBER_TOO_SMALL}</code></td>
84 * <td>The entered number is too small. This error message is returned if the
85 * number is less than the specified minimum number and no maximum number was
86 * specified. (If both a minimum and a maximum number are specified, the error
87 * code <code>{@value ValidationMessageConstants#ERR_NUMBER_INTERVAL}</code>
88 * is used.)</td>
89 * <td valign="top">{0} = the minimum number</td>
90 * </tr>
91 * <tr>
92 * <td valign="top"><code>{@value ValidationMessageConstants#ERR_NUMBER_TOO_BIG}</code></td>
93 * <td>The entered number is too big. This error message is returned if the
94 * number is greater than the specified maximum number and no minimum number was
95 * specified. (If both a minimum and a maximum number are specified, the error
96 * code <code>{@value ValidationMessageConstants#ERR_NUMBER_INTERVAL}</code>
97 * is used.)</td>
98 * <td valign="top">{0} = the maximum number</td>
99 * </tr>
100 * <tr>
101 * <td valign="top"><code>{@value ValidationMessageConstants#ERR_NUMBER_INTERVAL}</code></td>
102 * <td>The entered number is not in the interval spanned by the minimum and the
103 * maximum value. If both a minimum and a maximum are specified and the entered
104 * number does not meet these constraints, this error message is produced rather
105 * than one of
106 * <code>{@value ValidationMessageConstants#ERR_NUMBER_TOO_SMALL}</code> or
107 * <code>{@value ValidationMessageConstants#ERR_NUMBER_TOO_BIG}</code>.</td>
108 * <td valign="top">{0} = the minimum value, {1} = the maximum value</td>
109 * </tr>
110 * </table>
111 * </p>
112 * <p>
113 * The class implements both the <code>{@link Transformer}</code> and
114 * {@link Validator} interfaces. It is safe to use an instance
115 * concurrently as transformer and validator for the same or multiple input
116 * fields.
117 * </p>
118 *
119 * @author Oliver Heger
120 * @version $Id: NumberTransformerBase.java 205 2012-01-29 18:29:57Z oheger $
121 * @param <T> the type handled by this transformer
122 */
123 public abstract class NumberTransformerBase<T extends Number> implements
124 Transformer, Validator
125 {
126 /** Constant for the minimum property. */
127 protected static final String PROP_MINIMUM = "minimum";
128
129 /** Constant for the maximum property. */
130 protected static final String PROP_MAXIMUM = "maximum";
131
132 /** Stores the minimum allowed value. */
133 private T minimum;
134
135 /** Stores the maximum allowed value. */
136 private T maximum;
137
138 /**
139 * Returns the minimum value.
140 *
141 * @return the minimum value (can be <b>null</b>)
142 */
143 public T getMinimum()
144 {
145 return minimum;
146 }
147
148 /**
149 * Sets the minimum value. If a minimum value is specified, the validator
150 * implementation will check whether an entered number is not less than this
151 * minimum value.
152 *
153 * @param minimum the minimum value
154 */
155 public void setMinimum(T minimum)
156 {
157 this.minimum = minimum;
158 }
159
160 /**
161 * Returns the maximum value.
162 *
163 * @return the maximum value (can be <b>null</b>)
164 */
165 public T getMaximum()
166 {
167 return maximum;
168 }
169
170 /**
171 * Sets the maximum value. If a maximum value is specified, the validator
172 * implementation will check whether an entered number is not greater than
173 * this maximum value.
174 *
175 * @param maximum the maximum value
176 */
177 public void setMaximum(T maximum)
178 {
179 this.maximum = maximum;
180 }
181
182 /**
183 * Transforms the specified object to the target format. This implementation
184 * tries to convert the passed in object to a <code>Number</code> of the
185 * type specified by the generics parameter for this class. This is done by
186 * using a <code>java.text.NumberFormat</code> object. If the passed in
187 * object is <b>null</b>, <b>null</b> will also be returned.
188 *
189 * @param o the object to be transformed
190 * @param ctx the transformer context
191 * @return the transformed object
192 * @throws Exception if conversion fails
193 */
194 public Object transform(Object o, TransformerContext ctx) throws Exception
195 {
196 return transformToNumber(o, ctx, createFormat(ctx.getLocale()));
197 }
198
199 /**
200 * Validates the specified object. This implementation checks whether the
201 * object can be transformed to a number. If this is the case,
202 * <code>isNumberValid()</code> will be called to check whether the number
203 * lies in a valid range. Depending on these checks a validation result
204 * object is returned. A <b>null</b> object or an empty string are
205 * considered valid.
206 *
207 * @param o the object to be validated
208 * @param ctx the transformer context
209 * @return an object with the results of the validation
210 */
211 public ValidationResult isValid(Object o, TransformerContext ctx)
212 {
213 NumberFormat fmt = createFormat(ctx.getLocale());
214 try
215 {
216 T number = transformToNumber(o, ctx, fmt);
217 if (number == null)
218 {
219 return DefaultValidationResult.VALID;
220 }
221 else
222 {
223 Configuration config = new MapConfiguration(ctx.properties());
224 return isNumberValid(number, fmt, ctx, fetchProperty(config,
225 PROP_MINIMUM, getMinimum()), fetchProperty(config,
226 PROP_MAXIMUM, getMaximum()));
227 }
228 }
229 catch (ParseException pex)
230 {
231 // the string could not be parsed to a number
232 return errorResult(ValidationMessageConstants.ERR_INVALID_NUMBER,
233 ctx, o);
234 }
235 catch (IllegalArgumentException iex)
236 {
237 // the number does not fit into the allowed value range
238 return errorResult(
239 ValidationMessageConstants.ERR_NUMBER_OUT_OF_RANGE, ctx, o);
240 }
241 }
242
243 /**
244 * Transforms the given object into a number. This method is called by both
245 * <code>transform()</code> and <code>isValid()</code>. It performs the
246 * actual transformation. The passed in object may be <b>null</b> or empty,
247 * in which case <b>null</b> is returned.
248 *
249 * @param o the object to be transformed
250 * @param ctx the transformer context
251 * @param fmt the format object to be used
252 * @return the converted number
253 * @throws ParseException if transformation fails
254 */
255 protected T transformToNumber(Object o, TransformerContext ctx,
256 NumberFormat fmt) throws ParseException
257 {
258 if (o == null)
259 {
260 return null;
261 }
262
263 String n = String.valueOf(o);
264 if (n.length() < 1)
265 {
266 return null;
267 }
268
269 ParsePosition ppos = new ParsePosition(0);
270 Number num = fmt.parse(n, ppos);
271 if (ppos.getErrorIndex() >= 0 || ppos.getIndex() < n.length())
272 {
273 throw new ParseException("Invalid number: " + n, ppos.getIndex());
274 }
275
276 return convertToTarget(num);
277 }
278
279 /**
280 * Validates an entered number. This method is called by
281 * <code>isValid()</code> if the passed in object can be successfully
282 * converted into a number. It checks this number against the minimum and
283 * maximum values (if defined).
284 *
285 * @param n the number to check
286 * @param fmt the format object (used for formatting the minimum and/or
287 * maximum values in error messages)
288 * @param ctx the transformation context
289 * @param min the minimum value (can be <b>null</b>)
290 * @param max the maximum value (can be <b>null</b>)
291 * @return an object with the results of the validation
292 */
293 @SuppressWarnings("unchecked")
294 protected ValidationResult isNumberValid(T n, NumberFormat fmt,
295 TransformerContext ctx, T min, T max)
296 {
297 Comparable<T> comp = (Comparable<T>) n;
298
299 if (min != null && comp.compareTo(min) < 0)
300 {
301 return rangeErrorResult(true, ctx, min, max, fmt);
302 }
303
304 if (max != null && comp.compareTo(max) > 0)
305 {
306 return rangeErrorResult(false, ctx, min, max, fmt);
307 }
308
309 return DefaultValidationResult.VALID;
310 }
311
312 /**
313 * Creates a validation result if an error occurred.
314 *
315 * @param errorKey the key of the error message
316 * @param ctx the transformer context
317 * @param params optional parameters for the error message
318 * @return the validation result with this error
319 */
320 protected ValidationResult errorResult(String errorKey,
321 TransformerContext ctx, Object... params)
322 {
323 DefaultValidationResult vr = new DefaultValidationResult.Builder()
324 .addValidationMessage(
325 ctx.getValidationMessageHandler().getValidationMessage(
326 ctx, errorKey, params)).build();
327 return vr;
328 }
329
330 /**
331 * Converts the specified number to the target type supported by this
332 * transformer. This method is called after the object to be transformed was
333 * parsed by a <code>NumberFormat</code> object. There is no guarantee
334 * that the result of this parsing process matches the desired type. If this
335 * is not the case, this method will be called.
336 *
337 * @param n the number to be converted
338 * @return the converted number
339 * @throws IllegalArgumentException if the number cannot be converted to the
340 * target type (e.g. because it does not fit into the supported
341 * range)
342 */
343 protected abstract T convert(Number n);
344
345 /**
346 * Creates the format object for parsing a number. This method is called
347 * whenever a string (entered by the user) has to be converted into a
348 * number.
349 *
350 * @param locale the locale to use
351 * @return the format object for parsing the number
352 */
353 protected abstract NumberFormat createFormat(Locale locale);
354
355 /**
356 * Fetches a property of the supported type from the specified configuration
357 * object. This method is called for determining the current values of
358 * type-related properties (e.g. the minimum and maximum values). An
359 * implementation has to invoke the appropriate methods on the passed in
360 * <code>Configuration</code> object.
361 *
362 * @param config the configuration object
363 * @param property the name of the property to be obtained
364 * @param defaultValue the default value for this property
365 * @return the value of this property
366 */
367 protected abstract T fetchProperty(Configuration config, String property,
368 T defaultValue);
369
370 /**
371 * Converts the passed in number to the target type if necessary. If the
372 * number is already of the target type, nothing is done.
373 *
374 * @param n the number to convert
375 * @return the converted number
376 */
377 @SuppressWarnings("unchecked")
378 private T convertToTarget(Number n)
379 {
380 Class<?> targetClass = (Class<?>) ((ParameterizedType) getClass()
381 .getGenericSuperclass()).getActualTypeArguments()[0];
382 if (targetClass.equals(n.getClass()))
383 {
384 return (T) n;
385 }
386 else
387 {
388 return convert(n);
389 }
390 }
391
392 /**
393 * Creates a validation result object when the number is either too small or
394 * too big. The error message depends on the fact whether both the minimum
395 * and the maximum value are defined.
396 *
397 * @param tooSmall <b>true</b> if the entered number is too small, <b>false</b>
398 * otherwise
399 * @param ctx the transformer context
400 * @param min the minimum value
401 * @param max the maximum value
402 * @param fmt the format object
403 * @return the validation result
404 */
405 private ValidationResult rangeErrorResult(boolean tooSmall,
406 TransformerContext ctx, T min, T max, NumberFormat fmt)
407 {
408 boolean interval = tooSmall ? max != null : min != null;
409 if (interval)
410 {
411 return errorResult(ValidationMessageConstants.ERR_NUMBER_INTERVAL,
412 ctx, fmt.format(min), fmt.format(max));
413 }
414 else
415 {
416 if (tooSmall)
417 {
418 return errorResult(
419 ValidationMessageConstants.ERR_NUMBER_TOO_SMALL, ctx,
420 fmt.format(min));
421 }
422 else
423 {
424 return errorResult(
425 ValidationMessageConstants.ERR_NUMBER_TOO_BIG, ctx, fmt
426 .format(max));
427 }
428 }
429 }
430 }