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