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