/home/wpollock1/public_html/Java/RegExLab.java

// Regular expression lab.  Allows user to enter REs and match them against
// text from any selected file.  This GUI application has a swing
// user interface showing buttons, (split) scroll panes, file choosers,
// checkboxes, Mnemonic (shortcut) keys, and sophisticated layout.
//
// TODO: Add (Regular Expression) help, and possibly dropdown list
//       of previously used REs.
//
// Written 6/2004 by Wayne Pollock, Tampa Florida USA.
// Updated 9/2015 by WP: Had bug (won't match first character after Reset);
//                       Updated for modern Java (Generics).

package com.wpollock.regexp;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.util.*;
import java.util.regex.*;
import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.event.*;

public class RegExLab extends JFrame
{
	private static final long serialVersionUID = 1L;
   private static final String DEFAULT_TEXT = "Data.txt";
// State Information:
   private String text;  // Contents of selected file.
   private boolean regExpChanged = true; // Forces RE to re-compile if true.
   private boolean textChanged = true;  // Resets matcher position to 0 if true.
   private Pattern refExpPattern = null;  // The compiled RE.
   private Matcher regExp = null; // The current state (position) of the RE.
   private Container container;
   private java.util.List<JCheckBox> checkboxOptions;  // List of option checkboxes.

   // Visual Components:
   private JFileChooser chooser;
   private JTextField regExTF;    // This is where the user types the RE.
   private JButton clearBtn;      // Clears regExTF.
   private JButton nextMatchBtn;  // highlight next match.  (Default btn).
   private JButton resetBtn;      // Reset the matcher to the start of the text.
   private JButton selectFileBtn; // Read in new text from file.
   private JButton helpBtn;       // Reg.Exp. brief help.
   private JLabel textPaneTitle;  // Holds the file name of the selected text.
   private JTextArea textPane;    // Holds the text of the selected file.
   private JScrollPane textScrollPane;
   private JTextArea captureGroupPane; // Displays RE "capture groups", if any.
   private JScrollPane captureGroupScrollPane;
   private JSplitPane splitPane;  // Holds the text and capture group panes.
   private JCheckBox caseInsensitiveOption, multilineOption, dotallOption,
      unicodeCaseOption, canonEqOption, unixLineOption, literalOption,
      unicodeCCOption, commentsOption;

   public static void main ( String [] args )
   {
      JFrame frame = new RegExLab();
      frame.pack();  // Size the frame to just hold everything.

      // Center the Frame on the screen:
      frame.setLocationRelativeTo(null);
      frame.setVisible( true );
   }

   public RegExLab ( )
   {
      super( "Regular Expression Lab" );

      // Create components and set their properties:
      text = initializeText();

      nextMatchBtn = new JButton( "Show Next Match" );
        nextMatchBtn.setMnemonic( KeyEvent.VK_N );

      resetBtn = new JButton( "Reset" );
        resetBtn.setMnemonic( KeyEvent.VK_R );

      selectFileBtn = new JButton( "Select File..." );
        selectFileBtn.setMnemonic( KeyEvent.VK_S );

      helpBtn = new JButton( "Help..." );
        helpBtn.setMnemonic( KeyEvent.VK_H );
        helpBtn.setToolTipText( "Show Regular Expression Summary" );
        helpBtn.setEnabled( false );  // Need permission before adding this!

      checkboxOptions = new ArrayList<>();
      caseInsensitiveOption = new JCheckBox( "Case Insensitive" );
        caseInsensitiveOption.setActionCommand(
            Integer.toString( Pattern.CASE_INSENSITIVE ) );
        caseInsensitiveOption.setMnemonic( KeyEvent.VK_I );
        caseInsensitiveOption.setToolTipText(
                  "Enables ASCII case-insensitive matching" );
        checkboxOptions.add( caseInsensitiveOption );
      multilineOption = new JCheckBox( "Multi-line" );
        multilineOption.setActionCommand( Integer.toString(Pattern.MULTILINE) );
        multilineOption.setMnemonic( KeyEvent.VK_M );
        multilineOption.setToolTipText( "Enables multi-line mode "
           + "('^' and '$' match after/before line terminators, "
           + "rather than the beginning and end of the whole text)" );
        checkboxOptions.add( multilineOption );
      dotallOption = new JCheckBox( "Dot All" );
        dotallOption.setActionCommand( Integer.toString(Pattern.DOTALL) );
        dotallOption.setMnemonic( KeyEvent.VK_D );
        dotallOption.setToolTipText( "Enables Dot All mode "
           + "(so '.' matches line terminators too)" );
        checkboxOptions.add( dotallOption );
      unicodeCaseOption = new JCheckBox( "Unicode Case" );
        unicodeCaseOption.setActionCommand(
            Integer.toString( Pattern.UNICODE_CASE ) );
        unicodeCaseOption.setMnemonic( KeyEvent.VK_U );
        unicodeCaseOption.setToolTipText( "Enables Unicode-aware case-insensitive matching");
        checkboxOptions.add( unicodeCaseOption );
      canonEqOption = new JCheckBox( "Canon Eq" );
        canonEqOption.setActionCommand( Integer.toString(Pattern.CANON_EQ) );
        canonEqOption.setMnemonic( KeyEvent.VK_E );
        canonEqOption.setToolTipText( "Enables canonical equivalence "
           + "(when different Unicode sequences of characters represent the "
           + "same text)" );
        checkboxOptions.add( canonEqOption );
      unixLineOption = new JCheckBox( "Unix Line" );
        unixLineOption.setActionCommand( Integer.toString(Pattern.UNIX_LINES) );
        unixLineOption.setMnemonic( KeyEvent.VK_X );
        unixLineOption.setToolTipText( "Enable Unix Line "
            + "(\\n only as end of line) Mode" );
        checkboxOptions.add( unixLineOption );
      literalOption = new JCheckBox( "Literal" );
        literalOption.setActionCommand( Integer.toString(Pattern.LITERAL) );
        literalOption.setMnemonic( KeyEvent.VK_L );
        literalOption.setToolTipText( "Enable Literal Mode (Treat all "
           + "characters in the Reg Exp literally, including '.', '*', etc.)" );
        checkboxOptions.add( literalOption );
      unicodeCCOption = new JCheckBox( "Unicode Character Class" );
        unicodeCCOption.setActionCommand( Integer.toString(Pattern.UNICODE_CHARACTER_CLASS) );
        unicodeCCOption.setMnemonic( KeyEvent.VK_O );
        unicodeCCOption.setToolTipText( "Enable conforming Unicode Character classes Mode and Unicode Case mode" );
        checkboxOptions.add( unicodeCCOption );
      commentsOption = new JCheckBox( "Comments" );
        commentsOption.setActionCommand( Integer.toString(Pattern.COMMENTS) );
        commentsOption.setMnemonic( KeyEvent.VK_C );
        commentsOption.setToolTipText( "Allow comments and white-space in RE" );
        checkboxOptions.add( commentsOption );

      regExTF = new JTextField( 35 );  // width will adjust if frame is resized.
        regExTF.setFont( new Font( "Monospaced", Font.BOLD, 16 ) );

      clearBtn = new JButton();
        clearBtn.setMnemonic( KeyEvent.VK_R );
        Image clearIcon = null;
        try {
           InputStream is = getClass().getResourceAsStream("clear-rtl-icon.png");
           clearIcon = ImageIO.read( is );
        } catch ( IOException ioe ) {
           clearIcon = null;
        }

        if ( clearIcon != null ) {
            clearIcon =
               clearIcon.getScaledInstance(16, 16, java.awt.Image.SCALE_SMOOTH);
            clearBtn.setIcon( new ImageIcon(clearIcon) );
            clearBtn.setBorderPainted( false );
            clearBtn.setContentAreaFilled( false );
        } else {
            clearBtn.setText( "Clear" );
        }

      Box tfWithBtn = Box.createHorizontalBox();
        tfWithBtn.add( regExTF );
        tfWithBtn.add( Box.createGlue() );
        tfWithBtn.add( clearBtn );
        tfWithBtn.setBackground( regExTF.getBackground() );
        tfWithBtn.setBorder( regExTF.getBorder() );
        regExTF.setBorder(null);
        tfWithBtn.setBorder( new CompoundBorder(
           new EmptyBorder(5, 5, 5, 5), new EtchedBorder() ) );

      textPaneTitle = new JLabel( "Select a file to change the search text:",
                                  JLabel.CENTER );
        textPaneTitle.setFont( new Font( "SansSeriff", Font.BOLD, 18 ) );
        textPaneTitle.setOpaque( true );  // So background color shows.
        textPaneTitle.setForeground( Color.BLUE );
        textPaneTitle.setBackground( Color.CYAN );

      textPane = new JTextArea( 12, 50 );
        textPane.setFont( new Font( "Monospaced", Font.BOLD, 16 ) );
        textPane.setBackground( Color.CYAN );
        textPane.setSelectionColor( Color.YELLOW );
        textPane.setMargin( new Insets(5,5,5,5) );  // Adds some padding.
        textPane.setTabSize( 4 );
        textPane.setEditable( false );
        textPane.setFocusable( false );   // So tab key focus cycle skips this.
        textPane.setText( text );
        textPane.getCaret().setBlinkRate( 0 );  // Set cursor to non-blinking.
        textPane.getCaret().setVisible( true );

      textScrollPane = new JScrollPane( textPane );
        textScrollPane.setBackground( Color.CYAN );
        textScrollPane.setColumnHeaderView( textPaneTitle );

      captureGroupPane = new JTextArea( 6, 50 );
        captureGroupPane.setFont( new Font( "Monospaced", Font.BOLD, 16 ) );
        captureGroupPane.setBackground( Color.CYAN );
        captureGroupPane.setMargin( new Insets(5,5,5,5) );
        captureGroupPane.setEditable( false );
        captureGroupPane.setFocusable( false );
        captureGroupPane.getCaret().setVisible( false );  // Always hide cursor.

      captureGroupScrollPane = new JScrollPane( captureGroupPane );
        JLabel capturePaneTitle = new JLabel( "Capture Groups from last match:",
             JLabel.CENTER );
          capturePaneTitle.setFont( new Font( "SansSeriff", Font.BOLD, 18 ) );
          capturePaneTitle.setOpaque( true );  // So background color shows.
          capturePaneTitle.setForeground( Color.BLUE );
          capturePaneTitle.setBackground( Color.CYAN );
        captureGroupScrollPane.setBackground( Color.CYAN );
        captureGroupScrollPane.setColumnHeaderView( capturePaneTitle );

      chooser = new JFileChooser();
        chooser.setDialogTitle( "Select text file:" );
        TextFilter tf = new TextFilter();  // This is a nested class, below.
        chooser.addChoosableFileFilter( tf );
        chooser.setFileFilter( tf );  // Make text filter the default.

      // Layout the components:

      container = getContentPane();
      container.setLayout( new BorderLayout() );

      JPanel top = new JPanel( new BorderLayout() );

        JPanel row1 = new JPanel( new BorderLayout() );
          row1.add( new JLabel(" Enter Regular Expression: " ),
                    BorderLayout.WEST );
          row1.add( tfWithBtn, BorderLayout.CENTER );
          JPanel btnPanel = new JPanel();
            btnPanel.add( nextMatchBtn );
            btnPanel.add( resetBtn );
            btnPanel.add( selectFileBtn );
          row1.add( btnPanel, BorderLayout.EAST );
        top.add( row1, BorderLayout.NORTH );

        JPanel row2 = new JPanel( new BorderLayout() );
          row2.setBorder( new EmptyBorder( 0, 5, 2, 5 ) );  // tweak spacing
          Box optionsPanel = Box.createHorizontalBox();
            optionsPanel.add( new JLabel( "Options: " ) );
            optionsPanel.add( Box.createGlue() );
            for ( Iterator<JCheckBox> it
                = checkboxOptions.iterator(); it.hasNext(); )
            {  optionsPanel.add( it.next() );
               optionsPanel.add( Box.createGlue() );
            }
            optionsPanel.add( Box.createGlue() );
          row2.add( optionsPanel, BorderLayout.CENTER );
          row2.add( helpBtn, BorderLayout.EAST );
        top.add( row2, BorderLayout.SOUTH );

      container.add( top, BorderLayout.NORTH );

      splitPane = new JSplitPane( JSplitPane.VERTICAL_SPLIT, true,
                                  textScrollPane, captureGroupScrollPane );
      splitPane.setOneTouchExpandable( true );
      container.add( splitPane, BorderLayout.CENTER );

      // Hook up event handling:

      setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

      // Hitting enter activates the default button unless otherwise handled:
      getRootPane().setDefaultButton( nextMatchBtn );

      // What to do when user changes the regular expression in regExTF:
      // (Note: by default, hitting enter causes the default button to fire.)
      regExTF.getDocument().addDocumentListener( new DocumentListener()
         {  public void insertUpdate  ( DocumentEvent ignored )
            {  regExpChanged = true; }
            public void removeUpdate  ( DocumentEvent ignored )
            {  regExpChanged = true; }
            public void changedUpdate ( DocumentEvent ignored ) { }
         }
      );

      // The Clear icon button clears the entered regular expression:
      clearBtn.addActionListener( new ActionListener()
         {  public void actionPerformed ( ActionEvent ae )
            {  regExTF.setText( null );
               regExTF.requestFocus();
            }
         }
      );

      // When the "select file" btn is clicked, let user chose a new text file,
      // and update the displayed text (and reset the RE) if successful:
      selectFileBtn.addActionListener( new ActionListener()
         {  public void actionPerformed ( ActionEvent ae )
            {  readFile();
               regExTF.requestFocus();
            }
         }
      );

      // When clicked, find and highlight the next match in the text.
      // Show dialog if no more matches:
      nextMatchBtn.addActionListener( new ActionListener()
         {  public void actionPerformed ( ActionEvent e )
            {  showNextMatch();
               regExTF.requestFocus();
            }
         }
      );

      // When clicked set the match position to zero and show cursor:
      resetBtn.addActionListener( new ActionListener()
         {  public void actionPerformed ( ActionEvent e )
            {  if ( regExp == null )
                  return;  // Nothing to reset yet.
               regExp.reset();
               textPane.setCaretPosition( 0 );
               textPane.getCaret().setVisible( true );
               textPane.getCaret().setSelectionVisible( true );
               regExTF.requestFocus();
            }
         }
      );

      // When clicked show the cursor (and hide previous selection):
      textPane.addMouseListener ( new MouseAdapter ()
         {  public void mousePressed ( MouseEvent me )
            {  textPane.getCaret().setVisible( true );
               textPane.getCaret().setSelectionVisible( true );
            }
         }
      );

      // When any option checkbox changes state, the regular expression
      // must be re-compiled with the new flags:
      ItemListener optionChangeListener = new ItemListener()
      {  public void itemStateChanged( ItemEvent ie )
         {  regExpChanged = true;
            regExTF.requestFocus();
         }
      };
      for ( Iterator<JCheckBox> it = checkboxOptions.iterator(); it.hasNext(); )
         it.next().addItemListener( optionChangeListener );
   }

   // Read in initial search text from "data.txt" file in jar:
   // TODO: Update to use NIO.
   private String initializeText ( )
   {
      StringBuffer sb = new StringBuffer( 1024 );
      try
      {  BufferedReader in = new BufferedReader( new InputStreamReader(
           getClass().getResourceAsStream( DEFAULT_TEXT ) ) );
         for ( String line = in.readLine(); line != null; line = in.readLine() )
         {  sb.append( line );
            sb.append( "\n" );
         }
         in.close();
         text = sb.toString();
      }
      catch ( Exception e )
      {  sb.setLength( 0 );
         sb.append( "Select a text file to change this search text.\n" );
      }
      return sb.toString();
   }

   private void readFile ()
   {
      // Show a file dialog box and have the user select a data file:
      int returnVal = chooser.showOpenDialog( null );
      if ( returnVal != JFileChooser.APPROVE_OPTION )
         return;  // User canceled operation

      // Read in the selected file's text:
      File file = chooser.getSelectedFile();
      try { textPane.read( new FileReader( file ), null );  }
      catch ( Exception ignored )
      {  JOptionPane.showMessageDialog( null, "Could not read from file \""
            + file.getName() + "\"", "Error!", JOptionPane.ERROR_MESSAGE );
         return;
      }

      text = textPane.getText();
      textChanged = true;
      textPane.setCaretPosition( 0 );
      textPane.getCaret().setVisible( true );
      textPane.getCaret().setSelectionVisible( true );
      textPaneTitle.setText( file.getName() );
   }


   // Highlight the next match (if any).  Note if the RE only changed
   // the Matcher must be regenerated, but the next match should be after
   // the current  position.  If the text changed the Matcher must be
   // reset to position 0 of the new text.
   // After a reset, show the caret position only (not the last match).

   private void showNextMatch ()
   {
      // Start the next search after the start of the last, to allow
      // overlapping matches.  If pos == 0 and nothing is selected, then
      // there was no previous match, so don't change pos from zero:
      int pos = textPane.getSelectionStart();
      if ( pos != 0 || textPane.getSelectionEnd() != 0 ) {
         ++pos;
      }

      if ( regExpChanged )
      {
         String re = regExTF.getText();
         if ( re == null || re.length() == 0 )
         {  JOptionPane.showMessageDialog( null, "No Regular Expression!",
              "Illegal Regular Expression", JOptionPane.ERROR_MESSAGE );
            return;
         }

         // This code adds the value of the action command of each selected
         // option checkbox, which was earlier set to the corresponding
         // integer constant from the Pattern class:
         int options = 0;
         for ( Iterator<JCheckBox> it
            = checkboxOptions.iterator(); it.hasNext(); )
         {  JCheckBox cb = it.next();
            if ( cb.isSelected() )
               options += Integer.parseInt( cb.getActionCommand() );
         }

         try
         {  refExpPattern = Pattern.compile( re, options );
         }
         catch ( PatternSyntaxException pse )
         {  JOptionPane.showMessageDialog( null, pse.getMessage(),
              "Illegal Regular Expression", JOptionPane.ERROR_MESSAGE );
            return;
         }
      }

      if ( regExpChanged || textChanged )
         regExp = refExpPattern.matcher( text );

      if ( textChanged )
      {  pos = 0;
         textPane.getCaret().setVisible( true );
         textPane.getCaret().setSelectionVisible( false );
      }

      regExpChanged = textChanged = false;

      if ( regExp.find( pos ) )
      {  // Highlight the match:
         textPane.setCaretPosition( regExp.start() );
         textPane.moveCaretPosition( regExp.end() );
         textScrollPane.validate();
         textPane.getCaret().setVisible( false );
         textPane.getCaret().setSelectionVisible( true );

         //Add any capture groups to capture group pane:
         captureGroupPane.setText( "" );
         int numGroups = regExp.groupCount();
         for ( int i = 1; i <= numGroups; ++i )
            captureGroupPane.append( i + ": " + regExp.group( i ) + "\n" );
      }
      else
      {  JOptionPane.showMessageDialog( null, "No matches remaining!" );
      }
   }


   private static class TextFilter extends javax.swing.filechooser.FileFilter
   {
      public final static String textExtension = "txt";

      public static String getExtension ( File f )
      {
         String ext = null;
         String s = f.getName();
         int i = s.lastIndexOf( '.' );

         if ( i > 0 &&  i < s.length() - 1 ) // Make sure dot isn't last char.
            ext = s.substring( i+1 ) . toLowerCase();
         return ext;
      }

      // Accept all directories and text files:
      public boolean accept ( File file )
      {
         if ( file.isDirectory() )
            return true;

         String extension = getExtension( file );
         if ( extension != null && extension.equals( textExtension ) )
               return true;
         return false;
      }

      // Return the description of this filter:
      public String getDescription ( )
      {
         return "Text Documents";
      }
   }
 }