View Javadoc

1   /*
2    * Copyright 2004-2005, 2007-2008 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not
5    * use this file except in compliance with the License. You may obtain a copy of
6    * 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, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations under
14   * the License.
15   */
16  package net.sf.morph.transform.converters;
17  
18  import java.math.BigDecimal;
19  import java.text.DecimalFormatSymbols;
20  import java.text.NumberFormat;
21  import java.text.ParsePosition;
22  import java.util.Locale;
23  
24  import net.sf.composite.util.ObjectUtils;
25  import net.sf.morph.Defaults;
26  import net.sf.morph.transform.Converter;
27  import net.sf.morph.transform.DecoratedConverter;
28  import net.sf.morph.transform.TransformationException;
29  import net.sf.morph.transform.transformers.BaseTransformer;
30  
31  import org.apache.commons.logging.Log;
32  import org.apache.commons.logging.LogFactory;
33  
34  /**
35   * Converts basic text types into primitive numbers or {@link java.lang.Number}
36   * objects.
37   *
38   * @author Matt Sgarlata
39   * @since Jan 4, 2005
40   */
41  public class TextToNumberConverter extends BaseTransformer implements DecoratedConverter {
42  
43  	private static final char RIGHT_PARENTHESES = ')';
44  
45  	private static final char LEFT_PARENTHESES = '(';
46  
47  	private static final Log logger = LogFactory.getLog(TextToNumberConverter.class);
48  
49  	/**
50  	 * Constant indicating whitespace characters should be ignored when
51  	 * converting text to numbers. This is the default treatment of whitespace
52  	 * characters by this converter.
53  	 */
54  	public static final int WHITESPACE_IGNORE = 0;
55  
56  	/**
57  	 * Constant indicating the presence of whitespace characters in text should
58  	 * prevent conversion of the text into a number (i.e. a
59  	 * TransformationException will be thrown if whitespace is in the text).
60  	 */
61  	public static final int WHITESPACE_REJECT = 1;
62  
63  	/**
64  	 * Constant indicating currency symbols should be ignored when converting
65  	 * text to numbers. This is the default treatment of currency symbols by
66  	 * this converter.
67  	 */
68  	public static final int CURRENCY_IGNORE = 0;
69  
70  	/**
71  	 * Constant indicating the presence of currency symbols in text should
72  	 * prevent conversion of the text into a number (i.e. a
73  	 * TransformationException will be thrown if currency symbols are present in
74  	 * the text).
75  	 */
76  	public static final int CURRENCY_REJECT = 1;
77  
78  	/**
79  	 * Constant indicating percentage symbols should be ignored when converting
80  	 * text to numbers.
81  	 */
82  	public static final int PERCENTAGE_IGNORE = 0;
83  
84  	/**
85  	 * Constant indicating the presence of percentage symbols in text should
86  	 * prevent conversion of the text into a number (i.e. a
87  	 * TransformationException will be thrown if percentage symbols are present
88  	 * in the text).
89  	 */
90  	public static final int PERCENTAGE_REJECT = 1;
91  
92  	/**
93  	 * Constant indicating a percentage symbol at the end of text should cause
94  	 * the text to be treated as a percentage and converted into a corresponding
95  	 * decimal number. For example, 10 would be converted to 10 and 10% would be
96  	 * converted to .10. If there are percentage symbols in any position other
97  	 * than the last character of the text, a TransformationException will be
98  	 * thrown. This is the default treatment of percentages by this converter.
99  	 */
100 	public static final int PERCENTAGE_CONVERT_TO_DECIMAL = 2;
101 
102 	/**
103 	 * Constant indicating parentheses should be ignored when converting
104 	 * text to numbers.
105 	 */
106 	public static final int PARENTHESES_IGNORE = 0;
107 
108 	/**
109 	 * Constant indicating the presence of parentheses in text should
110 	 * prevent conversion of the text into a number (i.e. a
111 	 * TransformationException will be thrown if parentheses are present
112 	 * in the text).
113 	 */
114 	public static final int PARENTHESES_REJECT = 1;
115 
116 	/**
117 	 * Constant indicating parentheses enclosing a number should cause the text
118 	 * to be treated as a negative number. For example, 10 would be converted to
119 	 * 10 and (10) would be converted to -10. If there are parentheses in
120 	 * positions other than the first or last character of the text, a
121 	 * TransformationException will be thrown. This is the default treatment of
122 	 * parentheses by this converter.
123 	 */
124 	public static final int PARENTHESES_NEGATE = 2;
125 
126 	/**
127 	 * The converter used to convert text types from one type to another.
128 	 */
129 	private Converter textConverter;
130 	/**
131 	 * The converter used to convert number types from one type to another.
132 	 */
133 	private Converter numberConverter;
134 
135 	/**
136 	 * Configuration option indicating how whitespace should be treated
137 	 * by this converter.  Default is {@link #WHITESPACE_IGNORE}.
138 	 */
139 	private int whitespaceHandling = WHITESPACE_IGNORE;
140 	/**
141 	 * Configuration option indicating how currencies should be treated by
142 	 * this converter.  Default is {@link #CURRENCY_IGNORE}.
143 	 */
144 	private int currencyHandling = CURRENCY_IGNORE;
145 	/**
146 	 * Configuration option indicating how percentages should be treated by
147 	 * this converter.  Default is {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
148 	 */
149 	private int percentageHandling = PERCENTAGE_CONVERT_TO_DECIMAL;
150 	/**
151 	 * Configuration option indicating how parantheses should be treated by
152 	 * this converter.  Default is {@link #PARENTHESES_NEGATE}.
153 	 */
154 	private int parenthesesHandling = PARENTHESES_NEGATE;
155 
156 	/**
157 	 * {@inheritDoc}
158 	 */
159 	protected Object convertImpl(Class destinationClass, Object source,
160 		Locale locale) throws Exception {
161 
162 		if (ObjectUtils.isEmpty(source)) {
163 			return null;
164 		}
165 		// convert the source to a String
166 		String string = (String) getTextConverter().convert(String.class,
167 			source, locale);
168 
169 //			// if a custom numberFormat has been specified, ues that for the
170 //			// conversion
171 //			if (numberFormat != null) {
172 //				Number number;
173 //				synchronized (numberFormat) {
174 //					number = numberFormat.parse(string);
175 //				}
176 //
177 //				// convert the number to the destination class requested
178 //				return getNumberConverter().convert(destinationClass, number,
179 //					locale);
180 //			}
181 
182 		StringBuffer charactersToParse = 
183 			// remove characters that should be ignored, such as currency symbols
184 			// when currency handling is set to CURRENCY_IGNORE
185 			removeIgnoredCharacters(string, locale);
186 
187 		// keep track of whether the conversion result needs to be negated
188 		// before it is returned
189 		boolean negate = handleParenthesesNegation(charactersToParse, locale);	
190 		negate = negate || handleNegativeSignNegation(charactersToParse, locale);
191 
192 		NumberFormat format = null;
193 		ParsePosition position = null;
194 		Number number = null;
195 		Object returnVal = null;
196 		String stringToParse = charactersToParse.toString();
197 
198 // could not get this to work for some reason
199 //			// try to do the conversion assuming the source is a currency value
200 //			format = NumberFormat.getCurrencyInstance(locale);
201 //			position = new ParsePosition(0);
202 //			number = format.parse(stringWithoutIgnoredSymbolsStr, position);
203 //			if (isParseSuccessful(stringWithoutIgnoredSymbolsStr, position)) {
204 //				// convert the number to the destination class requested
205 //				returnVal = getNumberConverter().convert(destinationClass, number,
206 //					locale);
207 //				if (logger.isDebugEnabled()) {
208 //					logger.debug("Successfully parsed '" + source + "' as a currency value of " + returnVal);
209 //				}
210 //				return returnVal;
211 //			}
212 //			else {
213 //				if (logger.isDebugEnabled()) {
214 //					logger.debug("Could not perform conversion of '" + source + "' by treating the source as a currency value");
215 //				}
216 //			}
217 
218 		// try to do the conversion to decimal assuming the source is a
219 		// percentage
220 		if (getPercentageHandling() == PERCENTAGE_CONVERT_TO_DECIMAL) {
221 			format = NumberFormat.getPercentInstance(locale);
222 			position = new ParsePosition(0);
223 			number = format.parse(stringToParse, position);
224 			if (isParseSuccessful(stringToParse, position)) {
225 				// negate the number if needed
226 				returnVal = negateIfNecessary(number, negate, locale);
227 				// convert the number to the destination class requested
228 				returnVal = getNumberConverter().convert(destinationClass, returnVal,
229 					locale);
230 				if (logger.isDebugEnabled()) {
231 					logger.debug("Successfully parsed '" + source + "' as a percentage with value " + returnVal);
232 				}
233 				return returnVal;
234 			}
235 			if (logger.isDebugEnabled()) {
236 				logger.debug("Could not perform conversion of '" + source + "' by treating the source as a percentage");
237 			}
238 		}
239 
240 		// try to do the conversion as a regular number
241 		format = NumberFormat.getInstance(locale);
242 		position = new ParsePosition(0);
243 		number = format.parse(stringToParse, position);
244 		if (isParseSuccessful(stringToParse, position)) {
245 			// negate the number if needed
246 			returnVal = negateIfNecessary(number, negate, locale);
247 			// convert the number to the destination class requested
248 			returnVal = getNumberConverter().convert(destinationClass, returnVal,
249 				locale);
250 			if (logger.isDebugEnabled()) {
251 				logger.debug("Successfully parsed '" + source + "' as a number or currency value of " + returnVal);
252 			}
253 			return returnVal;
254 		}
255 		if (logger.isDebugEnabled()) {
256 			logger.debug("Could not perform conversion of '" + source + "' by treating the source as a regular number or currency value");
257 		}
258 
259 //			// if the first character of the string is a currency symbol
260 //			if (Character.getType(stringWithoutIgnoredSymbolsStr.charAt(0)) == Character.CURRENCY_SYMBOL) {
261 //				// try doing the conversion as a regular number by stripping off the first character
262 //				format = NumberFormat.getInstance(locale);
263 //				position = new ParsePosition(1);
264 //				number = format.parse(stringWithoutIgnoredSymbolsStr, position);
265 //				if (isParseSuccessful(stringWithoutIgnoredSymbolsStr, position)) {
266 //					// convert the number to the destination class requested
267 //					return getNumberConverter().convert(destinationClass, number,
268 //						locale);
269 //				}
270 //				if (logger.isDebugEnabled()) {
271 //					logger.debug("Could not perform conversion of '" + source + "' by stripping the first character and treating as a normal number");
272 //				}
273 //			}
274 
275 		throw new TransformationException(destinationClass, source);
276 	}
277 
278 	/**
279 	 * Negate if necessary
280 	 * @param returnVal
281 	 * @param negate
282 	 * @param locale
283 	 * @return negated value if negate, else returnValue
284 	 */
285 	private Object negateIfNecessary(Number returnVal, boolean negate, Locale locale) {
286 		if (negate) {
287 			BigDecimal bd = (BigDecimal) getNumberConverter().convert(BigDecimal.class, returnVal, locale);
288 			return bd.negate();
289 		}
290 		return returnVal;
291     }
292 
293 	/**
294 	 * Remove any characters that should be ignored when performing the
295 	 * conversion.
296 	 * 
297 	 * @param string
298 	 *            the input string
299 	 * @param locale
300 	 *            the locale
301 	 * @return <code>string</code>, with all characters that should be
302 	 *         ignored removed
303 	 */
304 	private StringBuffer removeIgnoredCharacters(String string, Locale locale) {
305 		StringBuffer charactersToParse = new StringBuffer();
306 	    for (int i = 0; i < string.length(); i++) {
307 			char currentChar = string.charAt(i);
308 			if (getWhitespaceHandling() == WHITESPACE_IGNORE
309 				&& Character.isWhitespace(currentChar)) {
310 				continue;
311 			}
312 			if (getCurrencyHandling() == CURRENCY_IGNORE
313 				&& Character.getType(currentChar) == Character.CURRENCY_SYMBOL) {
314 				continue;
315 			}
316 			if (getPercentageHandling() == PERCENTAGE_IGNORE) {
317 				DecimalFormatSymbols symbols = new DecimalFormatSymbols(locale);
318 				if (currentChar == symbols.getPercent()) {
319 					continue;
320 				}
321 			}
322 			if (getParenthesesHandling() == PARENTHESES_IGNORE) {
323 				if (currentChar == LEFT_PARENTHESES
324 					|| currentChar == RIGHT_PARENTHESES) {
325 					continue;
326 				}
327 			}
328 			charactersToParse.append(currentChar);
329 		}
330 	    return charactersToParse;
331     }
332 
333 	/**
334 	 * Determines whether negation of the conversion result is needed based on
335 	 * the presence and handling method of parentheses.
336 	 * 
337 	 * @param charactersToParse
338 	 *            the characters to parse
339 	 * @param locale
340 	 *            the locale
341 	 * @return <code>true</code>, if the number is enclosed by parantheses
342 	 *         and parantheses handling is set to PARENTHESES_NEGATE or<br>
343 	 *         <code>false</code>, otherwise
344 	 */
345 	private boolean handleParenthesesNegation(StringBuffer charactersToParse, Locale locale) {
346 	    int lastCharIndex = charactersToParse.length() - 1;
347 		// if this is a number enclosed with parentheses and we should be
348 		// negating values in parentheses
349 		if (getParenthesesHandling() == PARENTHESES_NEGATE &&
350 			charactersToParse.charAt(0) == LEFT_PARENTHESES &&
351 			charactersToParse.charAt(lastCharIndex) == RIGHT_PARENTHESES) {
352 			// delete the closing paran
353 			charactersToParse.deleteCharAt(lastCharIndex);
354 			// delete the opening paran
355 			charactersToParse.deleteCharAt(0);
356 			// return true to indicate negation should take place
357 			return true;
358 		}
359 		// return false to indicate negation should not happen
360 		return false;
361     }
362 
363 	/**
364 	 * Determines whether negation of the conversion result is needed based on
365 	 * the presence of the minus sign character.
366 	 * 
367 	 * @param charactersToParse
368 	 *            the characters to parse
369 	 * @param locale
370 	 *            the locale
371 	 * @return <code>true</code>, if the number is enclosed by parantheses
372 	 *         and parantheses handling is set to PARENTHESES_NEGATE or<br>
373 	 *         <code>false</code>, otherwise
374 	 */
375 	private boolean handleNegativeSignNegation(StringBuffer charactersToParse, Locale locale) {
376 		if (charactersToParse.charAt(0) == '-') {
377 			charactersToParse.deleteCharAt(0);
378 			return true;
379 		}
380 		if (charactersToParse.charAt(charactersToParse.length() - 1) == '-') {
381 			charactersToParse.deleteCharAt(charactersToParse.length() - 1);
382 			return true;
383 		}
384 		return false;
385     }
386 
387 	/**
388 	 * Learn whether the entire string was consumed.
389 	 * @param stringWithoutIgnoredSymbolsStr
390 	 * @param position
391 	 * @return boolean
392 	 */
393 	protected boolean isParseSuccessful(String stringWithoutIgnoredSymbolsStr, ParsePosition position) {
394 		return position.getIndex() != 0 &&
395 			position.getIndex() == stringWithoutIgnoredSymbolsStr.length();
396 	}
397 
398 	/**
399 	 * {@inheritDoc}
400 	 */
401 	protected boolean isWrappingRuntimeExceptions() {
402 	    return true;
403     }
404 
405 	/**
406 	 * {@inheritDoc}
407 	 */
408 	protected Class[] getSourceClassesImpl() throws Exception {
409 		return getTextConverter().getSourceClasses();
410 	}
411 
412 	/**
413 	 * {@inheritDoc}
414 	 */
415 	protected Class[] getDestinationClassesImpl() throws Exception {
416 		return getNumberConverter().getDestinationClasses();
417 	}
418 
419 	/**
420 	 * Sets the converter used to convert text types from one type to another.
421 	 *
422 	 * @return the converter used to convert text types from one type to another
423 	 */
424 	public Converter getNumberConverter() {
425 		if (numberConverter == null) {
426 			setNumberConverter(Defaults.createNumberConverter());
427 		}
428 		return numberConverter;
429 	}
430 
431 	/**
432 	 * Sets the converter used to convert text types from one type to another.
433 	 *
434 	 * @param numberConverter
435 	 *            the converter used to convert text types from one type to
436 	 *            another
437 	 */
438 	public void setNumberConverter(Converter numberConverter) {
439 		this.numberConverter = numberConverter;
440 	}
441 
442 	/**
443 	 * Gets the converter used to convert text types from one type to another.
444 	 *
445 	 * @return the converter used to convert text types from one type to another
446 	 */
447 	public Converter getTextConverter() {
448 		if (textConverter == null) {
449 			setTextConverter(Defaults.createTextConverter());
450 		}
451 		return textConverter;
452 	}
453 
454 	/**
455 	 * Sets the converter used to convert text types from one type to another.
456 	 *
457 	 * @param textConverter
458 	 *            the converter used to convert text types from one type to
459 	 *            another
460 	 */
461 	public void setTextConverter(Converter textConverter) {
462 		this.textConverter = textConverter;
463 	}
464 
465 	/**
466 	 * Retrieves the configuration option indicating how currencies should be
467 	 * treated by this converter. Default is {@link #CURRENCY_IGNORE}.
468 	 *
469 	 * @return the configuration option indicating how currencies should be
470 	 *         treated by this converter
471 	 */
472 	public int getCurrencyHandling() {
473 		return currencyHandling;
474 	}
475 
476 	/**
477 	 * Sets the configuration option indicating how currencies should be treated
478 	 * by this converter. Default is {@link #CURRENCY_IGNORE}.
479 	 *
480 	 * @param currencyHandling
481 	 *            the configuration option indicating how currencies should be
482 	 *            treated by this converter. Default is {@link #CURRENCY_IGNORE}.
483 	 */
484 	public void setCurrencyHandling(int currencyHandling) {
485 		this.currencyHandling = currencyHandling;
486 	}
487 
488 	/**
489 	 * Retrieves the configuration option indicating how parantheses should be
490 	 * treated by this converter. Default is {@link #PARENTHESES_NEGATE}.
491 	 *
492 	 * @return the configuration option indicating how parantheses should be
493 	 *         treated by this converter. Default is {@link #PARENTHESES_NEGATE}.
494 	 */
495 	public int getParenthesesHandling() {
496 		return parenthesesHandling;
497 	}
498 
499 	/**
500 	 * Sets the configuration option indicating how parantheses should be
501 	 * treated by this converter. Default is {@link #PARENTHESES_NEGATE}.
502 	 *
503 	 * @param parenthesesHandling
504 	 *            the configuration option indicating how parantheses should be
505 	 *            treated by this converter. Default is
506 	 *            {@link #PARENTHESES_NEGATE}.
507 	 */
508 	public void setParenthesesHandling(int parenthesesHandling) {
509 		this.parenthesesHandling = parenthesesHandling;
510 	}
511 
512 	/**
513 	 * Gets the configuration option indicating how percentages should be treated by
514 	 * this converter.  Default is {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
515 	 * @return the configuration option indicating how percentages should be treated by
516 	 * this converter.  Default is {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
517 	 */
518 	public int getPercentageHandling() {
519 		return percentageHandling;
520 	}
521 
522 	/**
523 	 * Sets the configuration option indicating how percentages should be
524 	 * treated by this converter. Default is
525 	 * {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
526 	 *
527 	 * @param percentageHandling
528 	 *            the configuration option indicating how percentages should be
529 	 *            treated by this converter. Default is
530 	 *            {@link #PERCENTAGE_CONVERT_TO_DECIMAL}.
531 	 */
532 	public void setPercentageHandling(int percentageHandling) {
533 		this.percentageHandling = percentageHandling;
534 	}
535 
536 	/**
537 	 * Gets the configuration option indicating how whitespace should be treated
538 	 * by this converter. Default is {@link #WHITESPACE_IGNORE}.
539 	 *
540 	 * @return the configuration option indicating how whitespace should be
541 	 *         treated by this converter. Default is {@link #WHITESPACE_IGNORE}.
542 	 */
543 	public int getWhitespaceHandling() {
544 		return whitespaceHandling;
545 	}
546 
547 	/**
548 	 * Sets the configuration option indicating how whitespace should be treated
549 	 * by this converter. Default is {@link #WHITESPACE_IGNORE}.
550 	 *
551 	 * @param whitespaceHandling
552 	 *            the configuration option indicating how whitespace should be
553 	 *            treated by this converter. Default is
554 	 *            {@link #WHITESPACE_IGNORE}.
555 	 */
556 	public void setWhitespaceHandling(int whitespaceHandling) {
557 		this.whitespaceHandling = whitespaceHandling;
558 	}
559 
560 }