001    package org.springframework.richclient.components;
002    
003    import org.apache.commons.logging.Log;
004    import org.apache.commons.logging.LogFactory;
005    import org.springframework.util.Assert;
006    
007    import javax.swing.*;
008    import javax.swing.text.AttributeSet;
009    import javax.swing.text.BadLocationException;
010    import javax.swing.text.PlainDocument;
011    import java.awt.event.FocusEvent;
012    import java.awt.event.FocusListener;
013    import java.math.BigDecimal;
014    import java.math.BigInteger;
015    import java.text.DecimalFormat;
016    import java.text.DecimalFormatSymbols;
017    import java.text.NumberFormat;
018    import java.text.ParseException;
019    import java.util.ArrayList;
020    import java.util.Iterator;
021    import java.util.List;
022    
023    /**
024     * <p>
025     * This class can have different "read" and "write" formats. When showing the
026     * number the "read" format will be used. If the user enters the inputfield
027     * (gains focus), the "write" format will be used.
028     * </p>
029     *
030     * <p>
031     * A maximum of decimals/non-decimals can be specified so no more numbers can be
032     * input than strictly defined.
033     * </p>
034     *
035     * <p>
036     * A boolean can be specified to allow only positive numbers or positive and
037     * negative numbers. Switching between positive and negative can be done by
038     * using the +/- buttons anywhere in the inputfield.
039     * </p>
040     *
041     * TODO There's a third option: only negative numbers, this should be
042     * configurable as well.
043     *
044     * @author Jan Hoskens
045     *
046     */
047    public class BigDecimalTextField extends JTextField {
048    
049            private static final long serialVersionUID = -601376040393562990L;
050    
051            Log log = LogFactory.getLog(BigDecimalTextField.class);
052    
053            public static final NumberFormat DEFAULT_FORMAT = new DecimalFormat("###,###,###,##0.######");
054    
055            public static final NumberFormat DEFAULT_UNFORMAT = new DecimalFormat("#0.#######");
056    
057            public static final DecimalFormatSymbols symbols = new DecimalFormatSymbols();
058    
059            private Class numberClass = null;
060    
061            private final NumberFormat format;
062    
063            private final NumberFormat unformat;
064    
065            private Integer scale;
066    
067            private List listeners;
068    
069            private boolean internallySettingText = false;
070    
071            /**
072             * Default constructor.
073             */
074            public BigDecimalTextField() {
075                    this(2, 4, true);
076            }
077    
078            /**
079             * @see #BigDecimalTextField(int, int, boolean, Class, NumberFormat,
080             * NumberFormat)
081             */
082            public BigDecimalTextField(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign) {
083                    this(nrOfNonDecimals, nrOfDecimals, negativeSign, BigDecimal.class);
084            }
085    
086            /**
087             * @see #BigDecimalTextField(int, int, boolean, Class, NumberFormat,
088             * NumberFormat)
089             */
090            public BigDecimalTextField(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign, Class numberClass) {
091                    this(nrOfNonDecimals, nrOfDecimals, negativeSign, numberClass, DEFAULT_FORMAT);
092            }
093    
094            /**
095             * @see #BigDecimalTextField(int, int, boolean, Class, NumberFormat,
096             * NumberFormat)
097             */
098            public BigDecimalTextField(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign, Class numberClass,
099                            NumberFormat format) {
100                    this(nrOfNonDecimals, nrOfDecimals, negativeSign, numberClass, format, DEFAULT_UNFORMAT);
101            }
102    
103            /**
104             * @param nrOfNonDecimals Number of non-decimals.
105             * @param nrOfDecimals Number of decimals.
106             * @param negativeSign Negative numbers allowed.
107             * @param numberClass Class type (default BigDecimal).
108             * @param format The "read"-format.
109             * @param unformat The "edit"-format.
110             */
111            public BigDecimalTextField(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign, Class numberClass,
112                            NumberFormat format, NumberFormat unformat) {
113                    super();
114                    Assert.notNull(format);
115                    Assert.notNull(unformat);
116                    this.format = format;
117                    setBigDecimalFormat(format, numberClass);
118                    this.unformat = unformat;
119                    setBigDecimalFormat(unformat, numberClass);
120                    this.numberClass = numberClass;
121                    setDocument(new BigDecimalDocument(nrOfNonDecimals, nrOfDecimals, negativeSign));
122                    addFocusListener(new FormatFocusListener());
123            }
124    
125            /**
126             * When parsing a number, BigDecimalFormat can return numbers different than
127             * BigDecimal. This method will ensure that when using a {@link BigDecimal}
128             * or a {@link BigInteger}, the formatter will return a {@link BigDecimal}
129             * in order to prevent loss of precision. Note that you should use the
130             * {@link DecimalFormat} to make this work.
131             *
132             * @param format
133             * @param numberClass
134             *
135             * @see #getValue()
136             * @see DecimalFormat#setParseBigDecimal(boolean)
137             */
138            private static final void setBigDecimalFormat(NumberFormat format, Class numberClass) {
139                    if (format instanceof DecimalFormat && ((numberClass == BigDecimal.class) || (numberClass == BigInteger.class))) {
140                            ((DecimalFormat) format).setParseBigDecimal(true);
141                    }
142            }
143    
144            /**
145             * Add a UserInputListener.
146             *
147             * @param listener UserInputListener.
148             *
149             * @see UserInputListener
150             */
151            public void addUserInputListener(UserInputListener listener) {
152                    if (this.listeners == null)
153                            this.listeners = new ArrayList();
154                    this.listeners.add(listener);
155            }
156    
157            /**
158             * Remove a UserInputListener.
159             *
160             * @param listener UserInputListener.
161             *
162             * @see UserInputListener
163             */
164            public void removeUserInputListener(UserInputListener listener) {
165                    if (listeners != null) {
166                            this.listeners.remove(listener);
167                    }
168            }
169    
170            /**
171             * Fire an event to all UserInputListeners.
172             */
173            private void fireUserInputChange() {
174                    if (!internallySettingText && (this.listeners != null)) {
175                            for (Iterator it = this.listeners.iterator(); it.hasNext();) {
176                                    UserInputListener userInputListener = (UserInputListener) it.next();
177                                    userInputListener.update(this);
178                            }
179                    }
180            }
181    
182            /**
183             * Parses a number from the inputField and will adjust it's class if needed.
184             *
185             * @return Number the Parsed number.
186             */
187            public Number getValue() {
188                    if ((getText() == null) || "".equals(getText().trim()))
189                            return null;
190                    try {
191                            Number n = format.parse(getText());
192                            if (n.getClass() == this.numberClass)
193                                    return n;
194                            else if (this.numberClass == BigDecimal.class) {
195                                    BigDecimal bd = new BigDecimal(n.doubleValue());
196                                    if (scale != null) {
197                                            bd = bd.setScale(scale.intValue(), BigDecimal.ROUND_HALF_UP);
198                                    }
199                                    return bd;
200                            }
201                            else if (this.numberClass == Double.class)
202                                    return new Double(n.doubleValue());
203                            else if (this.numberClass == Float.class)
204                                    return new Float(n.floatValue());
205                            else if (this.numberClass == BigInteger.class)
206                                    // we have called setBigDecimalFormat to make sure a BigDecimal
207                                    // is returned so use toBigInteger on that class
208                                    return ((BigDecimal) n).toBigInteger();
209                            else if (this.numberClass == Long.class)
210                                    return new Long(n.longValue());
211                            else if (this.numberClass == Integer.class)
212                                    return new Integer(n.intValue());
213                            else if (this.numberClass == Short.class)
214                                    return new Short(n.shortValue());
215                            else if (this.numberClass == Byte.class)
216                                    return new Byte(n.byteValue());
217                            return null;
218                    }
219                    catch (Exception pe) {
220                            log.error("Error:  " + getText() + " is not a number.", pe);
221                            return null;
222                    }
223            }
224    
225            /**
226             * Format the number and show it.
227             *
228             * @param number Number to set.
229             */
230            public void setValue(Number number) {
231                    String txt = null;
232                    if (number != null) {
233                            txt = this.format.format(number);
234                    }
235                    setText(txt);
236            }
237    
238            /**
239             * Set text internally: will change text but not fire any event.
240             *
241             * @param s Text to set.
242             */
243            private void setTextInternally(String s) {
244                    internallySettingText = true;
245                    setText(s);
246                    internallySettingText = false;
247            }
248    
249            /**
250             * <p>
251             * When inputField gets focus, the contents will switch to "edit"-format
252             * (=unformat). In most cases a format without all decorations, just the
253             * number. In addition a selectAll() will be done.
254             * </p>
255             *
256             * TODO check if selectAll() is appropriate in all cases.
257             *
258             * <p>
259             * When inputField loses focus, the contents will switch to "read"-format
260             * (=format). This will probably contain some decorations.
261             * </p>
262             */
263            class FormatFocusListener implements FocusListener {
264    
265                    /**
266                     * Focus gained: "edit"-format and selectAll.
267                     */
268                    public void focusGained(FocusEvent e) {
269                            String s = getText();
270                            setTextInternally(format(unformat, format, s));
271                            selectAll();
272                    }
273    
274                    /**
275                     * Focus lost: "read"-format.
276                     */
277                    public void focusLost(FocusEvent e) {
278                            String s = getText();
279                            setTextInternally(format(format, unformat, s));
280                    }
281    
282                    /**
283                     * Format a string.
284                     *
285                     * @param toFormat Change to this format.
286                     * @param fromFormat Current format to be changed.
287                     * @param s String to be reformatted.
288                     * @return String which holds the number in the new format.
289                     */
290                    private String format(NumberFormat toFormat, NumberFormat fromFormat, String s) {
291                            if (!"".equals(s)) {
292                                    try {
293                                            return toFormat.format(fromFormat.parse(s));
294                                    }
295                                    catch (ParseException pe) {
296                                            log.error("Fout: De ingevulde waarde " + getText() + " is geen nummer.", pe);
297                                    }
298                            }
299                            return null;
300                    }
301            }
302    
303            /**
304             * Specific document that allows only input of numbers, decimal separator
305             * (or alternative) and sign. Maximum number of decimals/non-decimals will
306             * be respected at all times. Signing can be changed anywhere in the
307             * inputField by simply clicking +/-. Decimal separator input can be done
308             * with alternative character to allow both comma and point.
309             *
310             * @author jh
311             */
312            class BigDecimalDocument extends PlainDocument {
313    
314                    private final int nrOfNonDecimals;
315    
316                    private final int nrOfDecimals;
317    
318                    private final boolean negativeSign;
319    
320                    private final char decimalSeparator = symbols.getDecimalSeparator();
321    
322                    private final char alternativeSeparator;
323    
324                    /**
325                     * @see #BigDecimalDocument(int, int, boolean, char)
326                     */
327                    public BigDecimalDocument() {
328                            this(10, 2, true);
329                    }
330    
331                    /**
332                     * @see #BigDecimalDocument(int, int, boolean, char)
333                     */
334                    public BigDecimalDocument(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign) {
335                            this(nrOfNonDecimals, nrOfDecimals, negativeSign, symbols.getGroupingSeparator());
336                    }
337    
338                    /**
339                     * Constructor with several configurations. Alternative separator can be
340                     * given in order to make input easier. Eg. Comma and point can be used
341                     * for decimal separation.
342                     *
343                     * @param nrOfNonDecimals Maximum number of non-decimals.
344                     * @param nrOfDecimals Maximum number of decimals.
345                     * @param negativeSign Negative sign allowed.
346                     * @param alternativeSeparator Alternative separator.
347                     */
348                    public BigDecimalDocument(int nrOfNonDecimals, int nrOfDecimals, boolean negativeSign, char alternativeSeparator) {
349                            this.nrOfNonDecimals = nrOfNonDecimals;
350                            this.nrOfDecimals = nrOfDecimals;
351                            this.negativeSign = negativeSign;
352                            this.alternativeSeparator = alternativeSeparator;
353                    }
354    
355                    /**
356                     * Handles string insertion, checks several things like number of
357                     * non-decimals/decimals/sign...
358                     *
359                     * @inheritDoc
360                     */
361                    public void insertString(int offset, String str, AttributeSet a) throws BadLocationException {
362                            // first doing the single keys, then review what can be used for
363                            // cut/paste actions
364                            if ("-".equals(str)) {
365                                    if (this.negativeSign) // set - or flip to + if it's already
366                                    // there
367                                    {
368                                            if ((this.getLength() == 0) || !this.getText(0, 1).equals("-"))
369                                                    super.insertString(0, str, a);
370                                            else if (!(this.getLength() == 0) && this.getText(0, 1).equals("-"))
371                                                    super.remove(0, 1);
372                                            fireUserInputChange();
373                                    }
374                                    return;
375                            }
376                            else if ("+".equals(str)) {
377                                    if (this.negativeSign && (!(this.getLength() == 0) && this.getText(0, 1).equals("-"))) {
378                                            super.remove(0, 1);
379                                            fireUserInputChange();
380                                    }
381                                    return;
382                            }
383                else if (isShortCut(str))
384                {
385                    handleShortCut(str, offset, a);
386                    return;
387                }
388                            // check decimal signs
389                            else if ((str.length() == 1)
390                                            && ((this.alternativeSeparator == str.charAt(0)) || (this.decimalSeparator == str.charAt(0)))) {
391                                    if ((nrOfDecimals > 0) && (nrOfDecimals >= (getLength() - offset))
392                                                    && (getText(0, getLength()).indexOf(this.decimalSeparator) == -1)) {
393                                            super.insertString(offset, Character.toString(this.decimalSeparator), a);
394                                            fireUserInputChange();
395                                    }
396                                    return;
397                            }
398                            String s = getText(0, offset) + str;
399                            if (offset < getLength()) {
400                                    s += getText(offset, getLength() - offset);
401                            }
402    
403                            boolean isNegative = s.startsWith("-");
404                            char[] sarr = isNegative ? s.substring(1).toCharArray() : s.toCharArray();
405                            int sep = -1;
406                            int numberLength = 0; // count numbers, no special characters
407                            for (int i = 0; i < sarr.length; i++) {
408                                    if (sarr[i] == this.decimalSeparator) {
409                                            if (sep != -1) {// double decimalseparator??
410                                                    log
411                                                                    .warn("Error while inserting string: " + s + "[pos=" + i + "]"
412                                                                                    + " Double decimalseparator?");
413                                                    return;
414                                            }
415                                            sep = i;
416                                            if (numberLength > this.nrOfNonDecimals) {// too many
417                                                    // digits left
418                                                    // of decimal
419                                                    // separator
420                                                    log.warn("Error while inserting string: " + s + "[pos=" + i + "]" + " Too many non decimals? ["
421                                                                    + this.nrOfNonDecimals + "]");
422                                                    return;
423                                            }
424                                            else if ((sarr.length - sep - 1) > this.nrOfDecimals) {// too
425                                                    // many
426                                                    // digits
427                                                    // right
428                                                    // of
429                                                    // decimal
430                                                    // separator
431                                                    log.warn("Error while inserting string: " + s + "[pos=" + i + "]" + " Too many decimals? ["
432                                                                    + this.nrOfDecimals + "]");
433                                                    return;
434                                            }
435                                    }
436                                    else if (sarr[i] == symbols.getGroupingSeparator()) {
437                                            // ignore character
438                                    }
439                                    else if (!Character.isDigit(sarr[i])) {// non digit, no
440                                            // grouping/decimal
441                                            // separator not allowed
442                                            log.warn("Error while inserting string: " + s + "[pos=" + i + "]"
443                                                            + " String contains character that is no digit or separator?");
444                                            return;
445                                    }
446                                    else
447                                            ++numberLength;
448                            }
449                            if ((sep == -1) && (numberLength > this.nrOfNonDecimals)) {// no
450                                    // separator,
451                                    // number
452                                    // too
453                                    // big
454                                    log.warn("Error while inserting string: " + s + " Too many non decimals? [" + this.nrOfNonDecimals
455                                                    + "]");
456                                    return;
457                            }
458                            super.insertString(offset, str, a);
459                            fireUserInputChange();
460                    }
461    
462            private void handleShortCut(String str, int offset, AttributeSet a) throws BadLocationException
463            {
464                log.debug("handing shortcut " + str);
465                if (getLength() == 0)
466                {
467                    if (str.equals("k"))
468                    {
469                        super.insertString(0, "1000", a);
470                    }
471                    else if (str.equals("m"))
472                    {
473                        super.insertString(0, "1000000", a);
474                    }
475                    else if (str.equals("b"))
476                    {
477                        super.insertString(0, "1000000000", a);
478                    }
479                }
480                else if (getLength() == 1 && (getText(0, 1).equals("-") || getText(0, 1).equals("+")))
481                {
482                }
483                else
484                {
485                    String text = getText(0, offset);
486                    text = text.replace(',', '.');
487                    BigDecimal dec = new BigDecimal(text);
488                    if (str.equals("k"))
489                    {
490                        dec = dec.scaleByPowerOfTen(3);
491                    }
492                    else if (str.equals("m"))
493                    {
494                        dec = dec.scaleByPowerOfTen(6);
495                    }
496                    else if (str.equals("b"))
497                    {
498                        dec = dec.scaleByPowerOfTen(9);
499                    }
500                    super.remove(0, offset);
501                    String outcome = dec.toBigIntegerExact().toString();
502                    outcome = outcome.replace('.', decimalSeparator);
503                    super.insertString(0, outcome, a);
504                    fireUserInputChange();
505                }
506    
507            }
508    
509            private boolean isShortCut(String str)
510            {
511                return str.equals("k") || str.equals("m") || str.equals("b");
512            }
513    
514                    /**
515                     * Will trigger the UserInputListeners once after removing.
516                     *
517                     * @inheritDoc
518                     */
519                    public void remove(int offs, int len) throws BadLocationException {
520                            super.remove(offs, len);
521                            fireUserInputChange();
522                    }
523    
524                    /**
525                     * Will trigger the UserInputListeners once after replacing.
526                     *
527                     * @inheritDoc
528                     */
529                    public void replace(int offset, int length, String text, AttributeSet attrs) throws BadLocationException {
530                            boolean oldInternallySettingText = internallySettingText;
531                            internallySettingText = true;
532                            super.replace(offset, length, text, attrs);
533                            internallySettingText = oldInternallySettingText;
534                            fireUserInputChange();
535                    }
536            }
537    
538            /**
539             * @return Returns the scale.
540             */
541            public Integer getScale() {
542                    return scale;
543            }
544    
545            /**
546             * @param scale The scale to set.
547             */
548            public void setScale(Integer scale) {
549                    this.scale = scale;
550            }
551    }