RegExLab.java

Download RegExLab.java

  1: // Regular expression lab.  Allows user to enter REs and match them against
  2: // text from any selected file.  This GUI application has a swing
  3: // user interface showing buttons, (split) scroll panes, file choosers,
  4: // checkboxes, Mnemonic (shortcut) keys, and sophisticated layout.
  5: //
  6: // TODO: Add (Regular Expression) help, and possibly dropdown list
  7: //       of previously used REs.
  8: //
  9: // Written 6/2004 by Wayne Pollock, Tampa Florida USA.
 10: // Updated 9/2015 by WP: Had bug (won't match first character after Reset);
 11: //                       Updated for modern Java (Generics).
 12: 
 13: package com.wpollock.regexp;
 14: 
 15: import java.awt.*;
 16: import java.awt.event.*;
 17: import java.io.*;
 18: import java.util.*;
 19: import java.util.regex.*;
 20: import javax.imageio.ImageIO;
 21: import javax.swing.*;
 22: import javax.swing.border.*;
 23: import javax.swing.event.*;
 24: 
 25: public class RegExLab extends JFrame
 26: {
 27: 	private static final long serialVersionUID = 1L;
 28:    private static final String DEFAULT_TEXT = "Data.txt";
 29: // State Information:
 30:    private String text;  // Contents of selected file.
 31:    private boolean regExpChanged = true; // Forces RE to re-compile if true.
 32:    private boolean textChanged = true;  // Resets matcher position to 0 if true.
 33:    private Pattern refExpPattern = null;  // The compiled RE.
 34:    private Matcher regExp = null; // The current state (position) of the RE.
 35:    private Container container;
 36:    private java.util.List<JCheckBox> checkboxOptions;  // List of option checkboxes.
 37: 
 38:    // Visual Components:
 39:    private JFileChooser chooser;
 40:    private JTextField regExTF;    // This is where the user types the RE.
 41:    private JButton clearBtn;      // Clears regExTF.
 42:    private JButton nextMatchBtn;  // highlight next match.  (Default btn).
 43:    private JButton resetBtn;      // Reset the matcher to the start of the text.
 44:    private JButton selectFileBtn; // Read in new text from file.
 45:    private JButton helpBtn;       // Reg.Exp. brief help.
 46:    private JLabel textPaneTitle;  // Holds the file name of the selected text.
 47:    private JTextArea textPane;    // Holds the text of the selected file.
 48:    private JScrollPane textScrollPane;
 49:    private JTextArea captureGroupPane; // Displays RE "capture groups", if any.
 50:    private JScrollPane captureGroupScrollPane;
 51:    private JSplitPane splitPane;  // Holds the text and capture group panes.
 52:    private JCheckBox caseInsensitiveOption, multilineOption, dotallOption,
 53:       unicodeCaseOption, canonEqOption, unixLineOption, literalOption,
 54:       unicodeCCOption, commentsOption;
 55: 
 56:    public static void main ( String [] args )
 57:    {
 58:       JFrame frame = new RegExLab();
 59:       frame.pack();  // Size the frame to just hold everything.
 60: 
 61:       // Center the Frame on the screen:
 62:       frame.setLocationRelativeTo(null);
 63:       frame.setVisible( true );
 64:    }
 65: 
 66:    public RegExLab ( )
 67:    {
 68:       super( "Regular Expression Lab" );
 69: 
 70:       // Create components and set their properties:
 71:       text = initializeText();
 72: 
 73:       nextMatchBtn = new JButton( "Show Next Match" );
 74:         nextMatchBtn.setMnemonic( KeyEvent.VK_N );
 75: 
 76:       resetBtn = new JButton( "Reset" );
 77:         resetBtn.setMnemonic( KeyEvent.VK_R );
 78: 
 79:       selectFileBtn = new JButton( "Select File..." );
 80:         selectFileBtn.setMnemonic( KeyEvent.VK_S );
 81: 
 82:       helpBtn = new JButton( "Help..." );
 83:         helpBtn.setMnemonic( KeyEvent.VK_H );
 84:         helpBtn.setToolTipText( "Show Regular Expression Summary" );
 85:         helpBtn.setEnabled( false );  // Need permission before adding this!
 86: 
 87:       checkboxOptions = new ArrayList<>();
 88:       caseInsensitiveOption = new JCheckBox( "Case Insensitive" );
 89:         caseInsensitiveOption.setActionCommand(
 90:             Integer.toString( Pattern.CASE_INSENSITIVE ) );
 91:         caseInsensitiveOption.setMnemonic( KeyEvent.VK_I );
 92:         caseInsensitiveOption.setToolTipText(
 93:                   "Enables ASCII case-insensitive matching" );
 94:         checkboxOptions.add( caseInsensitiveOption );
 95:       multilineOption = new JCheckBox( "Multi-line" );
 96:         multilineOption.setActionCommand( Integer.toString(Pattern.MULTILINE) );
 97:         multilineOption.setMnemonic( KeyEvent.VK_M );
 98:         multilineOption.setToolTipText( "Enables multi-line mode "
 99:            + "('^' and '$' match after/before line terminators, "
100:            + "rather than the beginning and end of the whole text)" );
101:         checkboxOptions.add( multilineOption );
102:       dotallOption = new JCheckBox( "Dot All" );
103:         dotallOption.setActionCommand( Integer.toString(Pattern.DOTALL) );
104:         dotallOption.setMnemonic( KeyEvent.VK_D );
105:         dotallOption.setToolTipText( "Enables Dot All mode "
106:            + "(so '.' matches line terminators too)" );
107:         checkboxOptions.add( dotallOption );
108:       unicodeCaseOption = new JCheckBox( "Unicode Case" );
109:         unicodeCaseOption.setActionCommand(
110:             Integer.toString( Pattern.UNICODE_CASE ) );
111:         unicodeCaseOption.setMnemonic( KeyEvent.VK_U );
112:         unicodeCaseOption.setToolTipText( "Enables Unicode-aware case-insensitive matching");
113:         checkboxOptions.add( unicodeCaseOption );
114:       canonEqOption = new JCheckBox( "Canon Eq" );
115:         canonEqOption.setActionCommand( Integer.toString(Pattern.CANON_EQ) );
116:         canonEqOption.setMnemonic( KeyEvent.VK_E );
117:         canonEqOption.setToolTipText( "Enables canonical equivalence "
118:            + "(when different Unicode sequences of characters represent the "
119:            + "same text)" );
120:         checkboxOptions.add( canonEqOption );
121:       unixLineOption = new JCheckBox( "Unix Line" );
122:         unixLineOption.setActionCommand( Integer.toString(Pattern.UNIX_LINES) );
123:         unixLineOption.setMnemonic( KeyEvent.VK_X );
124:         unixLineOption.setToolTipText( "Enable Unix Line "
125:             + "(\\n only as end of line) Mode" );
126:         checkboxOptions.add( unixLineOption );
127:       literalOption = new JCheckBox( "Literal" );
128:         literalOption.setActionCommand( Integer.toString(Pattern.LITERAL) );
129:         literalOption.setMnemonic( KeyEvent.VK_L );
130:         literalOption.setToolTipText( "Enable Literal Mode (Treat all "
131:            + "characters in the Reg Exp literally, including '.', '*', etc.)" );
132:         checkboxOptions.add( literalOption );
133:       unicodeCCOption = new JCheckBox( "Unicode Character Class" );
134:         unicodeCCOption.setActionCommand( Integer.toString(Pattern.UNICODE_CHARACTER_CLASS) );
135:         unicodeCCOption.setMnemonic( KeyEvent.VK_O );
136:         unicodeCCOption.setToolTipText( "Enable conforming Unicode Character classes Mode and Unicode Case mode" );
137:         checkboxOptions.add( unicodeCCOption );
138:       commentsOption = new JCheckBox( "Comments" );
139:         commentsOption.setActionCommand( Integer.toString(Pattern.COMMENTS) );
140:         commentsOption.setMnemonic( KeyEvent.VK_C );
141:         commentsOption.setToolTipText( "Allow comments and white-space in RE" );
142:         checkboxOptions.add( commentsOption );
143: 
144:       regExTF = new JTextField( 35 );  // width will adjust if frame is resized.
145:         regExTF.setFont( new Font( "Monospaced", Font.BOLD, 16 ) );
146: 
147:       clearBtn = new JButton();
148:         clearBtn.setMnemonic( KeyEvent.VK_R );
149:         Image clearIcon = null;
150:         try {
151:            InputStream is = getClass().getResourceAsStream("clear-rtl-icon.png");
152:            clearIcon = ImageIO.read( is );
153:         } catch ( IOException ioe ) {
154:            clearIcon = null;
155:         }
156: 
157:         if ( clearIcon != null ) {
158:             clearIcon =
159:                clearIcon.getScaledInstance(16, 16, java.awt.Image.SCALE_SMOOTH);
160:             clearBtn.setIcon( new ImageIcon(clearIcon) );
161:             clearBtn.setBorderPainted( false );
162:             clearBtn.setContentAreaFilled( false );
163:         } else {
164:             clearBtn.setText( "Clear" );
165:         }
166: 
167:       Box tfWithBtn = Box.createHorizontalBox();
168:         tfWithBtn.add( regExTF );
169:         tfWithBtn.add( Box.createGlue() );
170:         tfWithBtn.add( clearBtn );
171:         tfWithBtn.setBackground( regExTF.getBackground() );
172:         tfWithBtn.setBorder( regExTF.getBorder() );
173:         regExTF.setBorder(null);
174:         tfWithBtn.setBorder( new CompoundBorder(
175:            new EmptyBorder(5, 5, 5, 5), new EtchedBorder() ) );
176: 
177:       textPaneTitle = new JLabel( "Select a file to change the search text:",
178:                                   JLabel.CENTER );
179:         textPaneTitle.setFont( new Font( "SansSeriff", Font.BOLD, 18 ) );
180:         textPaneTitle.setOpaque( true );  // So background color shows.
181:         textPaneTitle.setForeground( Color.BLUE );
182:         textPaneTitle.setBackground( Color.CYAN );
183: 
184:       textPane = new JTextArea( 12, 50 );
185:         textPane.setFont( new Font( "Monospaced", Font.BOLD, 16 ) );
186:         textPane.setBackground( Color.CYAN );
187:         textPane.setSelectionColor( Color.YELLOW );
188:         textPane.setMargin( new Insets(5,5,5,5) );  // Adds some padding.
189:         textPane.setTabSize( 4 );
190:         textPane.setEditable( false );
191:         textPane.setFocusable( false );   // So tab key focus cycle skips this.
192:         textPane.setText( text );
193:         textPane.getCaret().setBlinkRate( 0 );  // Set cursor to non-blinking.
194:         textPane.getCaret().setVisible( true );
195: 
196:       textScrollPane = new JScrollPane( textPane );
197:         textScrollPane.setBackground( Color.CYAN );
198:         textScrollPane.setColumnHeaderView( textPaneTitle );
199: 
200:       captureGroupPane = new JTextArea( 6, 50 );
201:         captureGroupPane.setFont( new Font( "Monospaced", Font.BOLD, 16 ) );
202:         captureGroupPane.setBackground( Color.CYAN );
203:         captureGroupPane.setMargin( new Insets(5,5,5,5) );
204:         captureGroupPane.setEditable( false );
205:         captureGroupPane.setFocusable( false );
206:         captureGroupPane.getCaret().setVisible( false );  // Always hide cursor.
207: 
208:       captureGroupScrollPane = new JScrollPane( captureGroupPane );
209:         JLabel capturePaneTitle = new JLabel( "Capture Groups from last match:",
210:              JLabel.CENTER );
211:           capturePaneTitle.setFont( new Font( "SansSeriff", Font.BOLD, 18 ) );
212:           capturePaneTitle.setOpaque( true );  // So background color shows.
213:           capturePaneTitle.setForeground( Color.BLUE );
214:           capturePaneTitle.setBackground( Color.CYAN );
215:         captureGroupScrollPane.setBackground( Color.CYAN );
216:         captureGroupScrollPane.setColumnHeaderView( capturePaneTitle );
217: 
218:       chooser = new JFileChooser();
219:         chooser.setDialogTitle( "Select text file:" );
220:         TextFilter tf = new TextFilter();  // This is a nested class, below.
221:         chooser.addChoosableFileFilter( tf );
222:         chooser.setFileFilter( tf );  // Make text filter the default.
223: 
224:       // Layout the components:
225: 
226:       container = getContentPane();
227:       container.setLayout( new BorderLayout() );
228: 
229:       JPanel top = new JPanel( new BorderLayout() );
230: 
231:         JPanel row1 = new JPanel( new BorderLayout() );
232:           row1.add( new JLabel(" Enter Regular Expression: " ),
233:                     BorderLayout.WEST );
234:           row1.add( tfWithBtn, BorderLayout.CENTER );
235:           JPanel btnPanel = new JPanel();
236:             btnPanel.add( nextMatchBtn );
237:             btnPanel.add( resetBtn );
238:             btnPanel.add( selectFileBtn );
239:           row1.add( btnPanel, BorderLayout.EAST );
240:         top.add( row1, BorderLayout.NORTH );
241: 
242:         JPanel row2 = new JPanel( new BorderLayout() );
243:           row2.setBorder( new EmptyBorder( 0, 5, 2, 5 ) );  // tweak spacing
244:           Box optionsPanel = Box.createHorizontalBox();
245:             optionsPanel.add( new JLabel( "Options: " ) );
246:             optionsPanel.add( Box.createGlue() );
247:             for ( Iterator<JCheckBox> it
248:                 = checkboxOptions.iterator(); it.hasNext(); )
249:             {  optionsPanel.add( it.next() );
250:                optionsPanel.add( Box.createGlue() );
251:             }
252:             optionsPanel.add( Box.createGlue() );
253:           row2.add( optionsPanel, BorderLayout.CENTER );
254:           row2.add( helpBtn, BorderLayout.EAST );
255:         top.add( row2, BorderLayout.SOUTH );
256: 
257:       container.add( top, BorderLayout.NORTH );
258: 
259:       splitPane = new JSplitPane( JSplitPane.VERTICAL_SPLIT, true,
260:                                   textScrollPane, captureGroupScrollPane );
261:       splitPane.setOneTouchExpandable( true );
262:       container.add( splitPane, BorderLayout.CENTER );
263: 
264:       // Hook up event handling:
265: 
266:       setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
267: 
268:       // Hitting enter activates the default button unless otherwise handled:
269:       getRootPane().setDefaultButton( nextMatchBtn );
270: 
271:       // What to do when user changes the regular expression in regExTF:
272:       // (Note: by default, hitting enter causes the default button to fire.)
273:       regExTF.getDocument().addDocumentListener( new DocumentListener()
274:          {  public void insertUpdate  ( DocumentEvent ignored )
275:             {  regExpChanged = true; }
276:             public void removeUpdate  ( DocumentEvent ignored )
277:             {  regExpChanged = true; }
278:             public void changedUpdate ( DocumentEvent ignored ) { }
279:          }
280:       );
281: 
282:       // The Clear icon button clears the entered regular expression:
283:       clearBtn.addActionListener( new ActionListener()
284:          {  public void actionPerformed ( ActionEvent ae )
285:             {  regExTF.setText( null );
286:                regExTF.requestFocus();
287:             }
288:          }
289:       );
290: 
291:       // When the "select file" btn is clicked, let user chose a new text file,
292:       // and update the displayed text (and reset the RE) if successful:
293:       selectFileBtn.addActionListener( new ActionListener()
294:          {  public void actionPerformed ( ActionEvent ae )
295:             {  readFile();
296:                regExTF.requestFocus();
297:             }
298:          }
299:       );
300: 
301:       // When clicked, find and highlight the next match in the text.
302:       // Show dialog if no more matches:
303:       nextMatchBtn.addActionListener( new ActionListener()
304:          {  public void actionPerformed ( ActionEvent e )
305:             {  showNextMatch();
306:                regExTF.requestFocus();
307:             }
308:          }
309:       );
310: 
311:       // When clicked set the match position to zero and show cursor:
312:       resetBtn.addActionListener( new ActionListener()
313:          {  public void actionPerformed ( ActionEvent e )
314:             {  if ( regExp == null )
315:                   return;  // Nothing to reset yet.
316:                regExp.reset();
317:                textPane.setCaretPosition( 0 );
318:                textPane.getCaret().setVisible( true );
319:                textPane.getCaret().setSelectionVisible( true );
320:                regExTF.requestFocus();
321:             }
322:          }
323:       );
324: 
325:       // When clicked show the cursor (and hide previous selection):
326:       textPane.addMouseListener ( new MouseAdapter ()
327:          {  public void mousePressed ( MouseEvent me )
328:             {  textPane.getCaret().setVisible( true );
329:                textPane.getCaret().setSelectionVisible( true );
330:             }
331:          }
332:       );
333: 
334:       // When any option checkbox changes state, the regular expression
335:       // must be re-compiled with the new flags:
336:       ItemListener optionChangeListener = new ItemListener()
337:       {  public void itemStateChanged( ItemEvent ie )
338:          {  regExpChanged = true;
339:             regExTF.requestFocus();
340:          }
341:       };
342:       for ( Iterator<JCheckBox> it = checkboxOptions.iterator(); it.hasNext(); )
343:          it.next().addItemListener( optionChangeListener );
344:    }
345: 
346:    // Read in initial search text from "data.txt" file in jar:
347:    // TODO: Update to use NIO.
348:    private String initializeText ( )
349:    {
350:       StringBuffer sb = new StringBuffer( 1024 );
351:       try
352:       {  BufferedReader in = new BufferedReader( new InputStreamReader(
353:            getClass().getResourceAsStream( DEFAULT_TEXT ) ) );
354:          for ( String line = in.readLine(); line != null; line = in.readLine() )
355:          {  sb.append( line );
356:             sb.append( "\n" );
357:          }
358:          in.close();
359:          text = sb.toString();
360:       }
361:       catch ( Exception e )
362:       {  sb.setLength( 0 );
363:          sb.append( "Select a text file to change this search text.\n" );
364:       }
365:       return sb.toString();
366:    }
367: 
368:    private void readFile ()
369:    {
370:       // Show a file dialog box and have the user select a data file:
371:       int returnVal = chooser.showOpenDialog( null );
372:       if ( returnVal != JFileChooser.APPROVE_OPTION )
373:          return;  // User canceled operation
374: 
375:       // Read in the selected file's text:
376:       File file = chooser.getSelectedFile();
377:       try { textPane.read( new FileReader( file ), null );  }
378:       catch ( Exception ignored )
379:       {  JOptionPane.showMessageDialog( null, "Could not read from file \""
380:             + file.getName() + "\"", "Error!", JOptionPane.ERROR_MESSAGE );
381:          return;
382:       }
383: 
384:       text = textPane.getText();
385:       textChanged = true;
386:       textPane.setCaretPosition( 0 );
387:       textPane.getCaret().setVisible( true );
388:       textPane.getCaret().setSelectionVisible( true );
389:       textPaneTitle.setText( file.getName() );
390:    }
391: 
392: 
393:    // Highlight the next match (if any).  Note if the RE only changed
394:    // the Matcher must be regenerated, but the next match should be after
395:    // the current  position.  If the text changed the Matcher must be
396:    // reset to position 0 of the new text.
397:    // After a reset, show the caret position only (not the last match).
398: 
399:    private void showNextMatch ()
400:    {
401:       // Start the next search after the start of the last, to allow
402:       // overlapping matches.  If pos == 0 and nothing is selected, then
403:       // there was no previous match, so don't change pos from zero:
404:       int pos = textPane.getSelectionStart();
405:       if ( pos != 0 || textPane.getSelectionEnd() != 0 ) {
406:          ++pos;
407:       }
408: 
409:       if ( regExpChanged )
410:       {
411:          String re = regExTF.getText();
412:          if ( re == null || re.length() == 0 )
413:          {  JOptionPane.showMessageDialog( null, "No Regular Expression!",
414:               "Illegal Regular Expression", JOptionPane.ERROR_MESSAGE );
415:             return;
416:          }
417: 
418:          // This code adds the value of the action command of each selected
419:          // option checkbox, which was earlier set to the corresponding
420:          // integer constant from the Pattern class:
421:          int options = 0;
422:          for ( Iterator<JCheckBox> it
423:             = checkboxOptions.iterator(); it.hasNext(); )
424:          {  JCheckBox cb = it.next();
425:             if ( cb.isSelected() )
426:                options += Integer.parseInt( cb.getActionCommand() );
427:          }
428: 
429:          try
430:          {  refExpPattern = Pattern.compile( re, options );
431:          }
432:          catch ( PatternSyntaxException pse )
433:          {  JOptionPane.showMessageDialog( null, pse.getMessage(),
434:               "Illegal Regular Expression", JOptionPane.ERROR_MESSAGE );
435:             return;
436:          }
437:       }
438: 
439:       if ( regExpChanged || textChanged )
440:          regExp = refExpPattern.matcher( text );
441: 
442:       if ( textChanged )
443:       {  pos = 0;
444:          textPane.getCaret().setVisible( true );
445:          textPane.getCaret().setSelectionVisible( false );
446:       }
447: 
448:       regExpChanged = textChanged = false;
449: 
450:       if ( regExp.find( pos ) )
451:       {  // Highlight the match:
452:          textPane.setCaretPosition( regExp.start() );
453:          textPane.moveCaretPosition( regExp.end() );
454:          textScrollPane.validate();
455:          textPane.getCaret().setVisible( false );
456:          textPane.getCaret().setSelectionVisible( true );
457: 
458:          //Add any capture groups to capture group pane:
459:          captureGroupPane.setText( "" );
460:          int numGroups = regExp.groupCount();
461:          for ( int i = 1; i <= numGroups; ++i )
462:             captureGroupPane.append( i + ": " + regExp.group( i ) + "\n" );
463:       }
464:       else
465:       {  JOptionPane.showMessageDialog( null, "No matches remaining!" );
466:       }
467:    }
468: 
469: 
470:    private static class TextFilter extends javax.swing.filechooser.FileFilter
471:    {
472:       public final static String textExtension = "txt";
473: 
474:       public static String getExtension ( File f )
475:       {
476:          String ext = null;
477:          String s = f.getName();
478:          int i = s.lastIndexOf( '.' );
479: 
480:          if ( i > 0 &&  i < s.length() - 1 ) // Make sure dot isn't last char.
481:             ext = s.substring( i+1 ) . toLowerCase();
482:          return ext;
483:       }
484: 
485:       // Accept all directories and text files:
486:       public boolean accept ( File file )
487:       {
488:          if ( file.isDirectory() )
489:             return true;
490: 
491:          String extension = getExtension( file );
492:          if ( extension != null && extension.equals( textExtension ) )
493:                return true;
494:          return false;
495:       }
496: 
497:       // Return the description of this filter:
498:       public String getDescription ( )
499:       {
500:          return "Text Documents";
501:       }
502:    }
503:  }