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: " (input data) " 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}