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 */
25package javax.swing.text;
26
27import sun.reflect.misc.ReflectUtil;
28import sun.swing.SwingUtilities2;
29
30import java.io.Serializable;
31import java.lang.reflect.*;
32import java.text.ParseException;
33import javax.swing.*;
34
35/**
36 * <code>DefaultFormatter</code> formats arbitrary objects. Formatting is done
37 * by invoking the <code>toString</code> method. In order to convert the
38 * value back to a String, your class must provide a constructor that
39 * takes a String argument. If no single argument constructor that takes a
40 * String is found, the returned value will be the String passed into
41 * <code>stringToValue</code>.
42 * <p>
43 * Instances of <code>DefaultFormatter</code> can not be used in multiple
44 * instances of <code>JFormattedTextField</code>. To obtain a copy of
45 * an already configured <code>DefaultFormatter</code>, use the
46 * <code>clone</code> method.
47 * <p>
48 * <strong>Warning:</strong>
49 * Serialized objects of this class will not be compatible with
50 * future Swing releases. The current serialization support is
51 * appropriate for short term storage or RMI between applications running
52 * the same version of Swing.  As of 1.4, support for long term storage
53 * of all JavaBeans&trade;
54 * has been added to the <code>java.beans</code> package.
55 * Please see {@link java.beans.XMLEncoder}.
56 *
57 * @see javax.swing.JFormattedTextField.AbstractFormatter
58 *
59 * @since 1.4
60 */
61@SuppressWarnings("serial") // Same-version serialization only
62public class DefaultFormatter extends JFormattedTextField.AbstractFormatter
63                    implements Cloneable, Serializable {
64    /** Indicates if the value being edited must match the mask. */
65    private boolean allowsInvalid;
66
67    /** If true, editing mode is in overwrite (or strikethough). */
68    private boolean overwriteMode;
69
70    /** If true, any time a valid edit happens commitEdit is invoked. */
71    private boolean commitOnEdit;
72
73    /** Class used to create new instances. */
74    private Class<?> valueClass;
75
76    /** NavigationFilter that forwards calls back to DefaultFormatter. */
77    private NavigationFilter navigationFilter;
78
79    /** DocumentFilter that forwards calls back to DefaultFormatter. */
80    private DocumentFilter documentFilter;
81
82    /** Used during replace to track the region to replace. */
83    transient ReplaceHolder replaceHolder;
84
85
86    /**
87     * Creates a DefaultFormatter.
88     */
89    public DefaultFormatter() {
90        overwriteMode = true;
91        allowsInvalid = true;
92    }
93
94    /**
95     * Installs the <code>DefaultFormatter</code> onto a particular
96     * <code>JFormattedTextField</code>.
97     * This will invoke <code>valueToString</code> to convert the
98     * current value from the <code>JFormattedTextField</code> to
99     * a String. This will then install the <code>Action</code>s from
100     * <code>getActions</code>, the <code>DocumentFilter</code>
101     * returned from <code>getDocumentFilter</code> and the
102     * <code>NavigationFilter</code> returned from
103     * <code>getNavigationFilter</code> onto the
104     * <code>JFormattedTextField</code>.
105     * <p>
106     * Subclasses will typically only need to override this if they
107     * wish to install additional listeners on the
108     * <code>JFormattedTextField</code>.
109     * <p>
110     * If there is a <code>ParseException</code> in converting the
111     * current value to a String, this will set the text to an empty
112     * String, and mark the <code>JFormattedTextField</code> as being
113     * in an invalid state.
114     * <p>
115     * While this is a public method, this is typically only useful
116     * for subclassers of <code>JFormattedTextField</code>.
117     * <code>JFormattedTextField</code> will invoke this method at
118     * the appropriate times when the value changes, or its internal
119     * state changes.
120     *
121     * @param ftf JFormattedTextField to format for, may be null indicating
122     *            uninstall from current JFormattedTextField.
123     */
124    public void install(JFormattedTextField ftf) {
125        super.install(ftf);
126        positionCursorAtInitialLocation();
127    }
128
129    /**
130     * Sets when edits are published back to the
131     * <code>JFormattedTextField</code>. If true, <code>commitEdit</code>
132     * is invoked after every valid edit (any time the text is edited). On
133     * the other hand, if this is false than the <code>DefaultFormatter</code>
134     * does not publish edits back to the <code>JFormattedTextField</code>.
135     * As such, the only time the value of the <code>JFormattedTextField</code>
136     * will change is when <code>commitEdit</code> is invoked on
137     * <code>JFormattedTextField</code>, typically when enter is pressed
138     * or focus leaves the <code>JFormattedTextField</code>.
139     *
140     * @param commit Used to indicate when edits are committed back to the
141     *               JTextComponent
142     */
143    public void setCommitsOnValidEdit(boolean commit) {
144        commitOnEdit = commit;
145    }
146
147    /**
148     * Returns when edits are published back to the
149     * <code>JFormattedTextField</code>.
150     *
151     * @return true if edits are committed after every valid edit
152     */
153    public boolean getCommitsOnValidEdit() {
154        return commitOnEdit;
155    }
156
157    /**
158     * Configures the behavior when inserting characters. If
159     * <code>overwriteMode</code> is true (the default), new characters
160     * overwrite existing characters in the model.
161     *
162     * @param overwriteMode Indicates if overwrite or overstrike mode is used
163     */
164    public void setOverwriteMode(boolean overwriteMode) {
165        this.overwriteMode = overwriteMode;
166    }
167
168    /**
169     * Returns the behavior when inserting characters.
170     *
171     * @return true if newly inserted characters overwrite existing characters
172     */
173    public boolean getOverwriteMode() {
174        return overwriteMode;
175    }
176
177    /**
178     * Sets whether or not the value being edited is allowed to be invalid
179     * for a length of time (that is, <code>stringToValue</code> throws
180     * a <code>ParseException</code>).
181     * It is often convenient to allow the user to temporarily input an
182     * invalid value.
183     *
184     * @param allowsInvalid Used to indicate if the edited value must always
185     *        be valid
186     */
187    public void setAllowsInvalid(boolean allowsInvalid) {
188        this.allowsInvalid = allowsInvalid;
189    }
190
191    /**
192     * Returns whether or not the value being edited is allowed to be invalid
193     * for a length of time.
194     *
195     * @return false if the edited value must always be valid
196     */
197    public boolean getAllowsInvalid() {
198        return allowsInvalid;
199    }
200
201    /**
202     * Sets that class that is used to create new Objects. If the
203     * passed in class does not have a single argument constructor that
204     * takes a String, String values will be used.
205     *
206     * @param valueClass Class used to construct return value from
207     *        stringToValue
208     */
209    public void setValueClass(Class<?> valueClass) {
210        this.valueClass = valueClass;
211    }
212
213    /**
214     * Returns that class that is used to create new Objects.
215     *
216     * @return Class used to construct return value from stringToValue
217     */
218    public Class<?> getValueClass() {
219        return valueClass;
220    }
221
222    /**
223     * Converts the passed in String into an instance of
224     * <code>getValueClass</code> by way of the constructor that
225     * takes a String argument. If <code>getValueClass</code>
226     * returns null, the Class of the current value in the
227     * <code>JFormattedTextField</code> will be used. If this is null, a
228     * String will be returned. If the constructor throws an exception, a
229     * <code>ParseException</code> will be thrown. If there is no single
230     * argument String constructor, <code>string</code> will be returned.
231     *
232     * @throws ParseException if there is an error in the conversion
233     * @param string String to convert
234     * @return Object representation of text
235     */
236    public Object stringToValue(String string) throws ParseException {
237        Class<?> vc = getValueClass();
238        JFormattedTextField ftf = getFormattedTextField();
239
240        if (vc == null && ftf != null) {
241            Object value = ftf.getValue();
242
243            if (value != null) {
244                vc = value.getClass();
245            }
246        }
247        if (vc != null) {
248            Constructor<?> cons;
249
250            try {
251                ReflectUtil.checkPackageAccess(vc);
252                SwingUtilities2.checkAccess(vc.getModifiers());
253                cons = vc.getConstructor(new Class<?>[]{String.class});
254
255            } catch (NoSuchMethodException nsme) {
256                cons = null;
257            }
258
259            if (cons != null) {
260                try {
261                    SwingUtilities2.checkAccess(cons.getModifiers());
262                    return cons.newInstance(new Object[] { string });
263                } catch (Throwable ex) {
264                    throw new ParseException("Error creating instance", 0);
265                }
266            }
267        }
268        return string;
269    }
270
271    /**
272     * Converts the passed in Object into a String by way of the
273     * <code>toString</code> method.
274     *
275     * @throws ParseException if there is an error in the conversion
276     * @param value Value to convert
277     * @return String representation of value
278     */
279    public String valueToString(Object value) throws ParseException {
280        if (value == null) {
281            return "";
282        }
283        return value.toString();
284    }
285
286    /**
287     * Returns the <code>DocumentFilter</code> used to restrict the characters
288     * that can be input into the <code>JFormattedTextField</code>.
289     *
290     * @return DocumentFilter to restrict edits
291     */
292    protected DocumentFilter getDocumentFilter() {
293        if (documentFilter == null) {
294            documentFilter = new DefaultDocumentFilter();
295        }
296        return documentFilter;
297    }
298
299    /**
300     * Returns the <code>NavigationFilter</code> used to restrict where the
301     * cursor can be placed.
302     *
303     * @return NavigationFilter to restrict navigation
304     */
305    protected NavigationFilter getNavigationFilter() {
306        if (navigationFilter == null) {
307            navigationFilter = new DefaultNavigationFilter();
308        }
309        return navigationFilter;
310    }
311
312    /**
313     * Creates a copy of the DefaultFormatter.
314     *
315     * @return copy of the DefaultFormatter
316     */
317    public Object clone() throws CloneNotSupportedException {
318        DefaultFormatter formatter = (DefaultFormatter)super.clone();
319
320        formatter.navigationFilter = null;
321        formatter.documentFilter = null;
322        formatter.replaceHolder = null;
323        return formatter;
324    }
325
326
327    /**
328     * Positions the cursor at the initial location.
329     */
330    void positionCursorAtInitialLocation() {
331        JFormattedTextField ftf = getFormattedTextField();
332        if (ftf != null) {
333            ftf.setCaretPosition(getInitialVisualPosition());
334        }
335    }
336
337    /**
338     * Returns the initial location to position the cursor at. This forwards
339     * the call to <code>getNextNavigatableChar</code>.
340     */
341    int getInitialVisualPosition() {
342        return getNextNavigatableChar(0, 1);
343    }
344
345    /**
346     * Subclasses should override this if they want cursor navigation
347     * to skip certain characters. A return value of false indicates
348     * the character at <code>offset</code> should be skipped when
349     * navigating throught the field.
350     */
351    boolean isNavigatable(int offset) {
352        return true;
353    }
354
355    /**
356     * Returns true if the text in <code>text</code> can be inserted.  This
357     * does not mean the text will ultimately be inserted, it is used if
358     * text can trivially reject certain characters.
359     */
360    boolean isLegalInsertText(String text) {
361        return true;
362    }
363
364    /**
365     * Returns the next editable character starting at offset incrementing
366     * the offset by <code>direction</code>.
367     */
368    private int getNextNavigatableChar(int offset, int direction) {
369        int max = getFormattedTextField().getDocument().getLength();
370
371        while (offset >= 0 && offset < max) {
372            if (isNavigatable(offset)) {
373                return offset;
374            }
375            offset += direction;
376        }
377        return offset;
378    }
379
380    /**
381     * A convenience methods to return the result of deleting
382     * <code>deleteLength</code> characters at <code>offset</code>
383     * and inserting <code>replaceString</code> at <code>offset</code>
384     * in the current text field.
385     */
386    String getReplaceString(int offset, int deleteLength,
387                            String replaceString) {
388        String string = getFormattedTextField().getText();
389        String result;
390
391        result = string.substring(0, offset);
392        if (replaceString != null) {
393            result += replaceString;
394        }
395        if (offset + deleteLength < string.length()) {
396            result += string.substring(offset + deleteLength);
397        }
398        return result;
399    }
400
401    /*
402     * Returns true if the operation described by <code>rh</code> will
403     * result in a legal edit.  This may set the <code>value</code>
404     * field of <code>rh</code>.
405     */
406    boolean isValidEdit(ReplaceHolder rh) {
407        if (!getAllowsInvalid()) {
408            String newString = getReplaceString(rh.offset, rh.length, rh.text);
409
410            try {
411                rh.value = stringToValue(newString);
412
413                return true;
414            } catch (ParseException pe) {
415                return false;
416            }
417        }
418        return true;
419    }
420
421    /**
422     * Invokes <code>commitEdit</code> on the JFormattedTextField.
423     */
424    void commitEdit() throws ParseException {
425        JFormattedTextField ftf = getFormattedTextField();
426
427        if (ftf != null) {
428            ftf.commitEdit();
429        }
430    }
431
432    /**
433     * Pushes the value to the JFormattedTextField if the current value
434     * is valid and invokes <code>setEditValid</code> based on the
435     * validity of the value.
436     */
437    void updateValue() {
438        updateValue(null);
439    }
440
441    /**
442     * Pushes the <code>value</code> to the editor if we are to
443     * commit on edits. If <code>value</code> is null, the current value
444     * will be obtained from the text component.
445     */
446    void updateValue(Object value) {
447        try {
448            if (value == null) {
449                String string = getFormattedTextField().getText();
450
451                value = stringToValue(string);
452            }
453
454            if (getCommitsOnValidEdit()) {
455                commitEdit();
456            }
457            setEditValid(true);
458        } catch (ParseException pe) {
459            setEditValid(false);
460        }
461    }
462
463    /**
464     * Returns the next cursor position from offset by incrementing
465     * <code>direction</code>. This uses
466     * <code>getNextNavigatableChar</code>
467     * as well as constraining the location to the max position.
468     */
469    int getNextCursorPosition(int offset, int direction) {
470        int newOffset = getNextNavigatableChar(offset, direction);
471        int max = getFormattedTextField().getDocument().getLength();
472
473        if (!getAllowsInvalid()) {
474            if (direction == -1 && offset == newOffset) {
475                // Case where hit backspace and only characters before
476                // offset are fixed.
477                newOffset = getNextNavigatableChar(newOffset, 1);
478                if (newOffset >= max) {
479                    newOffset = offset;
480                }
481            }
482            else if (direction == 1 && newOffset >= max) {
483                // Don't go beyond last editable character.
484                newOffset = getNextNavigatableChar(max - 1, -1);
485                if (newOffset < max) {
486                    newOffset++;
487                }
488            }
489        }
490        return newOffset;
491    }
492
493    /**
494     * Resets the cursor by using getNextCursorPosition.
495     */
496    void repositionCursor(int offset, int direction) {
497        getFormattedTextField().getCaret().setDot(getNextCursorPosition
498                                                  (offset, direction));
499    }
500
501
502    /**
503     * Finds the next navigable character.
504     */
505    int getNextVisualPositionFrom(JTextComponent text, int pos,
506                                  Position.Bias bias, int direction,
507                                  Position.Bias[] biasRet)
508                                           throws BadLocationException {
509        int value = text.getUI().getNextVisualPositionFrom(text, pos, bias,
510                                                           direction, biasRet);
511
512        if (value == -1) {
513            return -1;
514        }
515        if (!getAllowsInvalid() && (direction == SwingConstants.EAST ||
516                                    direction == SwingConstants.WEST)) {
517            int last = -1;
518
519            while (!isNavigatable(value) && value != last) {
520                last = value;
521                value = text.getUI().getNextVisualPositionFrom(
522                              text, value, bias, direction,biasRet);
523            }
524            int max = getFormattedTextField().getDocument().getLength();
525            if (last == value || value == max) {
526                if (value == 0) {
527                    biasRet[0] = Position.Bias.Forward;
528                    value = getInitialVisualPosition();
529                }
530                if (value >= max && max > 0) {
531                    // Pending: should not assume forward!
532                    biasRet[0] = Position.Bias.Forward;
533                    value = getNextNavigatableChar(max - 1, -1) + 1;
534                }
535            }
536        }
537        return value;
538    }
539
540    /**
541     * Returns true if the edit described by <code>rh</code> will result
542     * in a legal value.
543     */
544    boolean canReplace(ReplaceHolder rh) {
545        return isValidEdit(rh);
546    }
547
548    /**
549     * DocumentFilter method, funnels into <code>replace</code>.
550     */
551    void replace(DocumentFilter.FilterBypass fb, int offset,
552                     int length, String text,
553                     AttributeSet attrs) throws BadLocationException {
554        ReplaceHolder rh = getReplaceHolder(fb, offset, length, text, attrs);
555
556        replace(rh);
557    }
558
559    /**
560     * If the edit described by <code>rh</code> is legal, this will
561     * return true, commit the edit (if necessary) and update the cursor
562     * position.  This forwards to <code>canReplace</code> and
563     * <code>isLegalInsertText</code> as necessary to determine if
564     * the edit is in fact legal.
565     * <p>
566     * All of the DocumentFilter methods funnel into here, you should
567     * generally only have to override this.
568     */
569    boolean replace(ReplaceHolder rh) throws BadLocationException {
570        boolean valid = true;
571        int direction = 1;
572
573        if (rh.length > 0 && (rh.text == null || rh.text.length() == 0) &&
574               (getFormattedTextField().getSelectionStart() != rh.offset ||
575                   rh.length > 1)) {
576            direction = -1;
577        }
578
579        if (getOverwriteMode() && rh.text != null &&
580            getFormattedTextField().getSelectedText() == null)
581        {
582            rh.length = Math.min(Math.max(rh.length, rh.text.length()),
583                                 rh.fb.getDocument().getLength() - rh.offset);
584        }
585        if ((rh.text != null && !isLegalInsertText(rh.text)) ||
586            !canReplace(rh) ||
587            (rh.length == 0 && (rh.text == null || rh.text.length() == 0))) {
588            valid = false;
589        }
590        if (valid) {
591            int cursor = rh.cursorPosition;
592
593            rh.fb.replace(rh.offset, rh.length, rh.text, rh.attrs);
594            if (cursor == -1) {
595                cursor = rh.offset;
596                if (direction == 1 && rh.text != null) {
597                    cursor = rh.offset + rh.text.length();
598                }
599            }
600            updateValue(rh.value);
601            repositionCursor(cursor, direction);
602            return true;
603        }
604        else {
605            invalidEdit();
606        }
607        return false;
608    }
609
610    /**
611     * NavigationFilter method, subclasses that wish finer control should
612     * override this.
613     */
614    void setDot(NavigationFilter.FilterBypass fb, int dot, Position.Bias bias){
615        fb.setDot(dot, bias);
616    }
617
618    /**
619     * NavigationFilter method, subclasses that wish finer control should
620     * override this.
621     */
622    void moveDot(NavigationFilter.FilterBypass fb, int dot,
623                 Position.Bias bias) {
624        fb.moveDot(dot, bias);
625    }
626
627
628    /**
629     * Returns the ReplaceHolder to track the replace of the specified
630     * text.
631     */
632    ReplaceHolder getReplaceHolder(DocumentFilter.FilterBypass fb, int offset,
633                                   int length, String text,
634                                   AttributeSet attrs) {
635        if (replaceHolder == null) {
636            replaceHolder = new ReplaceHolder();
637        }
638        replaceHolder.reset(fb, offset, length, text, attrs);
639        return replaceHolder;
640    }
641
642
643    /**
644     * ReplaceHolder is used to track where insert/remove/replace is
645     * going to happen.
646     */
647    static class ReplaceHolder {
648        /** The FilterBypass that was passed to the DocumentFilter method. */
649        DocumentFilter.FilterBypass fb;
650        /** Offset where the remove/insert is going to occur. */
651        int offset;
652        /** Length of text to remove. */
653        int length;
654        /** The text to insert, may be null. */
655        String text;
656        /** AttributeSet to attach to text, may be null. */
657        AttributeSet attrs;
658        /** The resulting value, this may never be set. */
659        Object value;
660        /** Position the cursor should be adjusted from.  If this is -1
661         * the cursor position will be adjusted based on the direction of
662         * the replace (-1: offset, 1: offset + text.length()), otherwise
663         * the cursor position is adusted from this position.
664         */
665        int cursorPosition;
666
667        void reset(DocumentFilter.FilterBypass fb, int offset, int length,
668                   String text, AttributeSet attrs) {
669            this.fb = fb;
670            this.offset = offset;
671            this.length = length;
672            this.text = text;
673            this.attrs = attrs;
674            this.value = null;
675            cursorPosition = -1;
676        }
677    }
678
679
680    /**
681     * NavigationFilter implementation that calls back to methods with
682     * same name in DefaultFormatter.
683     */
684    private class DefaultNavigationFilter extends NavigationFilter
685                             implements Serializable {
686        public void setDot(FilterBypass fb, int dot, Position.Bias bias) {
687            JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
688            if (tc.composedTextExists()) {
689                // bypass the filter
690                fb.setDot(dot, bias);
691            } else {
692                DefaultFormatter.this.setDot(fb, dot, bias);
693            }
694        }
695
696        public void moveDot(FilterBypass fb, int dot, Position.Bias bias) {
697            JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
698            if (tc.composedTextExists()) {
699                // bypass the filter
700                fb.moveDot(dot, bias);
701            } else {
702                DefaultFormatter.this.moveDot(fb, dot, bias);
703            }
704        }
705
706        public int getNextVisualPositionFrom(JTextComponent text, int pos,
707                                             Position.Bias bias,
708                                             int direction,
709                                             Position.Bias[] biasRet)
710                                           throws BadLocationException {
711            if (text.composedTextExists()) {
712                // forward the call to the UI directly
713                return text.getUI().getNextVisualPositionFrom(
714                        text, pos, bias, direction, biasRet);
715            } else {
716                return DefaultFormatter.this.getNextVisualPositionFrom(
717                        text, pos, bias, direction, biasRet);
718            }
719        }
720    }
721
722
723    /**
724     * DocumentFilter implementation that calls back to the replace
725     * method of DefaultFormatter.
726     */
727    private class DefaultDocumentFilter extends DocumentFilter implements
728                             Serializable {
729        public void remove(FilterBypass fb, int offset, int length) throws
730                              BadLocationException {
731            JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
732            if (tc.composedTextExists()) {
733                // bypass the filter
734                fb.remove(offset, length);
735            } else {
736                DefaultFormatter.this.replace(fb, offset, length, null, null);
737            }
738        }
739
740        public void insertString(FilterBypass fb, int offset,
741                                 String string, AttributeSet attr) throws
742                              BadLocationException {
743            JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
744            if (tc.composedTextExists() ||
745                Utilities.isComposedTextAttributeDefined(attr)) {
746                // bypass the filter
747                fb.insertString(offset, string, attr);
748            } else {
749                DefaultFormatter.this.replace(fb, offset, 0, string, attr);
750            }
751        }
752
753        public void replace(FilterBypass fb, int offset, int length,
754                                 String text, AttributeSet attr) throws
755                              BadLocationException {
756            JTextComponent tc = DefaultFormatter.this.getFormattedTextField();
757            if (tc.composedTextExists() ||
758                Utilities.isComposedTextAttributeDefined(attr)) {
759                // bypass the filter
760                fb.replace(offset, length, text, attr);
761            } else {
762                DefaultFormatter.this.replace(fb, offset, length, text, attr);
763            }
764        }
765    }
766}
767