View Javadoc

1   package com.melloware.jukes.gui.view.dialogs;
2   
3   import java.awt.BorderLayout;
4   import java.awt.EventQueue;
5   import java.awt.Frame;
6   import java.awt.event.ActionEvent;
7   import java.io.File;
8   import java.io.IOException;
9   import java.util.ArrayList;
10  import java.util.Collection;
11  import java.util.Enumeration;
12  import java.util.HashMap;
13  import java.util.Iterator;
14  import java.util.Map;
15  
16  import javax.swing.AbstractAction;
17  import javax.swing.Action;
18  import javax.swing.DefaultListModel;
19  import javax.swing.JButton;
20  import javax.swing.JComponent;
21  import javax.swing.JFileChooser;
22  import javax.swing.JLabel;
23  import javax.swing.JList;
24  import javax.swing.JPanel;
25  import javax.swing.JScrollPane;
26  import javax.swing.JTextField;
27  import javax.swing.ListSelectionModel;
28  import javax.swing.text.JTextComponent;
29  
30  import org.apache.commons.io.FileUtils;
31  import org.apache.commons.io.filefilter.DirectoryFileFilter;
32  import org.apache.commons.logging.Log;
33  import org.apache.commons.logging.LogFactory;
34  
35  import com.jgoodies.forms.builder.PanelBuilder;
36  import com.jgoodies.forms.factories.Borders;
37  import com.jgoodies.forms.factories.ButtonBarFactory;
38  import com.jgoodies.forms.layout.CellConstraints;
39  import com.jgoodies.forms.layout.FormLayout;
40  import com.jgoodies.uif.AbstractDialog;
41  import com.jgoodies.uif.action.ActionManager;
42  import com.jgoodies.uif.util.ResourceUtils;
43  import com.jgoodies.uif.util.Worker;
44  import com.jgoodies.uifextras.panel.HeaderPanel;
45  import com.jgoodies.validation.Severity;
46  import com.melloware.jukes.db.HibernateDao;
47  import com.melloware.jukes.db.HibernateUtil;
48  import com.melloware.jukes.exception.InfrastructureException;
49  import com.melloware.jukes.file.MusicDirectory;
50  import com.melloware.jukes.file.filter.FilterFactory;
51  import com.melloware.jukes.gui.tool.Actions;
52  import com.melloware.jukes.gui.tool.Resources;
53  import com.melloware.jukes.gui.tool.Settings;
54  import com.melloware.jukes.gui.view.component.ComponentFactory;
55  import com.melloware.jukes.gui.view.component.MessageCellRenderer;
56  import com.melloware.jukes.util.GuiUtil;
57  import com.melloware.jukes.util.JukesValidationMessage;
58  import com.melloware.jukes.util.MessageUtil;
59  
60  /**
61   * Searches an entire directory and subdiretories looking for all music folders
62   * and adding them to the catalog. If any error occurs the dir is skipped and
63   * the next one processed. Icons are used in the JList to display whether a
64   * success or failure adding this directory occured.
65   * <p>
66   * Copyright (c) 1999-2007 Melloware, Inc. <http://www.melloware.com>
67   * @author Emil A. Lefkof III <info@melloware.com>
68   * @version 4.0
69   */
70  @SuppressWarnings("unchecked")
71  public final class DiscFindDialog extends AbstractDialog {
72  
73     private static final Log LOG = LogFactory.getLog(DiscFindDialog.class);
74     private static final String HQL_DISC_LOCATIONS = ResourceUtils.getString("hql.disc.locations");
75     private final Map mapDiscs = new HashMap();
76     private DefaultListModel listModel;
77     private JButton buttonSave;
78     private JButton buttonApply;
79     private JButton buttonCancel;
80     private JButton buttonClose;
81     private JComponent buttonBar;
82     private JList list;
83     private JPanel panel;
84     private JLabel currentDirectory;
85     private JTextComponent directory;
86     private final Settings settings;
87     private Worker worker;
88     private static boolean closeDialog = false;
89  
90     /**
91      * Constructs a default about dialog using the given owner.
92      * @param owner the dialog's owner
93      */
94     public DiscFindDialog(Frame owner, Settings settings) {
95        super(owner);
96        LOG.debug("Disc Finder created.");
97        this.settings = settings;
98  
99        // since we are doing a find we want to compact the database on shutdown
100       HibernateUtil.setCompact(true);
101 
102       // load the discs into a map for fast access
103       loadAllDiscs();
104    }
105 
106    public DiscFindDialog(Frame owner, Settings settings, boolean startFinder) {
107       super(owner);
108       LOG.debug("Disc Finder created.");
109       DiscFindDialog.closeDialog = startFinder;
110       this.settings = settings;
111 
112       // since we are doing a find we want to compact the database on shutdown
113       HibernateUtil.setCompact(true);
114 
115       // load the discs into a map for fast access
116       loadAllDiscs();
117 
118       if (startFinder) {
119          this.build();
120          doApply();
121       }
122    }
123 
124    /*
125     * (non-Javadoc)
126     * @see com.jgoodies.swing.AbstractDialog#doApply()
127     */
128    public void doApply() {
129       LOG.debug("Apply pressed.");
130       buttonCancel.setEnabled(true);
131       buttonApply.setEnabled(false);
132       buttonClose.setEnabled(false);
133       buttonSave.setEnabled(false);
134       GuiUtil.setBusyCursor(this, true);
135       LOG.info("[START] Disc Finder");
136 
137       /*
138        * Invoking start() on the SwingWorker causes a new Thread to be created
139        * that will call construct(), and then finished(). Note that finished()
140        * is called even if the worker is interrupted because we catch the
141        * InterruptedException in doWork().
142        */
143       worker = new Worker() {
144          public Object construct() {
145             return doWork();
146          }
147 
148          public void finished() {
149             if (closeDialog) {
150                ActionManager.get(Actions.REFRESH_ID).actionPerformed(null);
151                LOG.info("[CLOSE] Disc Finder");
152                dispose();
153             } else {
154                threadFinished(get());
155             }
156          }
157       };
158       worker.start();
159 
160    }
161 
162    /*
163     * (non-Javadoc)
164     * @see com.jgoodies.swing.AbstractDialog#doCancel()
165     */
166    public void doCancel() {
167       LOG.debug("Cancel Pressed.");
168       if (worker != null) {
169          worker.interrupt();
170       }
171       buttonCancel.setEnabled(false);
172       buttonApply.setEnabled(true);
173       buttonClose.setEnabled(true);
174       buttonSave.setEnabled(true);
175    }
176 
177    /**
178     * Builds and answers the dialog's content.
179     * @return the dialog's content with tabbed pane and button bar
180     */
181    protected JComponent buildContent() {
182       final JPanel content = new JPanel(new BorderLayout());
183       content.add(buildMainPanel(), BorderLayout.CENTER);
184       content.add(buttonBar, BorderLayout.SOUTH);
185       return content;
186    }
187 
188    /**
189     * Builds and returns the dialog's header.
190     * @return the dialog's header component
191     */
192    protected JComponent buildHeader() {
193       return new HeaderPanel("Disc Finder ", "Pick a root directory and this will attempt to add all discs found\n"
194                + "under the directory specified  Please be patient this could take a long time.\n",
195                Resources.DISC_FINDER_ICON);
196    }
197 
198    /**
199     * Builds and returns the dialog's pane.
200     * @return the dialog's pane component
201     */
202    protected JComponent buildMainPanel() {
203       final JButton[] buttons = new JButton[4];
204       final JButton button = createApplyButton();
205       button.setText("Find");
206       final Action export = new AbstractAction("Save Error Report", Resources.FILE_TEXT_ICON) {
207          // This method is called when the button is pressed
208          public void actionPerformed(ActionEvent evt) {
209             saveErrorReport(evt);
210          }
211       };
212       buttonSave = new JButton(export);
213       buttonCancel = createCancelButton();
214       buttonApply = button;
215       buttonClose = createCloseButton(true);
216       buttonCancel.setEnabled(false);
217       buttons[0] = buttonApply;
218       buttons[1] = buttonCancel;
219       buttons[2] = buttonSave;
220       buttons[3] = buttonClose;
221       buttonBar = ButtonBarFactory.buildRightAlignedBar(buttons);
222       final FormLayout layout = new FormLayout("fill:pref:grow", "p, p, p, p");
223       final PanelBuilder builder = new PanelBuilder(layout);
224       builder.setDefaultDialogBorder();
225       final CellConstraints cc = new CellConstraints();
226       int row = 1;
227       builder.addSeparator("Find", cc.xy(1, row++));
228       builder.add(buildDirectoryPanel(), cc.xy(1, row++));
229       builder.add(buildListPanel(), cc.xy(1, row));
230       panel = builder.getPanel();
231       panel.setBorder(Borders.DIALOG_BORDER);
232       return panel;
233    }
234 
235    /**
236     * Saves the error report to a TXT file.
237     * <p>
238     * @param aEvt the event fired
239     */
240    protected void saveErrorReport(final ActionEvent aEvt) {
241       LOG.debug("Save Error Report pressed.");
242       final JFileChooser chooser = new JFileChooser();
243       chooser.setDialogTitle("Save Report");
244 
245       chooser.setFileFilter(FilterFactory.textFileFilter());
246       chooser.setMultiSelectionEnabled(false);
247       chooser.setFileHidingEnabled(true);
248       final int returnVal = chooser.showSaveDialog(this);
249       if (returnVal != JFileChooser.APPROVE_OPTION) {
250          return;
251       }
252       File file = chooser.getSelectedFile();
253       if (LOG.isDebugEnabled()) {
254          LOG.debug("Absolute: " + file.getAbsolutePath());
255       }
256 
257       // add the extension if missing
258       file = FilterFactory.forceTextExtension(file);
259 
260       try {
261          // now print errors and warns to the file
262          final ArrayList results = new ArrayList();
263          final Enumeration enumeration = listModel.elements();
264          while (enumeration.hasMoreElements()) {
265             final JukesValidationMessage message = (JukesValidationMessage) enumeration.nextElement();
266             if ((message.severity() == Severity.ERROR) || (message.severity() == Severity.WARNING)) {
267                // formatted Text contains the directory
268                results.add("DIR: " + message.formattedText());
269                // tooltip contains the error
270                results.add(message.severity().toString().toUpperCase() + ": " + message.getToolTip());
271                // add spacer between directories
272                results.add("  ");
273             }
274          }
275 
276          FileUtils.writeLines(file, null, results);
277 
278          MessageUtil.showInformation(this, "Report saved successfully.");
279       } catch (IOException ex) {
280          LOG.error("Error writing file: \n\n" + ex.getMessage(), ex);
281       } catch (InfrastructureException ex) {
282          LOG.error("Error writing file: \n\n" + ex.getMessage(), ex);
283       } catch (Exception ex) {
284          LOG.error("Unexpected error writing file.", ex);
285       }
286    }
287 
288    /*
289     * (non-Javadoc)
290     * @see com.jgoodies.swing.AbstractDialog#doCloseWindow()
291     */
292    protected void doCloseWindow() {
293       super.doClose();
294    }
295 
296    /**
297     * Builds the directory selection panel.
298     * <p>
299     * @return the panel used to select the directory.
300     */
301    private JComponent buildDirectoryPanel() {
302       directory = new JTextField();
303       currentDirectory = new JLabel();
304       ((JTextField) directory).setColumns(50);
305       directory.setText(this.settings.getStartInDirectory().getAbsolutePath());
306       final FormLayout layout = new FormLayout(
307                "right:max(14dlu;pref), 4dlu, left:min(60dlu;pref):grow, pref, 40dlu, ,pref, pref", "p, 4px, p, 4px"); // extra
308                                                                                                                         // bottom
309                                                                                                                         // space
310                                                                                                                         // for
311                                                                                                                         // icons
312 
313       final PanelBuilder builder = new PanelBuilder(layout);
314       final CellConstraints cc = new CellConstraints();
315 
316       builder.addLabel("Search Directory:", cc.xy(1, 1));
317       builder.add(directory, cc.xyw(3, 1, 3));
318       builder.add(ComponentFactory.createDirectoryChooserButton(directory), cc.xy(6, 1));
319       builder.addLabel("Processing:", cc.xy(1, 3));
320       builder.add(currentDirectory, cc.xyw(3, 3, 3));
321 
322       return builder.getPanel();
323    }
324 
325    /**
326     * Builds the message list panel.
327     * <p>
328     * @return the panel used to display messages
329     */
330    private JComponent buildListPanel() {
331       listModel = new DefaultListModel();
332       // Create the list and put it in a scroll pane.
333       list = new JList(listModel);
334       list.setFocusable(false);
335       list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
336       list.setSelectedIndex(0);
337       list.setCellRenderer(new MessageCellRenderer());
338       list.setVisibleRowCount(19);
339       return new JScrollPane(list);
340    }
341 
342    /**
343     * This method represents the application code that we'd like to run on a
344     * separate thread.
345     */
346    private Object doWork() {
347       Object result = null;
348       try {
349          // clear all old elements out
350          listModel.removeAllElements();
351 
352          // get the directory from the text box
353          final String directory = this.directory.getText();
354          final File dir = new File(directory);
355          // make sure it is a directory
356          if ((!dir.isDirectory()) || (!dir.exists())) {
357             LOG.error("Please select a valid directory");
358             throw new InterruptedException();
359          }
360          // now recursively look for all MP3 directories
361          recurseDirectories(dir);
362 
363          if (Thread.interrupted()) {
364             LOG.debug("Thread interrupted.");
365             throw new InterruptedException();
366          }
367 
368       } catch (InterruptedException e) {
369          return result; // SwingWorker.get() returns this
370       }
371       return result; // or this
372    }
373 
374    /**
375     * Asks the data store if we already have added this disc by checking if a
376     * music file from this directory already exists.
377     * <p>
378     * @param aDirectory the path of the directory to check.
379     * @return true if we already have this, false if not
380     */
381    private boolean hasDiscAlready(final String aDirectory) {
382       return mapDiscs.containsKey(aDirectory);
383    }
384 
385    /**
386     * Puts all disc locations into a Map for fast access.
387     */
388    private void loadAllDiscs() {
389       final Collection discs = HibernateDao.findByQuery(HQL_DISC_LOCATIONS);
390 
391       for (final Iterator iter = discs.iterator(); iter.hasNext();) {
392          final Object queryResult = (Object) iter.next();
393          mapDiscs.put(queryResult, queryResult);
394       }
395    }
396 
397    /**
398     * A recursive subroutine that lists the contents of the directory dir,
399     * including the contents of its subdirectories to any level of nesting. It
400     * is assumed that dir is in fact a directory.
401     * <p>
402     * @param aDirectory the directory to recurse
403     * @throws InterruptedException if the thread is Interrupted stop processing
404     */
405    private void recurseDirectories(final File aDirectory) throws InterruptedException {
406       final String[] files = aDirectory.list(DirectoryFileFilter.INSTANCE);
407 
408       if (Thread.interrupted()) {
409          LOG.debug("Thread interrupted.");
410          throw new InterruptedException();
411       }
412 
413       // if there are more directories found recurse them
414       if (files.length > 0) {
415          for (int i = 0; i < files.length; i++) {
416             final File f = new File(aDirectory, files[i]);
417             if (f.isDirectory()) {
418                recurseDirectories(f);
419             }
420          }
421       } else {
422          // else this is a bottome level node, check for music files
423          updateProcessing(aDirectory.getAbsolutePath());
424          if (hasDiscAlready(aDirectory.getAbsolutePath())) {
425             return;
426          }
427          final JukesValidationMessage message = MusicDirectory.loadDiscFromDirectory(aDirectory, this.settings
428                   .isUpdateTags());
429          if (message.getSeverity() == Severity.OK) {
430             message.setMessage(aDirectory.getAbsolutePath());
431          } else if (message.getSeverity() == Severity.WARNING) {
432             message.setMessage(aDirectory.getAbsolutePath());
433          } else {
434             message.setMessage("ERROR " + aDirectory.getAbsolutePath());
435          }
436          updateList(message);
437       }
438    }
439 
440    /**
441     * When the thread is finished this method is called.
442     * <p>
443     * @param result the Object return from the doWork thread.
444     */
445    private void threadFinished(Object result) {
446       if (LOG.isDebugEnabled()) {
447          LOG.debug("Thread Finished");
448          LOG.debug(result);
449       }
450       GuiUtil.setBusyCursor(this, false);
451       buttonCancel.setEnabled(false);
452       buttonApply.setEnabled(true);
453       buttonClose.setEnabled(true);
454       buttonSave.setEnabled(true);
455 
456       // refresh the tree
457       ActionManager.get(Actions.REFRESH_ID).actionPerformed(null);
458       LOG.info("[STOP] Disc Finder");
459    }
460 
461    /**
462     * When the worker needs to update the GUI we do so by queuing a Runnable for
463     * the event dispatching thread with SwingUtilities.invokeLater(). In this
464     * case we're just changing the progress bars value.
465     */
466    private void updateList(final JukesValidationMessage aMessage) {
467       final Runnable updateList = new Runnable() {
468          public void run() {
469             listModel.addElement(aMessage);
470             final int index = listModel.indexOf(aMessage);
471             list.setSelectedIndex(index);
472             list.ensureIndexIsVisible(index);
473          }
474       };
475       EventQueue.invokeLater(updateList);
476    }
477 
478    /**
479     * When the worker needs to update the GUI we do so by queuing a Runnable for
480     * the event dispatching thread with SwingUtilities.invokeLater(). In this
481     * case we're just changing the progress bars value.
482     */
483    private void updateProcessing(final String aDirectory) {
484       final Runnable updateList = new Runnable() {
485          public void run() {
486             currentDirectory.setText(aDirectory);
487          }
488       };
489       EventQueue.invokeLater(updateList);
490    }
491 
492 }