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: }