001: /*
002: * Copyright 2004 Jonathan M. Lehr
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS"
010: * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language
011: * governing permissions and limitations under the License.
012: *
013: * MODIFIED BY THE KUALI FOUNDATION
014: */
015: // begin Kuali Foundation modification
016: package org.kuali.core.web.format;
017:
018: // end Kuali Foundation modification
019:
020: // begin Kuali Foundation modification
021: import java.text.DecimalFormat;
022: import java.text.NumberFormat;
023: import java.text.ParseException;
024: import java.util.regex.Pattern;
025:
026: import org.apache.commons.lang.StringUtils;
027: import org.apache.log4j.Logger;
028: import org.kuali.RiceKeyConstants;
029: import org.kuali.core.util.KualiDecimal;
030: import org.kuali.core.util.KualiInteger;
031:
032: /**
033: * begin Kuali Foundation modification
034: * This class is used to format Currency objects.
035: * end Kuali Foundation modification
036: */
037: public class CurrencyFormatter extends Formatter {
038: // begin Kuali Foundation modification
039: // original code
040: /*
041: /** The default scale for currency values * /
042: public final static int SCALE = 2;
043:
044: /** The key used to look up the currency error string * /
045: public final static String CURRENCY_ERROR_KEY = "error.currency";
046:
047: public static final String SHOW_SYMBOL = "showCurrencySymbol";
048:
049: static final String PARSE_MSG = "Unable to parse a currency value from ";
050:
051: static final String FORMAT_MSG = "Unable to format a currency value from ";
052: */
053: private static Logger LOG = Logger
054: .getLogger(CurrencyFormatter.class);
055: public static final String SHOW_SYMBOL = "showCurrencySymbol";
056: private static final Pattern CURRENCY_PATTERN = Pattern
057: .compile("[-\\(\\)\\$\\.,0-9]*");
058: private static final Pattern TRAILING_DECIMAL_PATTERN = Pattern
059: .compile(".*\\.[0-9]{0,2}\\)?$");
060:
061: // end Kuali Foundation modification
062:
063: /**
064: * begin Kuali Foundation modification
065: * Unformats its argument and returns a KualiDecimal instance initialized with the resulting string value
066: *
067: * @see org.kuali.core.web.format.Formatter#convertToObject(java.lang.String)
068: * end Kuali Foundation modification
069: */
070: @Override
071: protected Object convertToObject(String target) {
072: // begin Kuali Foundation modification
073: KualiDecimal value = null;
074:
075: LOG.debug("convertToObject '" + target + "'");
076:
077: if (target != null) {
078: target = target.trim();
079:
080: String rawString = target;
081:
082: // parseable values are $1.23 and ($1.23), not (1.23)
083: if (target.startsWith("(")) {
084: if (!target.startsWith("($")) {
085: target = "($"
086: + StringUtils.substringAfter(target, "(");
087: }
088: }
089:
090: // Insert currency symbol if absent
091: if (!(target.startsWith("(") || target
092: .startsWith(getSymbol()))) {
093: target = interpolateSymbol(target);
094: }
095:
096: // preemptively detect non-numeric-related symbols, since NumberFormat.parse seems to be silently deleting them
097: // (i.e. 9aaaaaaaaaaaaaaa is silently converted into 9)
098: if (!CURRENCY_PATTERN.matcher(target).matches()) {
099: throw new FormatException("parsing",
100: RiceKeyConstants.ERROR_CURRENCY, rawString);
101: }
102:
103: // preemptively detect String with excessive digits after the decimal, to prevent them from being silently rounded
104: if (rawString.contains(".")
105: && !TRAILING_DECIMAL_PATTERN.matcher(rawString)
106: .matches()) {
107: throw new FormatException("parsing",
108: RiceKeyConstants.ERROR_CURRENCY_DECIMAL,
109: rawString);
110: }
111:
112: // actually reformat the numeric value
113: NumberFormat formatter = getCurrencyInstanceUsingParseBigDecimal();
114: try {
115: Number parsedNumber = formatter.parse(target);
116: value = new KualiDecimal(parsedNumber.toString());
117: } catch (NumberFormatException e) {
118: throw new FormatException("parsing",
119: RiceKeyConstants.ERROR_CURRENCY, rawString, e);
120: } catch (ParseException e) {
121: throw new FormatException("parsing",
122: RiceKeyConstants.ERROR_CURRENCY, rawString, e);
123: }
124: }
125:
126: return value;
127: // end Kuali Foundation modification
128: }
129:
130: protected String interpolateSymbol(String target) {
131: if (target.startsWith("-")) {
132: int dashPos = target.indexOf('-');
133: int symbolPos = target.indexOf(getSymbol());
134: int index = (dashPos > symbolPos ? dashPos : symbolPos);
135: return "($" + target.substring(index + 1) + ")";
136: }
137: return target.startsWith("(") ? "($" + target.indexOf("(" + 1)
138: : "$" + target;
139: }
140:
141: protected String removeSymbol(String target) {
142: int index = target.indexOf(getSymbol());
143: String prefix = (index > 0 ? target.substring(0, index) : "");
144: return prefix + target.substring(index + 1);
145: }
146:
147: protected String getSymbol() {
148: return "$";
149: }
150:
151: protected boolean showSymbol() {
152: String showSymbol = (settings == null ? null
153: : (String) settings.get(SHOW_SYMBOL));
154: return Boolean.valueOf(showSymbol).booleanValue();
155: }
156:
157: /**
158: * Returns a string representation of its argument formatted as a currency value.
159: *
160: * begin Kuali Foundation modification
161: * @see org.kuali.core.web.format.Formatter#format(java.lang.Object)
162: * end Kuali Foundation modification
163: */
164: // begin Kuali Foundation modification
165: @Override
166: // end Kuali Foundation modification
167: public Object format(Object obj) {
168: // begin Kuali Foundation modification
169: // major code rewrite, original code commented
170: /*
171: if (obj == null)
172: return null;
173:
174: NumberFormat formatter = NumberFormat.getCurrencyInstance();
175: String string = null;
176:
177: try {
178: BigDecimal number = (BigDecimal) obj;
179: number = number.setScale(SCALE, BigDecimal.ROUND_HALF_UP);
180: string = formatter.format(number.doubleValue());
181: } catch (IllegalArgumentException e) {
182: throw new FormatException(FORMAT_MSG + obj, e);
183: } catch (ClassCastException e) {
184: throw new FormatException(FORMAT_MSG + obj, e);
185: }
186:
187: return showSymbol() ? string : removeSymbol(string);
188: */
189: LOG.debug("format '" + obj + "'");
190: if (obj == null)
191: return null;
192:
193: NumberFormat formatter = getCurrencyInstanceUsingParseBigDecimal();
194: String string = null;
195:
196: try {
197: Number number;
198:
199: if (obj instanceof KualiInteger) {
200: formatter.setMaximumFractionDigits(0);
201: number = (KualiInteger) obj;
202:
203: // Note that treating the number as a KualiDecimal below is obsolete. But it doesn't do any harm either since
204: // we already set maximumFractionDigits above.
205: } else {
206: number = (KualiDecimal) obj;
207: }
208:
209: // run the incoming KualiDecimal's string representation through convertToObject, so that KualiDecimal objects
210: // containing ill-formatted incoming values will cause the same errors here that ill-formatted Strings do in
211: // convertToObject
212: KualiDecimal convertedNumber = (KualiDecimal) convertToObject(number
213: .toString());
214:
215: string = formatter
216: .format(convertedNumber.bigDecimalValue());
217: } catch (IllegalArgumentException e) {
218: throw new FormatException("formatting",
219: RiceKeyConstants.ERROR_CURRENCY, obj.toString(), e);
220: } catch (ClassCastException e) {
221: throw new FormatException("formatting",
222: RiceKeyConstants.ERROR_CURRENCY, obj.toString(), e);
223: }
224:
225: return showSymbol() ? string : removeSymbol(string);
226: // end Kuali Foundation modification
227: }
228:
229: // begin Kuali Foundation modification
230: /**
231: * retrieves a currency formatter instance and sets ParseBigDecimal to true to fix [KULEDOCS-742]
232: *
233: * @return CurrencyInstance
234: */
235: static final NumberFormat getCurrencyInstanceUsingParseBigDecimal() {
236: NumberFormat formatter = NumberFormat.getCurrencyInstance();
237: if (formatter instanceof DecimalFormat) {
238: ((DecimalFormat) formatter).setParseBigDecimal(true);
239: }
240: return formatter;
241: }
242: // end Kuali Foundation modification
243: }
|