View Javadoc

    1   /**
    2    * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
    3    */
    4   package net.sourceforge.pmd.cpd;
    5
    6   import java.awt.BorderLayout;
    7   import java.awt.Component;
    8   import java.awt.Dimension;
    9   import java.awt.Point;
    10  import java.awt.Toolkit;
    11  import java.awt.datatransfer.StringSelection;
    12  import java.awt.event.ActionEvent;
    13  import java.awt.event.ActionListener;
    14  import java.awt.event.ItemEvent;
    15  import java.awt.event.ItemListener;
    16  import java.awt.event.KeyEvent;
    17  import java.awt.event.MouseAdapter;
    18  import java.awt.event.MouseEvent;
    19  import java.io.File;
    20  import java.io.FileOutputStream;
    21  import java.io.IOException;
    22  import java.io.PrintWriter;
    23  import java.util.ArrayList;
    24  import java.util.Collections;
    25  import java.util.Comparator;
    26  import java.util.HashMap;
    27  import java.util.HashSet;
    28  import java.util.Iterator;
    29  import java.util.List;
    30  import java.util.Map;
    31  import java.util.Properties;
    32  import java.util.Set;
    33
    34  import javax.swing.AbstractButton;
    35  import javax.swing.BorderFactory;
    36  import javax.swing.JButton;
    37  import javax.swing.JCheckBox;
    38  import javax.swing.JCheckBoxMenuItem;
    39  import javax.swing.JComboBox;
    40  import javax.swing.JComponent;
    41  import javax.swing.JFileChooser;
    42  import javax.swing.JFrame;
    43  import javax.swing.JLabel;
    44  import javax.swing.JMenu;
    45  import javax.swing.JMenuBar;
    46  import javax.swing.JMenuItem;
    47  import javax.swing.JOptionPane;
    48  import javax.swing.JPanel;
    49  import javax.swing.JProgressBar;
    50  import javax.swing.JScrollPane;
    51  import javax.swing.JTable;
    52  import javax.swing.JTextArea;
    53  import javax.swing.JTextField;
    54  import javax.swing.KeyStroke;
    55  import javax.swing.SwingConstants;
    56  import javax.swing.Timer;
    57  import javax.swing.event.ListSelectionEvent;
    58  import javax.swing.event.ListSelectionListener;
    59  import javax.swing.event.TableModelListener;
    60  import javax.swing.table.DefaultTableCellRenderer;
    61  import javax.swing.table.JTableHeader;
    62  import javax.swing.table.TableColumn;
    63  import javax.swing.table.TableColumnModel;
    64  import javax.swing.table.TableModel;
    65
    66  import net.sourceforge.pmd.PMD;
    67  import net.sourceforge.pmd.util.IOUtil;
    68
    69  public class GUI implements CPDListener {
    70
    71  //	private interface Renderer {
    72  //		String render(Iterator<Match> items);
    73  //	}
    74
    75  	private static final Object[][] RENDERER_SETS = new Object[][] {
    76  		{ "Text", 		new Renderer() { public String render(Iterator<Match> items) { return new SimpleRenderer().render(items); } } },
    77  		{ "XML", 		new Renderer() { public String render(Iterator<Match> items) { return new XMLRenderer().render(items); } } },
    78  		{ "CSV (comma)",new Renderer() { public String render(Iterator<Match> items) { return new CSVRenderer(',').render(items); } } },
    79  		{ "CSV (tab)",	new Renderer() { public String render(Iterator<Match> items) { return new CSVRenderer('\t').render(items); } } }
    80  		};
    81
    82  	private static abstract class LanguageConfig {
    83  		public abstract Language languageFor(LanguageFactory lf, Properties p);
    84  		public boolean canIgnoreIdentifiers() { return false; }
    85  		public boolean canIgnoreLiterals() { return false; }
    86  		public boolean canIgnoreAnnotations() { return false; }
    87  		public abstract String[] extensions();
    88  	};
    89
    90  	private static final Object[][] LANGUAGE_SETS = new Object[][] {
    91  		{"Java", 			new LanguageConfig() {
    92  									public Language languageFor(LanguageFactory lf, Properties p) { return lf.createLanguage("java"); }
    93  									public boolean canIgnoreIdentifiers() { return true; }
    94  									public boolean canIgnoreLiterals() { return true; }
    95  									public boolean canIgnoreAnnotations() { return true; }
    96  									public String[] extensions() { return new String[] {".java", ".class" }; }; } },
    97  		{"JSP", 			new LanguageConfig() {
    98  									public Language languageFor(LanguageFactory lf, Properties p) { return lf.createLanguage("jsp"); }
    99  									public String[] extensions() { return new String[] {".jsp" }; }; } },
    100 		{"C++", 			new LanguageConfig() {
    101 									public Language languageFor(LanguageFactory lf, Properties p) { return lf.createLanguage("cpp"); }
    102 									public String[] extensions() { return new String[] {".cpp", ".c" }; }; } },
    103 		{"Ruby",			new LanguageConfig() {
    104 									public Language languageFor(LanguageFactory lf, Properties p) { return lf.createLanguage("ruby"); }
    105 									public String[] extensions() { return new String[] {".rb" }; }; } },
    106 		{"Fortran",			new LanguageConfig() {
    107 									public Language languageFor(LanguageFactory lf, Properties p) { return lf.createLanguage("fortran"); }
    108 									public String[] extensions() { return new String[] {".for", ".f", ".f66", ".f77", ".f90" }; }; } },
    109 		{"PHP",				new LanguageConfig() {
    110 									public Language languageFor(LanguageFactory lf, Properties p) { return lf.createLanguage("php"); }
    111 									public String[] extensions() { return new String[] {".php" }; };	} },
    112 		{"C#",				new LanguageConfig() {
    113 									public Language languageFor(LanguageFactory lf, Properties p) { return lf.createLanguage("cs"); }
    114 									public String[] extensions() { return new String[] {".cs" }; };	} },
    115 		{"Ecmascript",			new LanguageConfig() {
    116 									public Language languageFor(LanguageFactory lf, Properties p) { return lf.createLanguage("js"); }
    117 									public String[] extensions() { return new String[] {".js" }; }; } },
    118 		{"by extension...",		new LanguageConfig() {
    119 									public Language languageFor(LanguageFactory lf, Properties p) { return lf.createLanguage(LanguageFactory.BY_EXTENSION, p); }
    120 									public String[] extensions() { return new String[] {"" }; }; } },
    121 		};
    122
    123 	private static final int		DEFAULT_CPD_MINIMUM_LENGTH = 75;
    124 	private static final Map<String, LanguageConfig> LANGUAGE_CONFIGS_BY_LABEL = new HashMap<String, LanguageConfig>(LANGUAGE_SETS.length);
    125 	private static final KeyStroke	COPY_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_C,ActionEvent.CTRL_MASK,false);
    126 	private static final KeyStroke	DELETE_KEY_STROKE = KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0);
    127
    128 	private class ColumnSpec {
    129 		private String label;
    130 		private int alignment;
    131 		private int width;
    132 		private Comparator<Match> sorter;
    133
    134 		public ColumnSpec(String aLabel, int anAlignment, int aWidth, Comparator<Match> aSorter) {
    135 			label = aLabel;
    136 			alignment = anAlignment;
    137 			width = aWidth;
    138 			sorter = aSorter;
    139 		}
    140 		public String label() { return label; };
    141 		public int alignment() { return alignment; };
    142 		public int width() { return width; };
    143 		public Comparator<Match> sorter() { return sorter; };
    144 	}
    145
    146 	private final ColumnSpec[] matchColumns = new ColumnSpec[] {
    147 		new ColumnSpec("Source", 	SwingConstants.LEFT, -1, Match.LABEL_COMPARATOR),
    148 		new ColumnSpec("Matches", 	SwingConstants.RIGHT, 60, Match.MATCHES_COMPARATOR),
    149 		new ColumnSpec("Lines", 	SwingConstants.RIGHT, 45, Match.LINES_COMPARATOR),
    150 		};
    151
    152 	static {
    153 		for (int i=0; i<LANGUAGE_SETS.length; i++) {
    154 			LANGUAGE_CONFIGS_BY_LABEL.put((String)LANGUAGE_SETS[i][0], (LanguageConfig)LANGUAGE_SETS[i][1]);
    155 		}
    156 	}
    157
    158 	private static LanguageConfig languageConfigFor(String label) {
    159 		return LANGUAGE_CONFIGS_BY_LABEL.get(label);
    160 	}
    161
    162     private static class CancelListener implements ActionListener {
    163         public void actionPerformed(ActionEvent e) {
    164             System.exit(0);
    165         }
    166     }
    167
    168     private class GoListener implements ActionListener {
    169         public void actionPerformed(ActionEvent e) {
    170             new Thread(new Runnable() {
    171                 public void run() {
    172                     tokenizingFilesBar.setValue(0);
    173                     tokenizingFilesBar.setString("");
    174                     resultsTextArea.setText("");
    175                     phaseLabel.setText("");
    176                     timeField.setText("");
    177                     go();
    178                 }
    179             }).start();
    180         }
    181     }
    182
    183     private class SaveListener implements ActionListener {
    184
    185     	final Renderer renderer;
    186
    187     	public SaveListener(Renderer theRenderer) {
    188     		renderer = theRenderer;
    189     	}
    190
    191         public void actionPerformed(ActionEvent evt) {
    192             JFileChooser fcSave	= new JFileChooser();
    193             int ret = fcSave.showSaveDialog(GUI.this.frame);
    194             File f = fcSave.getSelectedFile();
    195             if (f == null || ret != JFileChooser.APPROVE_OPTION) {
    196         	return;
    197             }
    198
    199             if (!f.canWrite()) {
    200                 PrintWriter pw = null;
    201                 try {
    202                     pw = new PrintWriter(new FileOutputStream(f));
    203                     pw.write(renderer.render(matches.iterator()));
    204                     pw.flush();
    205                     JOptionPane.showMessageDialog(frame, "Saved " + matches.size() + " matches");
    206                 } catch (IOException e) {
    207                     error("Couldn't save file" + f.getAbsolutePath(), e);
    208                 } finally {
    209                     IOUtil.closeQuietly(pw);
    210                 }
    211             } else {
    212                 error("Could not write to file " + f.getAbsolutePath(), null);
    213             }
    214         }
    215
    216         private void error(String message, Exception e) {
    217             if (e != null) {
    218                 e.printStackTrace();
    219             }
    220             JOptionPane.showMessageDialog(GUI.this.frame, message);
    221         }
    222
    223     }
    224
    225     private class BrowseListener implements ActionListener {
    226         public void actionPerformed(ActionEvent e) {
    227             JFileChooser fc = new JFileChooser(rootDirectoryField.getText());
    228             fc.setFileSelectionMode(JFileChooser.FILES_AND_DIRECTORIES);
    229             fc.showDialog(frame, "Select");
    230             if (fc.getSelectedFile() != null) {
    231                 rootDirectoryField.setText(fc.getSelectedFile().getAbsolutePath());
    232             }
    233         }
    234     }
    235
    236 	private class AlignmentRenderer extends DefaultTableCellRenderer {
    237
    238 		private int[] alignments;
    239
    240 		public AlignmentRenderer(int[] theAlignments) {
    241 			alignments = theAlignments;
    242 		};
    243
    244 		public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
    245 			super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
    246
    247 			setHorizontalAlignment(alignments[column]);
    248
    249 			return this;
    250 		}
    251 	}
    252
    253     private JTextField rootDirectoryField	= new JTextField(System.getProperty("user.home"));
    254     private JTextField minimumLengthField	= new JTextField(Integer.toString(DEFAULT_CPD_MINIMUM_LENGTH));
    255     private JTextField encodingField		= new JTextField(System.getProperty("file.encoding"));
    256     private JTextField timeField			= new JTextField(6);
    257     private JLabel phaseLabel				= new JLabel();
    258     private JProgressBar tokenizingFilesBar = new JProgressBar();
    259     private JTextArea resultsTextArea		= new JTextArea();
    260     private JCheckBox recurseCheckbox		= new JCheckBox("", true);
    261     private JCheckBox ignoreIdentifiersCheckbox = new JCheckBox("", false);
    262     private JCheckBox ignoreLiteralsCheckbox = new JCheckBox("", false);
    263     private JCheckBox ignoreAnnotationsCheckbox = new JCheckBox("", false);
    264     private JComboBox languageBox			= new JComboBox();
    265     private JTextField extensionField		= new JTextField();
    266     private JLabel extensionLabel			= new JLabel("Extension:", SwingConstants.RIGHT);
    267     private JTable resultsTable				= new JTable();
    268     private JButton goButton;
    269     private JButton cancelButton;
    270     private JPanel progressPanel;
    271     private JFrame frame;
    272     private boolean trimLeadingWhitespace;
    273
    274     private List<Match> matches = new ArrayList<Match>();
    275
    276     private void addSaveOptionsTo(JMenu menu) {
    277
    278         JMenuItem saveItem;
    279
    280         for (int i=0; i<RENDERER_SETS.length; i++) {
    281         	saveItem = new JMenuItem("Save as " + RENDERER_SETS[i][0]);
    282         	saveItem.addActionListener(new SaveListener((Renderer)RENDERER_SETS[i][1]));
    283         	menu.add(saveItem);
    284         }
    285     }
    286
    287     public GUI() {
    288         frame = new JFrame("PMD Duplicate Code Detector (v " + PMD.VERSION + ')');
    289
    290         timeField.setEditable(false);
    291
    292         JMenu fileMenu = new JMenu("File");
    293         fileMenu.setMnemonic('f');
    294
    295         addSaveOptionsTo(fileMenu);
    296
    297         JMenuItem exitItem = new JMenuItem("Exit");
    298         exitItem.setMnemonic('x');
    299         exitItem.addActionListener(new CancelListener());
    300         fileMenu.add(exitItem);
    301         JMenu viewMenu = new JMenu("View");
    302         fileMenu.setMnemonic('v');
    303         JMenuItem trimItem = new JCheckBoxMenuItem("Trim leading whitespace");
    304         trimItem.addItemListener(new ItemListener() {
    305             public void itemStateChanged(ItemEvent e) {
    306                 AbstractButton button = (AbstractButton)e.getItem();
    307                 GUI.this.trimLeadingWhitespace = button.isSelected();
    308             }
    309         });
    310         viewMenu.add(trimItem);
    311         JMenuBar menuBar = new JMenuBar();
    312         menuBar.add(fileMenu);
    313         menuBar.add(viewMenu);
    314         frame.setJMenuBar(menuBar);
    315
    316         // first make all the buttons
    317         JButton browseButton = new JButton("Browse");
    318         browseButton.setMnemonic('b');
    319         browseButton.addActionListener(new BrowseListener());
    320         goButton = new JButton("Go");
    321         goButton.setMnemonic('g');
    322         goButton.addActionListener(new GoListener());
    323         cancelButton = new JButton("Cancel");
    324         cancelButton.addActionListener(new CancelListener());
    325
    326         JPanel settingsPanel = makeSettingsPanel(browseButton, goButton, cancelButton);
    327         progressPanel = makeProgressPanel();
    328         JPanel resultsPanel = makeResultsPanel();
    329
    330         adjustLanguageControlsFor((LanguageConfig)LANGUAGE_SETS[0][1]);
    331
    332         frame.getContentPane().setLayout(new BorderLayout());
    333         JPanel topPanel = new JPanel();
    334         topPanel.setLayout(new BorderLayout());
    335         topPanel.add(settingsPanel, BorderLayout.NORTH);
    336         topPanel.add(progressPanel, BorderLayout.CENTER);
    337         setProgressControls(false);	// not running now
    338         frame.getContentPane().add(topPanel, BorderLayout.NORTH);
    339         frame.getContentPane().add(resultsPanel, BorderLayout.CENTER);
    340         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    341         frame.pack();
    342         frame.setVisible(true);
    343     }
    344
    345     private void adjustLanguageControlsFor(LanguageConfig current) {
    346          ignoreIdentifiersCheckbox.setEnabled(current.canIgnoreIdentifiers());
    347          ignoreLiteralsCheckbox.setEnabled(current.canIgnoreLiterals());
    348          ignoreAnnotationsCheckbox.setEnabled(current.canIgnoreAnnotations());
    349          extensionField.setText(current.extensions()[0]);
    350          boolean enableExtension = current.extensions()[0].length() == 0;
    351          extensionField.setEnabled(enableExtension);
    352          extensionLabel.setEnabled(enableExtension);
    353     }
    354
    355     private JPanel makeSettingsPanel(JButton browseButton, JButton goButton, JButton cxButton) {
    356         JPanel settingsPanel = new JPanel();
    357         GridBagHelper helper = new GridBagHelper(settingsPanel, new double[]{0.2, 0.7, 0.1, 0.1});
    358         helper.addLabel("Root source directory:");
    359         helper.add(rootDirectoryField);
    360         helper.add(browseButton, 2);
    361         helper.nextRow();
    362         helper.addLabel("Report duplicate chunks larger than:");
    363         minimumLengthField.setColumns(4);
    364         helper.add(minimumLengthField);
    365         helper.addLabel("Language:");
    366         for (int i=0; i<LANGUAGE_SETS.length; i++) {
    367         	languageBox.addItem(LANGUAGE_SETS[i][0]);
    368         }
    369         languageBox.addActionListener(new ActionListener() {
    370             public void actionPerformed(ActionEvent e) {
    371             	adjustLanguageControlsFor(
    372             			languageConfigFor((String)languageBox.getSelectedItem())
    373             			);
    374             }
    375         });
    376         helper.add(languageBox);
    377         helper.nextRow();
    378         helper.addLabel("Also scan subdirectories?");
    379         helper.add(recurseCheckbox);
    380
    381         helper.add(extensionLabel);
    382         helper.add(extensionField);
    383
    384         helper.nextRow();
    385         helper.addLabel("Ignore literals?");
    386         helper.add(ignoreLiteralsCheckbox);
    387         helper.addLabel("");
    388         helper.addLabel("");
    389         helper.nextRow();
    390
    391         helper.nextRow();
    392         helper.addLabel("Ignore identifiers?");
    393         helper.add(ignoreIdentifiersCheckbox);
    394         helper.addLabel("");
    395         helper.addLabel("");
    396         helper.nextRow();
    397
    398         helper.nextRow();
    399         helper.addLabel("Ignore annotations?");
    400         helper.add(ignoreAnnotationsCheckbox);
    401         helper.add(goButton);
    402         helper.add(cxButton);
    403         helper.nextRow();
    404
    405         helper.addLabel("File encoding (defaults based upon locale):");
    406         encodingField.setColumns(1);
    407         helper.add(encodingField);
    408         helper.addLabel("");
    409         helper.addLabel("");
    410         helper.nextRow();
    411 //        settingsPanel.setBorder(BorderFactory.createTitledBorder("Settings"));
    412         return settingsPanel;
    413     }
    414
    415     private JPanel makeProgressPanel() {
    416         JPanel progressPanel = new JPanel();
    417         final double[] weights = {0.0, 0.8, 0.4, 0.2};
    418         GridBagHelper helper = new GridBagHelper(progressPanel, weights);
    419         helper.addLabel("Tokenizing files:");
    420         helper.add(tokenizingFilesBar, 3);
    421         helper.nextRow();
    422         helper.addLabel("Phase:");
    423         helper.add(phaseLabel);
    424         helper.addLabel("Time elapsed:");
    425         helper.add(timeField);
    426         helper.nextRow();
    427         progressPanel.setBorder(BorderFactory.createTitledBorder("Progress"));
    428         return progressPanel;
    429     }
    430
    431     private JPanel makeResultsPanel() {
    432         JPanel resultsPanel = new JPanel();
    433         resultsPanel.setLayout(new BorderLayout());
    434         JScrollPane areaScrollPane = new JScrollPane(resultsTextArea);
    435         resultsTextArea.setEditable(false);
    436         areaScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
    437         areaScrollPane.setPreferredSize(new Dimension(600, 300));
    438
    439         resultsPanel.add(makeMatchList(), BorderLayout.WEST);
    440         resultsPanel.add(areaScrollPane, BorderLayout.CENTER);
    441         return resultsPanel;
    442     }
    443
    444     private void populateResultArea() {
    445     	int[] selectionIndices = resultsTable.getSelectedRows();
    446     	TableModel model = resultsTable.getModel();
    447     	List<Match> selections = new ArrayList<Match>(selectionIndices.length);
    448     	for (int i=0; i<selectionIndices.length; i++) {
    449     		selections.add((Match)model.getValueAt(selectionIndices[i], 99));
    450     	}
    451     	String report = new SimpleRenderer(trimLeadingWhitespace).render(selections.iterator());
    452     	resultsTextArea.setText(report);
    453     	resultsTextArea.setCaretPosition(0);	// move to the top
    454     }
    455
    456     private void copyMatchListSelectionsToClipboard() {
    457
    458     	int[] selectionIndices = resultsTable.getSelectedRows();
    459     	int colCount = resultsTable.getColumnCount();
    460
    461     	StringBuilder sb = new StringBuilder();
    462
    463     	for (int r=0; r<selectionIndices.length; r++) {
    464 			if (r > 0) {
    465 			    sb.append('\n');
    466 			}
    467 			sb.append(resultsTable.getValueAt(selectionIndices[r], 0));
    468     		for (int c=1; c<colCount; c++) {
    469     			sb.append('\t');
    470     			sb.append(resultsTable.getValueAt(selectionIndices[r], c));
    471     		}
    472     	}
    473
    474     	StringSelection ss = new StringSelection(sb.toString());
    475         Toolkit.getDefaultToolkit().getSystemClipboard().setContents(ss, null);
    476     }
    477
    478     private void deleteMatchlistSelections() {
    479
    480     	int[] selectionIndices = resultsTable.getSelectedRows();
    481
    482     	for (int i=selectionIndices.length-1; i >=0; i--) {
    483     		matches.remove(selectionIndices[i]);
    484     	}
    485
    486     	resultsTable.getSelectionModel().clearSelection();
    487     	resultsTable.addNotify();
    488     }
    489
    490     private JComponent makeMatchList() {
    491
    492     	resultsTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
    493 			public void valueChanged(ListSelectionEvent e) {
    494 				populateResultArea();
    495 			}});
    496
    497     	resultsTable.registerKeyboardAction(new ActionListener() {
    498 			public void actionPerformed(ActionEvent e) { copyMatchListSelectionsToClipboard(); }
    499     		},"Copy", COPY_KEY_STROKE, JComponent.WHEN_FOCUSED);
    500
    501     	resultsTable.registerKeyboardAction(new ActionListener() {
    502 			public void actionPerformed(ActionEvent e) { deleteMatchlistSelections(); }
    503     		},"Del", DELETE_KEY_STROKE, JComponent.WHEN_FOCUSED);
    504
    505     	int[] alignments = new int[matchColumns.length];
    506     	for (int i=0; i<alignments.length; i++) {
    507     	    alignments[i] = matchColumns[i].alignment();
    508     	}
    509
    510     	resultsTable.setDefaultRenderer(Object.class, new AlignmentRenderer(alignments));
    511
    512     	final JTableHeader header = resultsTable.getTableHeader();
    513     	header.addMouseListener( new MouseAdapter() {
    514 			public void mouseClicked(MouseEvent e) {
    515 				sortOnColumn(header.columnAtPoint(new Point(e.getX(), e.getY())));
    516 				}
    517 			});
    518
    519         return new JScrollPane(resultsTable);
    520     }
    521
    522     private boolean isLegalPath(String path, LanguageConfig config) {
    523     	String[] extensions = config.extensions();
    524     	for (int i=0; i<extensions.length; i++) {
    525     		if (path.endsWith(extensions[i]) && extensions[i].length() > 0) {
    526     		    return true;
    527     		}
    528     	}
    529     	return false;
    530     }
    531
    532     private String setLabelFor(Match match) {
    533
    534     	Set<String> sourceIDs = new HashSet<String>(match.getMarkCount());
    535     	for (Iterator<TokenEntry> occurrences = match.iterator(); occurrences.hasNext();) {
    536              sourceIDs.add(occurrences.next().getTokenSrcID());
    537           }
    538     	String label;
    539
    540     	if (sourceIDs.size() == 1) {
    541     		String sourceId = sourceIDs.iterator().next();
    542     		int separatorPos = sourceId.lastIndexOf(File.separatorChar);
    543     		label = "..." + sourceId.substring(separatorPos);
    544     		} else {
    545     	    	label = "(" + sourceIDs.size() + " separate files)";
    546     		}
    547
    548     	match.setLabel(label);
    549     	return label;
    550     }
    551
    552     private void setProgressControls(boolean isRunning) {
    553         progressPanel.setVisible(isRunning);
    554         goButton.setEnabled(!isRunning);
    555         cancelButton.setEnabled(isRunning);
    556     }
    557
    558     private void go() {
    559     	String dirPath = rootDirectoryField.getText();
    560         try {
    561             if (!(new File(dirPath)).exists()) {
    562                 JOptionPane.showMessageDialog(frame,
    563                         "Can't read from that root source directory",
    564                         "Error", JOptionPane.ERROR_MESSAGE);
    565                 return;
    566             }
    567
    568             setProgressControls(true);
    569
    570             Properties p = new Properties();
    571             p.setProperty(JavaTokenizer.IGNORE_IDENTIFIERS, String.valueOf(ignoreIdentifiersCheckbox.isSelected()));
    572             p.setProperty(JavaTokenizer.IGNORE_LITERALS, String.valueOf(ignoreLiteralsCheckbox.isSelected()));
    573             p.setProperty(JavaTokenizer.IGNORE_ANNOTATIONS, String.valueOf(ignoreAnnotationsCheckbox.isSelected()));
    574             p.setProperty(LanguageFactory.EXTENSION, extensionField.getText());
    575             LanguageConfig conf = languageConfigFor((String)languageBox.getSelectedItem());
    576             Language language = conf.languageFor(new LanguageFactory(), p);
    577             language.setProperties(p);
    578             CPDConfiguration config = new CPDConfiguration(
    579             		Integer.parseInt(minimumLengthField.getText()),
    580             		language, encodingField.getText()
    581             		);
    582             CPD cpd = new CPD(config);
    583             cpd.setCpdListener(this);
    584             tokenizingFilesBar.setMinimum(0);
    585             phaseLabel.setText("");
    586             if (isLegalPath(dirPath, conf)) {	// should use the language file filter instead?
    587             	cpd.add(new File(dirPath));
    588             } else {
    589                 if (recurseCheckbox.isSelected()) {
    590                     cpd.addRecursively(dirPath);
    591                 } else {
    592                     cpd.addAllInDirectory(dirPath);
    593                 }
    594             }
    595             Timer t = createTimer();
    596             t.start();
    597             cpd.go();
    598             t.stop();
    599
    600         	matches = new ArrayList<Match>();
    601         	for (Iterator<Match> i = cpd.getMatches(); i.hasNext();) {
    602         		Match match = i.next();
    603         		setLabelFor(match);
    604         		matches.add(match);
    605         	}
    606
    607             setListDataFrom(cpd.getMatches());
    608             String report = new SimpleRenderer().render(cpd.getMatches());
    609             if (report.length() == 0) {
    610                 JOptionPane.showMessageDialog(frame,
    611                         "Done. Couldn't find any duplicates longer than " + minimumLengthField.getText() + " tokens");
    612             } else {
    613                 resultsTextArea.setText(report);
    614             }
    615         } catch (IOException t) {
    616             t.printStackTrace();
    617             JOptionPane.showMessageDialog(frame, "Halted due to " + t.getClass().getName() + "; " + t.getMessage());
    618         } catch (RuntimeException t) {
    619             t.printStackTrace();
    620             JOptionPane.showMessageDialog(frame, "Halted due to " + t.getClass().getName() + "; " + t.getMessage());
    621         }
    622         setProgressControls(false);
    623     }
    624
    625 	private Timer createTimer() {
    626
    627 		final long start = System.currentTimeMillis();
    628
    629 		Timer t = new Timer(1000, new ActionListener() {
    630 		    public void actionPerformed(ActionEvent e) {
    631 		        long now = System.currentTimeMillis();
    632 		        long elapsedMillis = now - start;
    633 		        long elapsedSeconds = elapsedMillis / 1000;
    634 		        long minutes = (long) Math.floor(elapsedSeconds / 60);
    635 		        long seconds = elapsedSeconds - (minutes * 60);
    636 		        timeField.setText(formatTime(minutes, seconds));
    637 		    }
    638 		});
    639 		return t;
    640 	}
    641
    642 	private static String formatTime(long minutes, long seconds) {
    643
    644 		StringBuilder sb = new StringBuilder(5);
    645 		if (minutes < 10) { sb.append('0'); }
    646 		sb.append(minutes).append(':');
    647 		if (seconds < 10) { sb.append('0'); }
    648 		sb.append(seconds);
    649 		return sb.toString();
    650 	}
    651
    652     private interface SortingTableModel<E> extends TableModel {
    653     	int sortColumn();
    654     	void sortColumn(int column);
    655     	boolean sortDescending();
    656     	void sortDescending(boolean flag);
    657     	void sort(Comparator<E> comparator);
    658     }
    659
    660     private TableModel tableModelFrom(final List<Match> items) {
    661
    662     	TableModel model = new SortingTableModel<Match>() {
    663
    664     		private int sortColumn;
    665     		private boolean sortDescending;
    666
    667     		 public Object getValueAt(int rowIndex, int columnIndex) {
    668     			Match match = items.get(rowIndex);
    669     			switch (columnIndex) {
    670     				case 0: return match.getLabel();
    671     				case 2: return Integer.toString(match.getLineCount());
    672     				case 1: return match.getMarkCount() > 2 ? Integer.toString(match.getMarkCount()) : "";
    673     				case 99: return match;
    674     				default: return "";
    675     				}
    676     		 	}
    677 			public int getColumnCount() { return matchColumns.length;	}
    678 			public int getRowCount() {	return items.size(); }
    679 			public boolean isCellEditable(int rowIndex, int columnIndex) {	return false;	}
    680 			public Class<?> getColumnClass(int columnIndex) { return Object.class;	}
    681 			public void setValueAt(Object aValue, int rowIndex, int columnIndex) {	}
    682 			public String getColumnName(int i) {	return matchColumns[i].label();	}
    683 			public void addTableModelListener(TableModelListener l) { }
    684 			public void removeTableModelListener(TableModelListener l) { }
    685 			public int sortColumn() { return sortColumn; };
    686 			public void sortColumn(int column) { sortColumn = column; };
    687 			public boolean sortDescending() { return sortDescending; };
    688 			public void sortDescending(boolean flag) { sortDescending = flag; };
    689 			public void sort(Comparator<Match> comparator) {
    690 				Collections.sort(items, comparator);
    691 				if (sortDescending) {
    692 				    Collections.reverse(items);
    693 				}
    694 				}
    695     		};
    696
    697     	return model;
    698     }
    699
    700     private void sortOnColumn(int columnIndex) {
    701     	Comparator<Match> comparator = matchColumns[columnIndex].sorter();
    702     	SortingTableModel<Match> model = (SortingTableModel<Match>)resultsTable.getModel();
    703     	if (model.sortColumn() == columnIndex) {
    704     		model.sortDescending(!model.sortDescending());
    705     	}
    706     	model.sortColumn(columnIndex);
    707     	model.sort(comparator);
    708
    709     	resultsTable.getSelectionModel().clearSelection();
    710     	resultsTable.repaint();
    711     }
    712
    713     private void setListDataFrom(Iterator iter) {
    714
    715     	resultsTable.setModel(tableModelFrom(matches));
    716
    717     	TableColumnModel colModel = resultsTable.getColumnModel();
    718     	TableColumn column;
    719     	int width;
    720
    721     	for (int i=0; i<matchColumns.length; i++) {
    722     		if (matchColumns[i].width() > 0) {
    723     			column = colModel.getColumn(i);
    724     			width = matchColumns[i].width();
    725     			column.setPreferredWidth(width);
    726     			column.setMinWidth(width);
    727     			column.setMaxWidth(width);
    728     		}
    729     	}
    730     }
    731
    732     // CPDListener
    733     public void phaseUpdate(int phase) {
    734         phaseLabel.setText(getPhaseText(phase));
    735     }
    736
    737     public String getPhaseText(int phase) {
    738         switch (phase) {
    739             case CPDListener.INIT:
    740                 return "Initializing";
    741             case CPDListener.HASH:
    742                 return "Hashing";
    743             case CPDListener.MATCH:
    744                 return "Matching";
    745             case CPDListener.GROUPING:
    746                 return "Grouping";
    747             case CPDListener.DONE:
    748                 return "Done";
    749             default :
    750                 return "Unknown";
    751         }
    752     }
    753
    754     public void addedFile(int fileCount, File file) {
    755         tokenizingFilesBar.setMaximum(fileCount);
    756         tokenizingFilesBar.setValue(tokenizingFilesBar.getValue() + 1);
    757     }
    758     // CPDListener
    759
    760
    761     public static void main(String[] args) {
    762     	//this should prevent the disk not found popup
    763         // System.setSecurityManager(null);
    764         new GUI();
    765     }
    766
    767 }