/** * Copyright 2005-2012 Akiban Technologies, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import java.awt.BorderLayout; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.regex.Pattern; import javax.swing.DefaultListModel; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JProgressBar; import javax.swing.JScrollPane; import javax.swing.JTextField; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import com.persistit.Exchange; import com.persistit.KeyFilter; import com.persistit.Persistit; import com.persistit.exception.PersistitException; /** * <p> * Demonstrates KeyFilter, Exchange pooling and use of Persistit within a * JFC/Swing application. This application presents a simple UI that lets you * load a list of file names from a text file or directory path, and then find * subsets of that list by typing partial matches with wildcards. For example, * you can find all instances of files named "read.me" by searching for * "*read.me". The search is made fast by indexing each name by its regular * spelling, and by its reversed spelling. Thus all "*read.me" files can be * found by traversing the keys of the reversed-spelling index from "em.daer" to * "em.daer". This program uses a KeyFilter to simplify this step. * </p> * <p> * This program can either read a list of file names from a file or traverse * directories. Specify either a text file name or a directory name in the Load * File text box. * </p> * * @version 1.0 */ public class FindFile extends JPanel { private DefaultListModel listModel = new DefaultListModel(); private JList list = new JList(listModel); private JTextField searchField = new JTextField(30); private JTextField loadField = new JTextField(30); private JButton searchButton = new JButton("Find"); private JButton loadButton = new JButton("Load"); private JButton clearButton = new JButton("Clear"); private JProgressBar progressBar = new JProgressBar(); private int currentCount; private int estimatedTotalCount; private String currentDirectory; private Persistit persistit; /** * Construct a Swing JPanel with a simple layout for loading, clearing and * searching for lists of files. * * @param defaultFileName * Name of a text file containing files name list. May be null; * this value is only used to populate a JTextField with a * default value. */ public FindFile(String defaultFileName, Persistit persistit) { setLayout(new BorderLayout()); JPanel northPanel = new JPanel(); northPanel.add(new JLabel("Search for")); northPanel.add(searchField); northPanel.add(searchButton); northPanel.add(progressBar); JPanel southPanel = new JPanel(); southPanel.add(new JLabel("Load file")); southPanel.add(loadField); southPanel.add(loadButton); southPanel.add(clearButton); add(northPanel, BorderLayout.NORTH); add(new JScrollPane(list), BorderLayout.CENTER); add(southPanel, BorderLayout.SOUTH); if (defaultFileName != null) loadField.setText(defaultFileName); this.persistit = persistit; searchButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { doSearch(); } }); loadButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { doLoad(); } }); clearButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent ae) { doClear(); } }); } /** * <p> * Search for all file names that match a specified wildcard expression. The * wildcard expression may contain "*" to represent zero or more arbitry * characters and "?" to represent exactly one arbitrary character. For * example, the expression "/opt/*java" finds files in subdirectories of * /opt that end with the four letters "java". To test for matches, this * method converts the wildcard expression to a regular expression. * </p> * <p> */ private void doSearch() { // // First clear the old content. // listModel.clear(); // // * and ? are treated as traditional wildcards. // String expr = searchField.getText(); StringBuilder sb = new StringBuilder(); for (int index = expr.length(); --index >= 0;) { int c = expr.charAt(index); if (c == '*' || c == '?') break; sb.append((char) c); } // // Substring the follows the final wildcard character. For example, // if the input is "/opt/*java", the suffix is "java". // final String suffix = sb.toString(); sb.setLength(0); int lastPrefixIndex = -1; boolean wildcardFound = false; sb.append("^"); for (int index = 0; index < expr.length(); index++) { int c = expr.charAt(index); switch (c) { // Characters that need to be quoted in the regular expression case '\\': case '.': case '^': case '$': case '[': case ']': { sb.append('\\'); sb.append((char) c); break; } // translaction of "*" case '*': { wildcardFound = true; sb.append(".*"); break; } // translation of "?" case '?': { wildcardFound = true; sb.append("."); } default: { sb.append((char) c); } if (!wildcardFound) lastPrefixIndex = index; } } sb.append("$"); final Pattern pattern = Pattern.compile(sb.toString()); final DefaultListModel model = (DefaultListModel) list.getModel(); // // Substring that precedes the first wildcard character. For example, // if the input is "/opt/*java", the prefix is "/opt/". // final String prefix = expr.substring(0, lastPrefixIndex + 1); // // Since the traversal may take some time, it needs to be // performed in a separate thread. // Thread workerThread = new Thread() { public void run() { if (suffix.length() > prefix.length()) { // // Traverse the index formed from reversing the spelling // of each file name. // traverseFileNames(suffix, pattern, false); } else { // // Traverse the index based on regular file name. // traverseFileNames(prefix, pattern, true); } adjustProgressBar(0, 0); } }; workerThread.start(); } /** * Load a list of files supplied in a text file. The work is done in a * separate thread; the worker thread periodically updates a progress bar. */ private void doLoad() { final String fileName = loadField.getText(); // // So we don't launch two competing threads (although nothing very // bad would happen if we did.) // loadButton.setEnabled(false); Thread workerThread = new Thread(new Runnable() { public void run() { loadFileNames(fileName); } }); workerThread.start(); } /** * Remove all the file names previously loaded into the index. This is * performed in a separate thread. * */ private void doClear() { Thread workerThread = new Thread(new Runnable() { public void run() { resetFileNames(); } }); workerThread.start(); } /** * Populate the DefaultListModel with file names that match the specified * pattern and update a progress bar while searching. * * @param fixed * @param pattern * @param forward */ private void traverseFileNames(String fixed, Pattern pattern, boolean forward) { currentCount = 0; estimatedTotalCount = 1000; final ArrayList selectedFileNames = new ArrayList(); Exchange ex = null; StringBuilder sb = new StringBuilder(); try { ex = persistit.getExchange("ffdemo", "filenames", true); ex.clear().append(forward ? "L2R" : "R2L"); // // Construct a KeyFilter that accepts all keys within the // designated subtree. // KeyFilter filter = new KeyFilter(ex.getKey()); if (fixed.length() != 0) { String end = fixed.substring(0, fixed.length() - 1) + new Character((char) (fixed.charAt(fixed.length() - 1) + 1)); // // append a Term that selects only the range accepted by the // fixed portion of the name. // filter = filter.append(KeyFilter.rangeTerm(fixed, end)); } while (ex.next(filter)) { String fileName = ex.getKey().indexTo(1).decodeString(); if (!forward) { sb.setLength(0); sb.append(fileName); fileName = sb.reverse().toString(); } // // Apply the Regex pattern, and if the name matches, // add it to the list model. // if (pattern.matcher(fileName).matches()) { selectedFileNames.add(fileName); currentCount++; if (currentCount + 200 > estimatedTotalCount) { estimatedTotalCount += 500; } if (currentCount % 100 == 0) { adjustProgressBar(currentCount, estimatedTotalCount); } } } estimatedTotalCount = currentCount; adjustProgressBar(currentCount, currentCount); setEnabled(searchField, true); SwingUtilities.invokeLater(new Runnable() { public void run() { int size = selectedFileNames.size(); for (int index = 0; index < size; index++) { listModel.addElement(selectedFileNames.get(index)); } } }); } catch (PersistitException pe) { pe.printStackTrace(); } finally { persistit.releaseExchange(ex); } } /** * Load and index a list of path names into a Persistit database. This * process takes a few seconds, and it updates a JProgressBar to indicate * progress. * * @param fromFileName * Name of the file to read from. */ private void loadFileNames(String fromFileName) { currentCount = 0; estimatedTotalCount = 1000; resetFileNames(); try { StringBuilder sb = new StringBuilder(); long time = System.currentTimeMillis(); File file = new File(fromFileName); if (file.isDirectory()) { loadFromDirectory(file, sb, 100); } else { loadFromTextFile(file, sb); } time = System.currentTimeMillis() - time; System.out.println("Took " + time + "ms to load " + currentCount + " file names"); adjustProgressBar(0, 0); } catch (IOException ioe) { ioe.printStackTrace(); } finally { setEnabled(loadButton, true); } } private void loadFromDirectory(File dir, StringBuilder sb, int depth) throws IOException { if (dir.exists()) { loadOneLine(dir.getPath(), sb); File[] files = dir.listFiles(); if (files == null || depth == 0) { System.out.println("Can't traverse directory " + dir); } else { for (File file : dir.listFiles()) { if (file.isDirectory()) { loadFromDirectory(file, sb, depth - 1); } else { loadOneLine(file.getPath(), sb); } } } } } private void loadFromTextFile(File file, StringBuilder sb) throws IOException { BufferedReader reader = new BufferedReader(new FileReader(file)); for (;;) { String line = reader.readLine(); boolean done = line == null; if (done) { break; } loadOneLine(line, sb); } } private void resetFileNames() { Exchange ex = null; try { // Obtain an Exchange from the Exchange pool. The arguments are // the same as Exchange's constructor. // ex = persistit.getExchange("ffdemo", "filenames", true); // // Delete all keys in the filenames tree. ex.removeAll(); } catch (PersistitException pe) { pe.printStackTrace(); } finally { // // Relinquish the Exchange back to the pool. Note that if // ex is null due to an exception, the releaseExchange method // does nothing. // persistit.releaseExchange(ex); } } /** * Parse and index a file name from one line of the source text file. For * Linix/Unix ls command, this involves recognizing lines that introduce * subdirectories. * * @param line * The line * @param sb * A StringBuilder to reuse for concatenation. */ private void loadOneLine(String line, StringBuilder sb) { // // handles output from Windows/DOS command // DIR /B /S and // 'nix command // ls -1 -p -R // if (line.length() == 0 || line.endsWith("/")) return; if (line.endsWith(":")) { currentDirectory = line.substring(0, line.length() - 1); return; } sb.setLength(0); if (currentDirectory != null) { sb.append(currentDirectory); sb.append('/'); } sb.append(line); // // fileName contains the fully formed file name (with path). // reversedFileName contains the name spelled in reverse. // String fileName = sb.toString(); String reversedFileName = sb.reverse().toString(); Exchange ex = null; try { // Obtain an Exchange from the Exchange pool. The arguments are // the same as Exchange's constructor. // ex = persistit.getExchange("ffdemo", "filenames", true); ex.getValue().put(null); // No value to store, just the keys ex.clear().append("L2R").append(fileName).store(); ex.clear().append("R2L").append(reversedFileName).store(); } catch (PersistitException pe) { pe.printStackTrace(); } finally { // // Relinquish the Exchange back to the pool. Note that if // ex is null due to an exception, the releaseExchange method // does nothing. // persistit.releaseExchange(ex); } if (currentCount % 1000 == 0) { adjustProgressBar(currentCount, estimatedTotalCount); if (currentCount % 10000 == 0) { System.out.println(Thread.currentThread() + " has loaded " + currentCount + " file names"); } } currentCount++; if (currentCount >= estimatedTotalCount) { estimatedTotalCount = estimatedTotalCount *= 2; } } /** * Adjust the JProgressBar. This is invoked by worker threads, so it must * call SwingUtilities.invokeLater to enqueue the action to run on the Swing * event dispatch thread. * * @param value * Current progress value * * @param maximum * Estimated total value */ private void adjustProgressBar(final int value, final int maximum) { SwingUtilities.invokeLater(new Runnable() { public void run() { progressBar.setMaximum(maximum); progressBar.setValue(value); } }); } /** * Enable or disable a component. This is invoked by worker threads so it * must call SwingUtilities.invokeLater to enqueue the action to run on the * Swing event dispatch thread. * * @param component * The component to disable or enable * * @param enabled * <tt>true</tt> to enable the component, * <tt>false,/tt> to disable it. */ private void setEnabled(final JComponent component, final boolean enabled) { SwingUtilities.invokeLater(new Runnable() { public void run() { component.setEnabled(enabled); } }); } /** * Main program entry point. Sets up a JFrame, adds a WindowListener to * close Persistit when the JFrame closes, and creates the FindFileDemo * panel within the JFrame. * * @param args */ public static void main(String[] args) { final Persistit persistit = new Persistit(); try { persistit.initialize(); } catch (PersistitException pe) { System.err.println("PersistitException during initialization"); pe.printStackTrace(); } JFrame frame = new JFrame("FileFindDemo"); frame.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); // // Persistit should always be closed on normal program exit. For // a Swing application, do this on the windowClosed event of // the containing JFrame. // frame.addWindowListener(new WindowAdapter() { public void windowClosed(WindowEvent we) { // // Persistit.close may take several seconds to complete. Best // not to do it on the Swing event dispatch thread. // Thread workerThread = new Thread(new Runnable() { public void run() { try { System.out.println("Closing Persistit"); persistit.close(); } catch (PersistitException pe) { System.err.println("PersistitException during close"); pe.printStackTrace(); } } }); workerThread.start(); } }); String defaultFileName = args.length == 0 ? null : args[0]; // // Create the UI panel. // frame.getContentPane().add(new FindFile(defaultFileName, persistit)); // // Force layout. // frame.pack(); // // Make it visible. // frame.setVisible(true); } }