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 gnu.java.lang.CPStringBuilder;
042    
043    import java.text.ParseException;
044    
045    import javax.swing.JFormattedTextField;
046    
047    /**
048     * @author Anthony Balkissoon abalkiss at redhat dot com
049     *
050     */
051    public 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    }