1 /*
   2  * Copyright (c) 2000, 2014, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package javax.swing.text;
  26 
  27 import java.lang.reflect.*;
  28 import java.text.*;
  29 import java.util.*;
  30 import sun.reflect.misc.ReflectUtil;
  31 import sun.swing.SwingUtilities2;
  32 
  33 /**
  34  * <code>NumberFormatter</code> subclasses <code>InternationalFormatter</code>
  35  * adding special behavior for numbers. Among the specializations are
  36  * (these are only used if the <code>NumberFormatter</code> does not display
  37  * invalid numbers, for example, <code>setAllowsInvalid(false)</code>):
  38  * <ul>
  39  *   <li>Pressing +/- (- is determined from the
  40  *       <code>DecimalFormatSymbols</code> associated with the
  41  *       <code>DecimalFormat</code>) in any field but the exponent
  42  *       field will attempt to change the sign of the number to
  43  *       positive/negative.
  44  *   <li>Pressing +/- (- is determined from the
  45  *       <code>DecimalFormatSymbols</code> associated with the
  46  *       <code>DecimalFormat</code>) in the exponent field will
  47  *       attempt to change the sign of the exponent to positive/negative.
  48  * </ul>
  49  * <p>
  50  * If you are displaying scientific numbers, you may wish to turn on
  51  * overwrite mode, <code>setOverwriteMode(true)</code>. For example:
  52  * <pre>
  53  * DecimalFormat decimalFormat = new DecimalFormat("0.000E0");
  54  * NumberFormatter textFormatter = new NumberFormatter(decimalFormat);
  55  * textFormatter.setOverwriteMode(true);
  56  * textFormatter.setAllowsInvalid(false);
  57  * </pre>
  58  * <p>
  59  * If you are going to allow the user to enter decimal
  60  * values, you should either force the DecimalFormat to contain at least
  61  * one decimal (<code>#.0###</code>), or allow the value to be invalid
  62  * <code>setAllowsInvalid(true)</code>. Otherwise users may not be able to
  63  * input decimal values.
  64  * <p>
  65  * <code>NumberFormatter</code> provides slightly different behavior to
  66  * <code>stringToValue</code> than that of its superclass. If you have
  67  * specified a Class for values, {@link #setValueClass}, that is one of
  68  * of <code>Integer</code>, <code>Long</code>, <code>Float</code>,
  69  * <code>Double</code>, <code>Byte</code> or <code>Short</code> and
  70  * the Format's <code>parseObject</code> returns an instance of
  71  * <code>Number</code>, the corresponding instance of the value class
  72  * will be created using the constructor appropriate for the primitive
  73  * type the value class represents. For example:
  74  * <code>setValueClass(Integer.class)</code> will cause the resulting
  75  * value to be created via
  76  * <code>new Integer(((Number)formatter.parseObject(string)).intValue())</code>.
  77  * This is typically useful if you
  78  * wish to set a min/max value as the various <code>Number</code>
  79  * implementations are generally not comparable to each other. This is also
  80  * useful if for some reason you need a specific <code>Number</code>
  81  * implementation for your values.
  82  * <p>
  83  * <strong>Warning:</strong>
  84  * Serialized objects of this class will not be compatible with
  85  * future Swing releases. The current serialization support is
  86  * appropriate for short term storage or RMI between applications running
  87  * the same version of Swing.  As of 1.4, support for long term storage
  88  * of all JavaBeans&trade;
  89  * has been added to the <code>java.beans</code> package.
  90  * Please see {@link java.beans.XMLEncoder}.
  91  *
  92  * @since 1.4
  93  */
  94 @SuppressWarnings("serial") // Same-version serialization only
  95 public class NumberFormatter extends InternationalFormatter {
  96     /** The special characters from the Format instance. */
  97     private String specialChars;
  98 
  99     /**
 100      * Creates a <code>NumberFormatter</code> with the a default
 101      * <code>NumberFormat</code> instance obtained from
 102      * <code>NumberFormat.getNumberInstance()</code>.
 103      */
 104     public NumberFormatter() {
 105         this(NumberFormat.getNumberInstance());
 106     }
 107 
 108     /**
 109      * Creates a NumberFormatter with the specified Format instance.
 110      *
 111      * @param format Format used to dictate legal values
 112      */
 113     public NumberFormatter(NumberFormat format) {
 114         super(format);
 115         setFormat(format);
 116         setAllowsInvalid(true);
 117         setCommitsOnValidEdit(false);
 118         setOverwriteMode(false);
 119     }
 120 
 121     /**
 122      * Sets the format that dictates the legal values that can be edited
 123      * and displayed.
 124      * <p>
 125      * If you have used the nullary constructor the value of this property
 126      * will be determined for the current locale by way of the
 127      * <code>NumberFormat.getNumberInstance()</code> method.
 128      *
 129      * @param format NumberFormat instance used to dictate legal values
 130      */
 131     public void setFormat(Format format) {
 132         super.setFormat(format);
 133 
 134         DecimalFormatSymbols dfs = getDecimalFormatSymbols();
 135 
 136         if (dfs != null) {
 137             StringBuilder sb = new StringBuilder();
 138 
 139             sb.append(dfs.getCurrencySymbol());
 140             sb.append(dfs.getDecimalSeparator());
 141             sb.append(dfs.getGroupingSeparator());
 142             sb.append(dfs.getInfinity());
 143             sb.append(dfs.getInternationalCurrencySymbol());
 144             sb.append(dfs.getMinusSign());
 145             sb.append(dfs.getMonetaryDecimalSeparator());
 146             sb.append(dfs.getNaN());
 147             sb.append(dfs.getPercent());
 148             sb.append('+');
 149             specialChars = sb.toString();
 150         }
 151         else {
 152             specialChars = "";
 153         }
 154     }
 155 
 156     /**
 157      * Invokes <code>parseObject</code> on <code>f</code>, returning
 158      * its value.
 159      */
 160     Object stringToValue(String text, Format f) throws ParseException {
 161         if (f == null) {
 162             return text;
 163         }
 164         Object value = f.parseObject(text);
 165 
 166         return convertValueToValueClass(value, getValueClass());
 167     }
 168 
 169     /**
 170      * Converts the passed in value to the passed in class. This only
 171      * works if <code>valueClass</code> is one of <code>Integer</code>,
 172      * <code>Long</code>, <code>Float</code>, <code>Double</code>,
 173      * <code>Byte</code> or <code>Short</code> and <code>value</code>
 174      * is an instanceof <code>Number</code>.
 175      */
 176     private Object convertValueToValueClass(Object value, Class valueClass) {
 177         if (valueClass != null && (value instanceof Number)) {
 178             Number numberValue = (Number)value;
 179             if (valueClass == Integer.class) {
 180                 return Integer.valueOf(numberValue.intValue());
 181             }
 182             else if (valueClass == Long.class) {
 183                 return Long.valueOf(numberValue.longValue());
 184             }
 185             else if (valueClass == Float.class) {
 186                 return Float.valueOf(numberValue.floatValue());
 187             }
 188             else if (valueClass == Double.class) {
 189                 return Double.valueOf(numberValue.doubleValue());
 190             }
 191             else if (valueClass == Byte.class) {
 192                 return Byte.valueOf(numberValue.byteValue());
 193             }
 194             else if (valueClass == Short.class) {
 195                 return Short.valueOf(numberValue.shortValue());
 196             }
 197         }
 198         return value;
 199     }
 200 
 201     /**
 202      * Returns the character that is used to toggle to positive values.
 203      */
 204     private char getPositiveSign() {
 205         return '+';
 206     }
 207 
 208     /**
 209      * Returns the character that is used to toggle to negative values.
 210      */
 211     private char getMinusSign() {
 212         DecimalFormatSymbols dfs = getDecimalFormatSymbols();
 213 
 214         if (dfs != null) {
 215             return dfs.getMinusSign();
 216         }
 217         return '-';
 218     }
 219 
 220     /**
 221      * Returns the character that is used to toggle to negative values.
 222      */
 223     private char getDecimalSeparator() {
 224         DecimalFormatSymbols dfs = getDecimalFormatSymbols();
 225 
 226         if (dfs != null) {
 227             return dfs.getDecimalSeparator();
 228         }
 229         return '.';
 230     }
 231 
 232     /**
 233      * Returns the DecimalFormatSymbols from the Format instance.
 234      */
 235     private DecimalFormatSymbols getDecimalFormatSymbols() {
 236         Format f = getFormat();
 237 
 238         if (f instanceof DecimalFormat) {
 239             return ((DecimalFormat)f).getDecimalFormatSymbols();
 240         }
 241         return null;
 242     }
 243 
 244     /**
 245      * Subclassed to return false if <code>text</code> contains in an invalid
 246      * character to insert, that is, it is not a digit
 247      * (<code>Character.isDigit()</code>) and
 248      * not one of the characters defined by the DecimalFormatSymbols.
 249      */
 250     boolean isLegalInsertText(String text) {
 251         if (getAllowsInvalid()) {
 252             return true;
 253         }
 254         for (int counter = text.length() - 1; counter >= 0; counter--) {
 255             char aChar = text.charAt(counter);
 256 
 257             if (!Character.isDigit(aChar) &&
 258                            specialChars.indexOf(aChar) == -1){
 259                 return false;
 260             }
 261         }
 262         return true;
 263     }
 264 
 265     /**
 266      * Subclassed to treat the decimal separator, grouping separator,
 267      * exponent symbol, percent, permille, currency and sign as literals.
 268      */
 269     boolean isLiteral(Map attrs) {
 270         if (!super.isLiteral(attrs)) {
 271             if (attrs == null) {
 272                 return false;
 273             }
 274             int size = attrs.size();
 275 
 276             if (attrs.get(NumberFormat.Field.GROUPING_SEPARATOR) != null) {
 277                 size--;
 278                 if (attrs.get(NumberFormat.Field.INTEGER) != null) {
 279                     size--;
 280                 }
 281             }
 282             if (attrs.get(NumberFormat.Field.EXPONENT_SYMBOL) != null) {
 283                 size--;
 284             }
 285             if (attrs.get(NumberFormat.Field.PERCENT) != null) {
 286                 size--;
 287             }
 288             if (attrs.get(NumberFormat.Field.PERMILLE) != null) {
 289                 size--;
 290             }
 291             if (attrs.get(NumberFormat.Field.CURRENCY) != null) {
 292                 size--;
 293             }
 294             if (attrs.get(NumberFormat.Field.SIGN) != null) {
 295                 size--;
 296             }
 297             return size == 0;
 298         }
 299         return true;
 300     }
 301 
 302     /**
 303      * Subclassed to make the decimal separator navigable, as well
 304      * as making the character between the integer field and the next
 305      * field navigable.
 306      */
 307     boolean isNavigatable(int index) {
 308         if (!super.isNavigatable(index)) {
 309             // Don't skip the decimal, it causes wierd behavior
 310             return getBufferedChar(index) == getDecimalSeparator();
 311         }
 312         return true;
 313     }
 314 
 315     /**
 316      * Returns the first <code>NumberFormat.Field</code> starting
 317      * <code>index</code> incrementing by <code>direction</code>.
 318      */
 319     private NumberFormat.Field getFieldFrom(int index, int direction) {
 320         if (isValidMask()) {
 321             int max = getFormattedTextField().getDocument().getLength();
 322             AttributedCharacterIterator iterator = getIterator();
 323 
 324             if (index >= max) {
 325                 index += direction;
 326             }
 327             while (index >= 0 && index < max) {
 328                 iterator.setIndex(index);
 329 
 330                 Map attrs = iterator.getAttributes();
 331 
 332                 if (attrs != null && attrs.size() > 0) {
 333                     for (Object key : attrs.keySet()) {
 334                         if (key instanceof NumberFormat.Field) {
 335                             return (NumberFormat.Field)key;
 336                         }
 337                     }
 338                 }
 339                 index += direction;
 340             }
 341         }
 342         return null;
 343     }
 344 
 345     /**
 346      * Overriden to toggle the value if the positive/minus sign
 347      * is inserted.
 348      */
 349     void replace(DocumentFilter.FilterBypass fb, int offset, int length,
 350                 String string, AttributeSet attr) throws BadLocationException {
 351         if (!getAllowsInvalid() && length == 0 && string != null &&
 352             string.length() == 1 &&
 353             toggleSignIfNecessary(fb, offset, string.charAt(0))) {
 354             return;
 355         }
 356         super.replace(fb, offset, length, string, attr);
 357     }
 358 
 359     /**
 360      * Will change the sign of the integer or exponent field if
 361      * <code>aChar</code> is the positive or minus sign. Returns
 362      * true if a sign change was attempted.
 363      */
 364     private boolean toggleSignIfNecessary(DocumentFilter.FilterBypass fb,
 365                                               int offset, char aChar) throws
 366                               BadLocationException {
 367         if (aChar == getMinusSign() || aChar == getPositiveSign()) {
 368             NumberFormat.Field field = getFieldFrom(offset, -1);
 369             Object newValue;
 370 
 371             try {
 372                 if (field == null ||
 373                     (field != NumberFormat.Field.EXPONENT &&
 374                      field != NumberFormat.Field.EXPONENT_SYMBOL &&
 375                      field != NumberFormat.Field.EXPONENT_SIGN)) {
 376                     newValue = toggleSign((aChar == getPositiveSign()));
 377                 }
 378                 else {
 379                     // exponent
 380                     newValue = toggleExponentSign(offset, aChar);
 381                 }
 382                 if (newValue != null && isValidValue(newValue, false)) {
 383                     int lc = getLiteralCountTo(offset);
 384                     String string = valueToString(newValue);
 385 
 386                     fb.remove(0, fb.getDocument().getLength());
 387                     fb.insertString(0, string, null);
 388                     updateValue(newValue);
 389                     repositionCursor(getLiteralCountTo(offset) -
 390                                      lc + offset, 1);
 391                     return true;
 392                 }
 393             } catch (ParseException pe) {
 394                 invalidEdit();
 395             }
 396         }
 397         return false;
 398     }
 399 
 400     /**
 401      * Invoked to toggle the sign. For this to work the value class
 402      * must have a single arg constructor that takes a String.
 403      */
 404     private Object toggleSign(boolean positive) throws ParseException {
 405         Object value = stringToValue(getFormattedTextField().getText());
 406 
 407         if (value != null) {
 408             // toString isn't localized, so that using +/- should work
 409             // correctly.
 410             String string = value.toString();
 411 
 412             if (string != null && string.length() > 0) {
 413                 if (positive) {
 414                     if (string.charAt(0) == '-') {
 415                         string = string.substring(1);
 416                     }
 417                 }
 418                 else {
 419                     if (string.charAt(0) == '+') {
 420                         string = string.substring(1);
 421                     }
 422                     if (string.length() > 0 && string.charAt(0) != '-') {
 423                         string = "-" + string;
 424                     }
 425                 }
 426                 if (string != null) {
 427                     Class<?> valueClass = getValueClass();
 428 
 429                     if (valueClass == null) {
 430                         valueClass = value.getClass();
 431                     }
 432                     try {
 433                         ReflectUtil.checkPackageAccess(valueClass);
 434                         SwingUtilities2.checkAccess(valueClass.getModifiers());
 435                         Constructor cons = valueClass.getConstructor(
 436                                               new Class[] { String.class });
 437                         if (cons != null) {
 438                             SwingUtilities2.checkAccess(cons.getModifiers());
 439                             return cons.newInstance(new Object[]{string});
 440                         }
 441                     } catch (Throwable ex) { }
 442                 }
 443             }
 444         }
 445         return null;
 446     }
 447 
 448     /**
 449      * Invoked to toggle the sign of the exponent (for scientific
 450      * numbers).
 451      */
 452     private Object toggleExponentSign(int offset, char aChar) throws
 453                              BadLocationException, ParseException {
 454         String string = getFormattedTextField().getText();
 455         int replaceLength = 0;
 456         int loc = getAttributeStart(NumberFormat.Field.EXPONENT_SIGN);
 457 
 458         if (loc >= 0) {
 459             replaceLength = 1;
 460             offset = loc;
 461         }
 462         if (aChar == getPositiveSign()) {
 463             string = getReplaceString(offset, replaceLength, null);
 464         }
 465         else {
 466             string = getReplaceString(offset, replaceLength,
 467                                       new String(new char[] { aChar }));
 468         }
 469         return stringToValue(string);
 470     }
 471 }