001package hirondelle.stocks.portfolio;
002
003import java.util.*;
004import java.util.regex.*;
005import javax.swing.*;
006import javax.swing.text.*;
007import java.awt.*;
008import java.awt.event.*;
009
010import hirondelle.stocks.util.Util;
011import hirondelle.stocks.util.Consts;
012import hirondelle.stocks.util.Args;
013import hirondelle.stocks.util.ui.UiUtil;
014
015/**
016* Verifies user input into a {@link javax.swing.text.JTextComponent} versus a 
017* regular expression.
018* 
019*<P> This class is likely useful for a wide number of simple input needs.
020* See the {@link java.util.regex.Pattern} class for details regarding regular 
021* expressions.
022*
023* <P>The {@link #main} method is provided as a developer tool for  
024* testing regular expressions versus user input, but the principal use of this 
025* class is to be passed to {@link javax.swing.JComponent#setInputVerifier}.
026*
027*<P> Upon detection of invalid input, this class takes the following actions : 
028*<ul>
029* <li> emit a beep
030* <li> overwrite the <tt>JTextComponent</tt> to display the following: 
031* INVALID: &quot; (input data) &quot;
032* <li> optionally, append the tooltip text to the content of the INVALID message ; this 
033* is useful only if the tooltip contains helpful information regarding input.
034* Warning : appending the tooltip text may cause the error 
035* text to be too long for the corresponding text field. 
036*</ul>
037*
038*<P> The user of this class is encouraged to always place conditions on data entry 
039* in the tooltip for the corresponding field.
040*/
041final class RegexInputVerifier extends InputVerifier {
042  
043  /* 
044  * Implementation Note:
045  * Use of JOptionPane to display error messages in an 
046  * InputVerifier seems buggy. There also seem to be issues 
047  * regarding focus and events.
048  */
049  
050  /**
051  * Constructor.
052  *  
053  * @param aPattern regular expression against which all user input will
054  * be verified; <tt>aPattern.pattern</tt> satisfies
055  * {@link Util#textHasContent}.
056  * @param aUseToolTip indicates if the tooltip text should be appended to 
057  * error messages displayed to the user. 
058  */
059  RegexInputVerifier(Pattern aPattern, UseToolTip aUseToolTip){
060    Args.checkForContent( aPattern.pattern() );
061    fMatcher = aPattern.matcher(Consts.EMPTY_STRING);
062    fUseToolTip = aUseToolTip.getValue();
063  }
064  
065  /** Enumeration compels the caller to use a style which reads clearly. */
066  enum UseToolTip {
067    TRUE(true),
068    FALSE(false);
069    boolean getValue(){
070      return fToggle;
071    }
072    private boolean fToggle;
073    private UseToolTip(boolean aToggle){
074      fToggle = aToggle;
075    }
076  }
077
078  /**
079  * Always returns <tt>true</tt>, in this implementation, such that focus can 
080  * always transfer to another component whenever the validation fails.
081  *
082  * <P>If <tt>super.shouldYieldFocus</tt> returns <tt>false</tt>, then 
083  * notify the user of an error.
084  *
085  * @param aComponent is a <tt>JTextComponent</tt>.
086  */
087  @Override public boolean shouldYieldFocus(JComponent aComponent){
088    boolean isValid = super.shouldYieldFocus(aComponent);
089    if (isValid){
090      //do nothing
091    }
092    else {
093      JTextComponent textComponent = (JTextComponent)aComponent;
094      notifyUserOfError(textComponent);
095    }
096    return true;
097  }
098  
099  /**
100  * Return <tt>true</tt> only if the untrimmed user input matches the 
101  * regular expression provided to the constructor.
102  *
103  * @param aComponent must be a <tt>JTextComponent</tt>.
104  */
105  @Override public boolean verify(JComponent aComponent) {
106    boolean result = false;
107    JTextComponent textComponent = (JTextComponent)aComponent;
108    fMatcher.reset(textComponent.getText());
109    if (fMatcher.matches()) {
110      result =  true;
111    }
112    return result;
113  }
114  
115  /**
116  * The text which begins all error messages.
117  *
118  * The caller may examine their text fields for the presence of 
119  * <tt>ERROR_MESSAGE_START</tt>, before processing input.
120  */
121  static final String ERROR_MESSAGE_START = "INVALID: ";
122
123  /**
124  * Matches user input against a regular expression.
125  */
126  private Matcher fMatcher;
127  
128  /**
129  * Indicates if the JTextField's tooltip text is to be appended to 
130  * error messages, as a second way of reminding the user.
131  */
132  private boolean fUseToolTip;
133
134  /* 
135  * Various regular expression patterns used to 
136  * construct convenience objects of this class:
137  */
138  
139  private static final String TEXT_FIELD =  "^(\\S)(.){1,75}(\\S)$";
140  private static final String NON_NEGATIVE_INTEGER_FIELD = "(\\d){1,9}";
141  private static final String INTEGER_FIELD = "(-)?" + NON_NEGATIVE_INTEGER_FIELD;
142  private static final String NON_NEGATIVE_FLOATING_POINT_FIELD = 
143    "(\\d){1,10}\\.(\\d){1,10}"
144  ;
145  private static final String FLOATING_POINT_FIELD =  
146    "(-)?" + NON_NEGATIVE_FLOATING_POINT_FIELD
147  ;
148  private static final String NON_NEGATIVE_MONEY_FIELD =  "(\\d){1,15}(\\.(\\d){2})?";
149  private static final String MONEY_FIELD =  "(-)?" + NON_NEGATIVE_MONEY_FIELD;
150  
151  /**  
152  * Convenience object for input of integers: ...-2,-1,0,1,2...
153  *
154  * <P>From 1 to 9 digits, possibly preceded by a minus sign.
155  * Corresponds approximately to the spec of <tt>Integer.parseInt</tt>.
156  * The limit on the number of digits is related to size of <tt>Integer.MAX_VALUE</tt>
157  * and <tt>Integer.MIN_VALUE</tt>.
158  */
159  static final RegexInputVerifier INTEGER = 
160    new RegexInputVerifier(Pattern.compile(INTEGER_FIELD), UseToolTip.FALSE)
161  ;
162
163  /**
164  * Convenience object for input of these integers: 0,1,2...
165  *
166  *<P> As in {@link #INTEGER}, but with no leading minus sign.
167  */
168  static final RegexInputVerifier NON_NEGATIVE_INTEGER = 
169    new RegexInputVerifier(Pattern.compile(NON_NEGATIVE_INTEGER_FIELD), UseToolTip.FALSE)
170  ;
171         
172  /**
173  * Convenience object for input of short amounts of text.
174  *
175  * <P>Text contains from 1 to 75 non-whitespace characters.
176  */
177  static final RegexInputVerifier TEXT = 
178    new RegexInputVerifier(Pattern.compile(TEXT_FIELD), UseToolTip.FALSE)
179  ;
180    
181  /**
182  * Convenience object for input of decimals numbers, eg -23.23321, 100.25.
183  *
184  * <P>Possible leading minus sign, 1 to 10 digits before the decimal, and 1 to 10 
185  * digits after the decimal.
186  */
187  static final RegexInputVerifier FLOATING_POINT = 
188    new RegexInputVerifier(Pattern.compile(FLOATING_POINT_FIELD), UseToolTip.FALSE)
189  ;
190
191  /**
192  * Convenience object for input of non-negative decimals numbers, eg 23.23321, 100.25.
193  *
194  * <P>As in {@link #FLOATING_POINT}, but no leading minus sign.
195  */
196  static final RegexInputVerifier NON_NEGATIVE_FLOATING_POINT = 
197    new RegexInputVerifier(
198      Pattern.compile(NON_NEGATIVE_FLOATING_POINT_FIELD), UseToolTip.FALSE
199    )
200  ;
201
202  /**
203  * Convenience object for input of money values, eg -23, 100.25.
204  *
205  * <P>Possible leading minus sign, from 1 to 15 leading digits, and optionally  
206  * a decimal place and two decimals.
207  */
208  static final RegexInputVerifier MONEY = 
209    new RegexInputVerifier(Pattern.compile(MONEY_FIELD), UseToolTip.FALSE)
210  ;
211  
212  /**
213  * Convenience object for input of non-negative money values, eg 23, 100.25.
214  *
215  * <P>As in {@link #MONEY}, except no leading minus sign
216  */
217  static final RegexInputVerifier NON_NEGATIVE_MONEY = 
218    new RegexInputVerifier(Pattern.compile(NON_NEGATIVE_MONEY_FIELD), UseToolTip.FALSE)
219  ;
220
221  /**
222  * If an error message is currently displayed in aComponent, then 
223  * do nothing; otherwise, display an error message to the user in a
224  * aComponent (see class description for format of message).
225  */
226  private void notifyUserOfError(JTextComponent aTextComponent){
227    if ( isShowingErrorMessage(aTextComponent) ){
228      //do nothing, since user has not yet re-input.
229    }
230    else {
231      UiUtil.beep();
232      showErrorMessage(aTextComponent);
233    }
234  }
235
236  private boolean isShowingErrorMessage(JTextComponent aTextComponent){
237    return aTextComponent.getText().startsWith(ERROR_MESSAGE_START);
238  }
239  
240  private void showErrorMessage(JTextComponent aTextComponent) {
241    StringBuilder message = new StringBuilder(ERROR_MESSAGE_START);
242    message.append("\"");
243    message.append(aTextComponent.getText());
244    message.append("\"");
245    if ( fUseToolTip ) {
246      message.append(aTextComponent.getToolTipText());
247    }
248    aTextComponent.setText(message.toString());
249  }
250  
251  /*
252  * All members appearing below are used only by the "main" developer test harness.
253  */
254  
255  /**
256  * Developer test harness for verifying a regular expression, using a simple 
257  * graphical interface and a <tt>RegexInputVerifier</tt>.
258  *
259  *<p>Use of the GUI is straightforward :
260  *<ul>
261  *<li> Enter a regular expression (regex).
262  *<li> Hit the <tt>Set Regex</tt> button to begin testing input versus the regex.
263  *<li> Enter text into the <tt>Text Input</tt> field.
264  *<li> When focus moves out of the <tt>Text Input</tt> field, then the input 
265  * is verified versus the regex.
266  *<li> To try out another regex, simply enter a new one and hit the 
267  * <tt>Set Regex</tt> button.
268  *<li> Tooltips offer simple reminders as well.
269  *</ul>
270  *
271  * <p>(For running this test harness, <tt>RegexInputVerifier</tt> needs to 
272  * be a public class; after testing is finished, it is probably a good idea to 
273  * change the scope to package-private, since the services of this class are 
274  * used only by the user interface layer.)
275  */
276  public static void main(String... aArgs){
277    showGuiForExercisingVerifier();
278  }
279  
280  private static JFrame fFrame;
281  private static JTextField fRegexField;
282  private static JTextField fInputField;
283  
284  private static void showGuiForExercisingVerifier(){
285    //General layout of the gui:
286    //  
287    //  Regular Expression:     [----] 
288    //  Test Input:             [----]
289    //
290    //  Set-Regex     Test       Close
291    //
292    fFrame = new JFrame("Test Regular Expressions - javapractices.com");
293    fFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
294    
295    JPanel content = new JPanel();
296    content.setLayout( new BoxLayout(content, BoxLayout.Y_AXIS) );
297    content.setBorder( UiUtil.getStandardBorder() );
298    content.add( getEditor() );
299    content.add( getCommandRow() );
300    
301    fFrame.getContentPane().add( content );
302    UiUtil.centerAndShow( fFrame );
303  }
304  
305  private static JComponent getEditor(){
306    final JPanel content = new JPanel();
307    content.setLayout( new GridBagLayout() );
308    GridBagConstraints constraints = UiUtil.getConstraints(0,0);
309    constraints.fill = GridBagConstraints.HORIZONTAL;
310    fRegexField = UiUtil.addSimpleEntryField(
311      content, "Regular Expression:", null, 
312      KeyEvent.VK_R, constraints, 
313      "Double backslashes are not needed. Set Regex to init."
314    );
315    constraints = UiUtil.getConstraints(1,0);
316    constraints.fill = GridBagConstraints.HORIZONTAL;
317    fInputField = UiUtil.addSimpleEntryField(
318      content, "Test Input:", null, 
319      KeyEvent.VK_I, constraints,
320      "This text is verified versus the regular expression"
321    );
322    UiUtil.addVerticalGridGlue(content, 2);
323    return content;
324  }
325  
326  private static JComponent getCommandRow(){
327    JButton setRegex = new JButton("Set Regex");
328    setRegex.setMnemonic(KeyEvent.VK_S);
329    setRegex.setToolTipText("Start testing input versus the above regular expression");
330    setRegex.addActionListener( new ActionListener() {
331      public void actionPerformed(ActionEvent event) {
332        setRegex();
333      }
334    });
335    
336    JButton test = new JButton("Test");
337    test.setMnemonic(KeyEvent.VK_T);
338    test.setToolTipText("Exercises verifier by simply receiving focus");
339    test.addActionListener( new ActionListener() {
340      public void actionPerformed(ActionEvent event) {
341        //do nothing
342        //exists only as a target for new focus, such that 
343        //the focus tries to move out of the input field
344      }
345    });
346    
347    JButton close = new JButton("Close");
348    close.setMnemonic(KeyEvent.VK_C);
349    close.addActionListener( new ActionListener() {
350      public void actionPerformed(ActionEvent event) {
351        fFrame.dispose();
352      }
353    });
354    
355    java.util.List<JComponent> buttons = new ArrayList<>();
356    buttons.add(setRegex) ;
357    buttons.add(test);
358    buttons.add(close);
359    
360    return UiUtil.getCommandRow( buttons );
361  }
362  
363  private static void setRegex(){
364    Pattern regex = Pattern.compile(fRegexField.getText());
365    RegexInputVerifier verifier = new RegexInputVerifier(regex, UseToolTip.FALSE);
366    fInputField.setInputVerifier(verifier);
367  }
368}