001/* MaskFormatter.java -- 
002   Copyright (C) 2005 Free Software Foundation, Inc.
003
004This file is part of GNU Classpath.
005
006GNU Classpath is free software; you can redistribute it and/or modify
007it under the terms of the GNU General Public License as published by
008the Free Software Foundation; either version 2, or (at your option)
009any later version.
010
011GNU Classpath is distributed in the hope that it will be useful, but
012WITHOUT ANY WARRANTY; without even the implied warranty of
013MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014General Public License for more details.
015
016You should have received a copy of the GNU General Public License
017along with GNU Classpath; see the file COPYING.  If not, write to the
018Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
01902110-1301 USA.
020
021Linking this library statically or dynamically with other modules is
022making a combined work based on this library.  Thus, the terms and
023conditions of the GNU General Public License cover the whole
024combination.
025
026As a special exception, the copyright holders of this library give you
027permission to link this library with independent modules to produce an
028executable, regardless of the license terms of these independent
029modules, and to copy and distribute the resulting executable under
030terms of your choice, provided that you also meet, for each linked
031independent module, the terms and conditions of the license of that
032module.  An independent module is a module which is not derived from
033or based on this library.  If you modify this library, you may extend
034this exception to your version of the library, but you are not
035obligated to do so.  If you do not wish to do so, delete this
036exception statement from your version. */
037
038
039package javax.swing.text;
040
041import gnu.java.lang.CPStringBuilder;
042
043import java.text.ParseException;
044
045import javax.swing.JFormattedTextField;
046
047/**
048 * @author Anthony Balkissoon abalkiss at redhat dot com
049 *
050 */
051public class MaskFormatter extends DefaultFormatter
052{
053  // The declaration of the valid mask characters
054  private static final char NUM_CHAR = '#';
055  private static final char ESCAPE_CHAR = '\'';
056  private static final char UPPERCASE_CHAR = 'U';
057  private static final char LOWERCASE_CHAR = 'L';
058  private static final char ALPHANUM_CHAR = 'A';
059  private static final char LETTER_CHAR = '?';
060  private static final char ANYTHING_CHAR = '*';
061  private static final char HEX_CHAR = 'H';
062  
063  /** The mask for this MaskFormatter **/
064  private String mask;
065  
066  /** 
067   * A String made up of the characters that are not valid for input for 
068   * this MaskFormatter. 
069   */
070  private String invalidChars;
071  
072  /** 
073   * A String made up of the characters that are valid for input for 
074   * this MaskFormatter. 
075   */
076  private String validChars;
077  
078  /** A String used in place of missing chracters if the value does not 
079   * completely fill in the spaces in the mask.
080   */
081  private String placeHolder;
082  
083  /** A character used in place of missing characters if the value does 
084   * not completely fill in the spaces in the mask.
085   */
086  private char placeHolderChar = ' ';
087  
088  /**
089   * Whether or not stringToValue should return literal characters in the mask.
090   */
091  private boolean valueContainsLiteralCharacters = true;
092  
093  /** A String used for easy access to valid HEX characters **/
094  private static String hexString = "0123456789abcdefABCDEF";
095  
096  /** An int to hold the length of the mask, accounting for escaped characters **/
097  int maskLength = 0;
098  
099  public MaskFormatter ()
100  {
101    // Override super's default behaviour, in MaskFormatter the default
102    // is not to allow invalid values
103    setAllowsInvalid(false);
104  }
105  
106  /**
107   * Creates a MaskFormatter with the specified mask.
108   * @specnote doesn't actually throw a ParseException although it 
109   * is declared to do so
110   * @param mask
111   * @throws java.text.ParseException
112   */
113  public MaskFormatter (String mask) throws java.text.ParseException
114  {
115    this();
116    setMask (mask);
117  }
118  
119  /**
120   * Returns the mask used in this MaskFormatter.
121   * @return the mask used in this MaskFormatter.
122   */
123  public String getMask()
124  {
125    return mask;
126  }
127  
128  /**
129   * Returns a String containing the characters that are not valid for input
130   * for this MaskFormatter.
131   * @return a String containing the invalid characters.
132   */
133  public String getInvalidCharacters()
134  {
135    return invalidChars;
136  }
137  
138  /**
139   * Sets characters that are not valid for input. If
140   * <code>invalidCharacters</code> is non-null then no characters contained
141   * in it will be allowed to be input.
142   * 
143   * @param invalidCharacters the String specifying invalid characters.
144   */
145  public void setInvalidCharacters (String invalidCharacters)
146  {
147    this.invalidChars = invalidCharacters;
148  }
149  
150  /**
151   * Returns a String containing the characters that are valid for input
152   * for this MaskFormatter.
153   * @return a String containing the valid characters.
154   */
155  public String getValidCharacters()
156  {
157    return validChars;
158  }
159  
160  /**
161   * Sets characters that are valid for input. If
162   * <code>validCharacters</code> is non-null then no characters that are
163   * not contained in it will be allowed to be input.
164   * 
165   * @param validCharacters the String specifying valid characters.
166   */
167  public void setValidCharacters (String validCharacters)
168  {
169    this.validChars = validCharacters;
170  }
171
172  /**
173   * Returns the place holder String that is used in place of missing 
174   * characters when the value doesn't completely fill in the spaces
175   * in the mask.
176   * @return the place holder String.
177   */
178  public String getPlaceholder()
179  {
180    return placeHolder;
181  }
182  
183  /**
184   * Sets the string to use if the value does not completely fill in the mask.
185   * If this is null, the place holder character will be used instead.
186   * @param placeholder the String to use if the value doesn't completely 
187   * fill in the mask.
188   */
189  public void setPlaceholder (String placeholder)
190  {
191    this.placeHolder = placeholder;
192  }
193  
194  /**
195   * Returns the character used in place of missing characters when the
196   * value doesn't completely fill the mask.
197   * @return the place holder character
198   */
199  public char getPlaceholderCharacter()
200  {
201    return placeHolderChar;
202  }
203  
204  /**
205   * Sets the char  to use if the value does not completely fill in the mask.
206   * This is only used if the place holder String has not been set or does 
207   * not completely fill in the mask.
208   * @param placeholder the char to use if the value doesn't completely 
209   * fill in the mask.
210   */
211  public void setPlaceholderCharacter (char placeholder)
212  {
213    this.placeHolderChar = placeholder;
214  }
215  
216  /**
217   * Returns true if stringToValue should return the literal 
218   * characters in the mask.
219   * @return true if stringToValue should return the literal 
220   * characters in the mask
221   */
222  public boolean getValueContainsLiteralCharacters()
223  {
224    return valueContainsLiteralCharacters;
225  }
226  
227  /**
228   * Determines whether stringToValue will return literal characters or not.
229   * @param containsLiteralChars if true, stringToValue will return the 
230   * literal characters in the mask, otherwise it will not.
231   */
232  public void setValueContainsLiteralCharacters (boolean containsLiteralChars)
233  {
234    this.valueContainsLiteralCharacters = containsLiteralChars;
235  }
236  
237  /**
238   * Sets the mask for this MaskFormatter.  
239   * @specnote doesn't actually throw a ParseException even though it is
240   * declared to do so
241   * @param mask the new mask for this MaskFormatter
242   * @throws ParseException if <code>mask</code> is not valid.
243   */
244  public void setMask (String mask) throws ParseException
245  {
246    this.mask = mask;
247
248    // Update the cached maskLength.
249    int end = mask.length() - 1;
250    maskLength = 0;    
251    for (int i = 0; i <= end; i++)
252      {
253        // Handle escape characters properly - they don't add to the maskLength
254        // but 2 escape characters in a row is really one escape character and
255        // one literal single quote, so that does add 1 to the maskLength.
256        if (mask.charAt(i) == '\'')
257          {            
258            // Escape characters at the end of the mask don't do anything.
259            if (i != end)
260              maskLength++;
261            i++;
262          }
263        else
264          maskLength++;
265      }
266  }
267  
268  /**
269   * Installs this MaskFormatter on the JFormattedTextField.
270   * Invokes valueToString to convert the current value from the 
271   * JFormattedTextField to a String, then installs the Actions from
272   * getActions, the DocumentFilter from getDocumentFilter, and the 
273   * NavigationFilter from getNavigationFilter.
274   * 
275   * If valueToString throws a ParseException, this method sets the text
276   * to an empty String and marks the JFormattedTextField as invalid.
277   */
278  public void install (JFormattedTextField ftf)
279  {
280    super.install(ftf);
281    if (ftf != null)
282      {
283        try
284        {
285          valueToString(ftf.getValue());
286        }
287        catch (ParseException pe)
288        {
289          // Set the text to an empty String and mark the JFormattedTextField
290          // as invalid.
291          ftf.setText("");
292          setEditValid(false);
293        }
294      }
295  }
296  
297  /**
298   * Parses the text using the mask, valid characters, and invalid characters
299   * to determine the appropriate Object to return.  This strips the literal
300   * characters if necessary and invokes super.stringToValue.  If the paramter
301   * is invalid for the current mask and valid/invalid character sets this 
302   * method will throw a ParseException.
303   * 
304   * @param value the String to parse
305   * @throws ParseException if value doesn't match the mask and valid/invalid
306   * character sets
307   */
308  public Object stringToValue (String value) throws ParseException
309  {
310    return super.stringToValue(convertStringToValue(value));
311  }
312  
313  private String convertStringToValue(String value)
314    throws ParseException
315  {
316    CPStringBuilder result = new CPStringBuilder();
317    char valueChar;
318    boolean isPlaceHolder;
319
320    int length = mask.length();
321    for (int i = 0, j = 0; j < length; j++)
322      {
323        char maskChar = mask.charAt(j);
324
325        if (i < value.length())
326          {
327            isPlaceHolder = false;
328            valueChar = value.charAt(i);
329            if (maskChar != ESCAPE_CHAR && maskChar != valueChar)
330              {
331                if (invalidChars != null
332                    && invalidChars.indexOf(valueChar) != -1)
333                  throw new ParseException("Invalid character: " + valueChar, i);
334                if (validChars != null
335                    && validChars.indexOf(valueChar) == -1)
336                  throw new ParseException("Invalid character: " + valueChar, i);
337              }
338          }
339        else if (placeHolder != null && i < placeHolder.length())
340          {
341            isPlaceHolder = true;
342            valueChar = placeHolder.charAt(i);
343          }
344        else
345          {
346            isPlaceHolder = true;
347            valueChar = placeHolderChar;
348          }
349
350        // This switch block on the mask character checks that the character 
351        // within <code>value</code> at that point is valid according to the
352        // mask and also converts to upper/lowercase as needed.
353        switch (maskChar)
354          {
355          case NUM_CHAR:
356            if (! Character.isDigit(valueChar))
357              throw new ParseException("Number expected: " + valueChar, i);
358            result.append(valueChar);
359            i++;
360            break;
361          case UPPERCASE_CHAR:
362            if (! Character.isLetter(valueChar))
363              throw new ParseException("Letter expected", i);
364            result.append(Character.toUpperCase(valueChar));
365            i++;
366            break;
367          case LOWERCASE_CHAR:
368            if (! Character.isLetter(valueChar))
369              throw new ParseException("Letter expected", i);
370            result.append(Character.toLowerCase(valueChar));
371            i++;
372            break;
373          case ALPHANUM_CHAR:
374            if (! Character.isLetterOrDigit(valueChar))
375              throw new ParseException("Letter or number expected", i);
376            result.append(valueChar);
377            i++;
378            break;
379          case LETTER_CHAR:
380            if (! Character.isLetter(valueChar))
381              throw new ParseException("Letter expected", i);
382            result.append(valueChar);
383            i++;
384            break;
385          case HEX_CHAR:
386            if (hexString.indexOf(valueChar) == -1 && ! isPlaceHolder)
387              throw new ParseException("Hexadecimal character expected", i);
388            result.append(valueChar);
389            i++;
390            break;
391          case ANYTHING_CHAR:
392            result.append(valueChar);
393            i++;
394            break;
395          case ESCAPE_CHAR:
396            // Escape character, check the next character to make sure that 
397            // the literals match
398            j++;
399            if (j < length)
400              {
401                maskChar = mask.charAt(j);
402                if (! isPlaceHolder && getValueContainsLiteralCharacters()
403                    && valueChar != maskChar)
404                  throw new ParseException ("Invalid character: "+ valueChar, i);
405                if (getValueContainsLiteralCharacters())
406                  {
407                    result.append(maskChar);
408                  }
409                i++;
410              }
411            else if (! isPlaceHolder)
412              throw new ParseException("Bad match at trailing escape: ", i);
413            break;
414          default:
415            if (! isPlaceHolder && getValueContainsLiteralCharacters()
416                && valueChar != maskChar)
417              throw new ParseException ("Invalid character: "+ valueChar, i);
418            if (getValueContainsLiteralCharacters())
419              {
420                result.append(maskChar);
421              }
422            i++;
423          }
424      }
425    return result.toString();
426  }
427
428  /**
429   * Returns a String representation of the Object value based on the mask.
430   * 
431   * @param value the value to convert
432   * @throws ParseException if value is invalid for this mask and valid/invalid
433   * character sets
434   */
435  public String valueToString(Object value) throws ParseException
436  {
437    String string = value != null ? value.toString() : "";
438    return convertValueToString(string);
439  }
440  
441  /**
442   * This method takes in a String and runs it through the mask to make
443   * sure that it is valid.  If <code>convert</code> is true, it also
444   * converts letters to upper/lowercase as required by the mask.
445   * @param value the String to convert
446   * @return the converted String
447   * @throws ParseException if the given String isn't valid for the mask
448   */
449  private String convertValueToString(String value)
450    throws ParseException
451  {
452    CPStringBuilder result = new CPStringBuilder();
453    char valueChar;
454    boolean isPlaceHolder;
455
456    int length = mask.length();
457    for (int i = 0, j = 0; j < length; j++)
458      {
459        char maskChar = mask.charAt(j);
460        if (i < value.length())
461          {
462            isPlaceHolder = false;
463            valueChar = value.charAt(i);
464            if (maskChar != ESCAPE_CHAR && valueChar != maskChar)
465              {
466                if (invalidChars != null
467                    && invalidChars.indexOf(valueChar) != -1)
468                  throw new ParseException("Invalid character: " + valueChar,
469                                           i);
470                if (validChars != null && validChars.indexOf(valueChar) == -1)
471                  throw new ParseException("Invalid character: " + valueChar +" maskChar: " + maskChar,
472                                           i);
473              }
474          }
475        else if (placeHolder != null && i < placeHolder.length())
476          {
477            isPlaceHolder = true;
478            valueChar = placeHolder.charAt(i);
479          }
480        else
481          {
482            isPlaceHolder = true;
483            valueChar = placeHolderChar;
484          }
485
486        // This switch block on the mask character checks that the character 
487        // within <code>value</code> at that point is valid according to the
488        // mask and also converts to upper/lowercase as needed.
489        switch (maskChar)
490          {
491          case NUM_CHAR:
492            if ( ! isPlaceHolder && ! Character.isDigit(valueChar))
493              throw new ParseException("Number expected: " + valueChar, i);
494            result.append(valueChar);
495            i++;
496            break;
497          case UPPERCASE_CHAR:
498            if (! Character.isLetter(valueChar))
499              throw new ParseException("Letter expected", i);
500            result.append(Character.toUpperCase(valueChar));
501            i++;
502            break;
503          case LOWERCASE_CHAR:
504            if (! Character.isLetter(valueChar))
505              throw new ParseException("Letter expected", i);
506            result.append(Character.toLowerCase(valueChar));
507            i++;
508            break;
509          case ALPHANUM_CHAR:
510            if (! Character.isLetterOrDigit(valueChar))
511              throw new ParseException("Letter or number expected", i);
512            result.append(valueChar);
513            i++;
514            break;
515          case LETTER_CHAR:
516            if (! Character.isLetter(valueChar))
517              throw new ParseException("Letter expected", i);
518            result.append(valueChar);
519            i++;
520            break;
521          case HEX_CHAR:
522            if (hexString.indexOf(valueChar) == -1 && ! isPlaceHolder)
523              throw new ParseException("Hexadecimal character expected", i);
524            result.append(valueChar);
525            i++;
526            break;
527          case ANYTHING_CHAR:
528            result.append(valueChar);
529            i++;
530            break;
531          case ESCAPE_CHAR:
532            // Escape character, check the next character to make sure that 
533            // the literals match
534            j++;
535            if (j < length)
536              {
537                maskChar = mask.charAt(j);
538                if (! isPlaceHolder && getValueContainsLiteralCharacters()
539                    && valueChar != maskChar)
540                  throw new ParseException ("Invalid character: "+ valueChar, i);
541                if (getValueContainsLiteralCharacters())
542                  i++;
543                result.append(maskChar);
544              }
545            break;
546          default:
547            if (! isPlaceHolder && getValueContainsLiteralCharacters()
548                && valueChar != maskChar)
549              throw new ParseException ("Invalid character: "+ valueChar, i);
550            if (getValueContainsLiteralCharacters())
551              i++;
552            result.append(maskChar);
553          }
554      }
555    return result.toString();
556  }
557
558}