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.lang.reflect.InvocationTargetException;
10  import java.util.ArrayList;
11  import java.util.Collection;
12  import java.util.Collections;
13  import java.util.Enumeration;
14  import java.util.HashMap;
15  import java.util.Iterator;
16  import java.util.List;
17  import java.util.Locale;
18  import java.util.Map;
19  
20  import javax.swing.AbstractAction;
21  import javax.swing.Action;
22  import javax.swing.DefaultListModel;
23  import javax.swing.JButton;
24  import javax.swing.JCheckBox;
25  import javax.swing.JComponent;
26  import javax.swing.JFileChooser;
27  import javax.swing.JList;
28  import javax.swing.JPanel;
29  import javax.swing.JProgressBar;
30  import javax.swing.JScrollPane;
31  import javax.swing.JTextField;
32  import javax.swing.ListSelectionModel;
33  import javax.swing.text.JTextComponent;
34  
35  import org.apache.commons.collections.list.SetUniqueList;
36  import org.apache.commons.io.FileUtils;
37  import org.apache.commons.lang.StringUtils;
38  import org.apache.commons.logging.Log;
39  import org.apache.commons.logging.LogFactory;
40  
41  import com.jgoodies.forms.builder.PanelBuilder;
42  import com.jgoodies.forms.factories.Borders;
43  import com.jgoodies.forms.factories.ButtonBarFactory;
44  import com.jgoodies.forms.layout.CellConstraints;
45  import com.jgoodies.forms.layout.FormLayout;
46  import com.jgoodies.uif.AbstractDialog;
47  import com.jgoodies.uif.util.Worker;
48  import com.jgoodies.uifextras.panel.HeaderPanel;
49  import com.jgoodies.validation.Severity;
50  import com.jgoodies.validation.ValidationMessage;
51  import com.melloware.jukes.db.HibernateDao;
52  import com.melloware.jukes.exception.InfrastructureException;
53  import com.melloware.jukes.file.filter.FilterFactory;
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   * Compares a text file list of a catalog against the current catalog loaded
64   * in the database.  Useful for comparing what one catalog has that the other
65   * one doesn't or what they have in common.
66   * <p>
67   * Copyright (c) 1999-2007 Melloware, Inc. <http://www.melloware.com>
68   * @author Emil A. Lefkof III <info@melloware.com>
69   * @version 4.0
70   */
71  @SuppressWarnings("unchecked")
72  public final class DifferenceToolDialog
73      extends AbstractDialog {
74  
75      private static final Log LOG = LogFactory.getLog(DifferenceToolDialog.class);
76      private static final String BREAK = " - ";
77      private final Map mapDiscs = new HashMap();
78      private DefaultListModel listModel;
79      private JButton buttonCancel;
80      private JButton buttonClose;
81      private JButton buttonDiff;
82      private JButton buttonIntersection;
83      private JButton buttonSave;
84      private JButton buttonUnion;
85      private JComponent buttonBar;
86      private JList list;
87      private JPanel panel;
88      private JProgressBar progressBar;
89      private JTextComponent file;
90      private JCheckBox caseSensitive;
91      private final Settings settings;
92      private Worker worker;
93  
94      /**
95       * Constructs a default about dialog using the given owner.
96       *
97       * @param owner   the dialog's owner
98       */
99      public DifferenceToolDialog(Frame owner, Settings settings) {
100         super(owner);
101         LOG.debug("Difference Tool created.");
102         this.settings = settings;
103         this.settings.getStartInDirectory();
104     }
105 
106     /* (non-Javadoc)
107      * @see com.jgoodies.swing.AbstractDialog#doCancel()
108      */
109     public void doCancel() {
110         LOG.debug("Cancel Pressed.");
111         if (worker != null) {
112             worker.interrupt();
113         }
114     }
115 
116     /**
117      * Builds and answers the dialog's content.
118      *
119      * @return the dialog's content with tabbed pane and button bar
120      */
121     protected JComponent buildContent() {
122         JPanel content = new JPanel(new BorderLayout());
123         content.add(buildMainPanel(), BorderLayout.CENTER);
124         content.add(buttonBar, BorderLayout.SOUTH);
125         return content;
126     }
127 
128     /**
129      * Builds and returns the dialog's header.
130      *
131      * @return the dialog's header component
132      */
133     protected JComponent buildHeader() {
134         return new HeaderPanel("Difference Analyzer Tool ",
135                                "Select an exported catalog text file and this tool will \n"
136                                + "compare all differences, unions, or intersections with the current catalog.\n",
137                                Resources.DIFFERENCE_TOOL_ICON);
138     }
139 
140     /**
141      * Builds and returns the dialog's pane.
142      *
143      * @return the dialog's  pane component
144      */
145     protected JComponent buildMainPanel() {
146         // Create an action with an icon
147         Action diff = new AbstractAction("Differences", Resources.DIFFERENCE_TOOL_DIFF_ICON) {
148             // This method is called when the button is pressed
149             public void actionPerformed(ActionEvent evt) {
150                 doCompare(evt);
151             }
152         };
153         Action intersection = new AbstractAction("Intersection", Resources.DIFFERENCE_TOOL_INTERSECTION_ICON) {
154             // This method is called when the button is pressed
155             public void actionPerformed(ActionEvent evt) {
156                 doCompare(evt);
157             }
158         };
159         Action union = new AbstractAction("Union", Resources.DIFFERENCE_TOOL_UNION_ICON) {
160             // This method is called when the button is pressed
161             public void actionPerformed(ActionEvent evt) {
162                 doCompare(evt);
163             }
164         };
165         Action export = new AbstractAction("Save", Resources.FILE_TEXT_ICON) {
166             // This method is called when the button is pressed
167             public void actionPerformed(ActionEvent evt) {
168                 save(evt);
169             }
170         };
171         buttonSave = new JButton(export);
172         buttonDiff = new JButton(diff);
173         buttonIntersection = new JButton(intersection);
174         buttonUnion = new JButton(union);
175         buttonCancel = createCancelButton();
176         buttonClose = createCloseButton(true);
177         buttonCancel.setEnabled(false);
178         JButton[] buttons = new JButton[6];
179         buttons[0] = buttonDiff;
180         buttons[1] = buttonIntersection;
181         buttons[2] = buttonUnion;
182         buttons[3] = buttonSave;
183         buttons[4] = buttonCancel;
184         buttons[5] = buttonClose;
185         buttonBar = ButtonBarFactory.buildCenteredBar(buttons);
186         FormLayout layout = new FormLayout("fill:pref:grow", "p, p, p, p, p, p, p, p");
187         PanelBuilder builder = new PanelBuilder(layout);
188         builder.setDefaultDialogBorder();
189         CellConstraints cc = new CellConstraints();
190         int row = 1;
191         builder.addSeparator("Find", cc.xy(1, row++));
192         builder.add(buildFilePanel(), cc.xy(1, row++));
193         builder.add(buildProgressPanel(), cc.xy(1, row++));
194         builder.add(buildOptionsPanel(), cc.xy(1, row++));
195         builder.add(buildListPanel(), cc.xy(1, 8));
196         panel = builder.getPanel();
197         panel.setBorder(Borders.DIALOG_BORDER);
198         return panel;
199     }
200 
201     /* (non-Javadoc)
202      * @see com.jgoodies.swing.AbstractDialog#doCloseWindow()
203      */
204     protected void doCloseWindow() {
205         super.doClose();
206     }
207 
208     /**
209     * Performs the comparison in a thread.
210     *
211     * @param aEvt the ActionEvent fired
212     */
213     protected void doCompare(ActionEvent aEvt) {
214         LOG.debug(aEvt.getActionCommand());
215 
216         final String command = aEvt.getActionCommand();
217         buttonCancel.setEnabled(true);
218         buttonDiff.setEnabled(false);
219         buttonUnion.setEnabled(false);
220         buttonIntersection.setEnabled(false);
221         buttonClose.setEnabled(false);
222         buttonSave.setEnabled(false);
223         GuiUtil.setBusyCursor(this, true);
224 
225         /* Invoking start() on the SwingWorker causes a new Thread
226          * to be created that will call construct(), and then finished().  Note that finished() is called even if the
227          * worker is interrupted because we catch the InterruptedException in doWork().
228          */
229         worker = new Worker() {
230                 public Object construct() {
231                     return doWork(command);
232                 }
233 
234                 public void finished() {
235                     threadFinished(get());
236                 }
237             };
238         worker.start();
239 
240     }
241 
242     /**
243     * Resizes the given component to give it a quadratic aspect ratio.
244     *
245     * @param component   the component to be resized
246     */
247     protected void resizeHook(JComponent component) {
248         //Resizer.ONE2ONE.resizeDialogContent(component);
249     }
250 
251     /**
252      * Saves the list to a text file.
253      * <p>
254      * @param aEvt the ActionEvent fired.
255      */
256     protected void save(ActionEvent aEvt) {
257         LOG.debug("Save pressed.");
258         JFileChooser chooser = new JFileChooser();
259         chooser.setDialogTitle("Save Report");
260 
261         chooser.setFileFilter(FilterFactory.textFileFilter());
262         chooser.setMultiSelectionEnabled(false);
263         chooser.setFileHidingEnabled(true);
264         final int returnVal = chooser.showSaveDialog(this);
265         if (returnVal != JFileChooser.APPROVE_OPTION) {
266             return;
267         }
268         File file = chooser.getSelectedFile();
269         if (LOG.isDebugEnabled()) {
270             LOG.debug("Absolute: " + file.getAbsolutePath());
271         }
272 
273         // add the extension if missing
274         file = FilterFactory.forceTextExtension(file);
275 
276         try {
277             // now query the catalog and save the results to the file
278             ArrayList results = new ArrayList();
279             Enumeration enumeration = listModel.elements();
280             while (enumeration.hasMoreElements()) {
281                 ValidationMessage message = (ValidationMessage)enumeration.nextElement();
282                 results.add(message.formattedText());
283             }
284 
285             FileUtils.writeLines(file, null, results);
286 
287             MessageUtil.showInformation(this, "Report saved successfully.");
288         } catch (IOException ex) {
289             LOG.error("Error writing file: \n\n" + ex.getMessage(), ex);
290         } catch (InfrastructureException ex) {
291             LOG.error("Error writing file: \n\n" + ex.getMessage(), ex);
292         } catch (Exception ex) {
293             LOG.error("Unexpected error writing file.", ex);
294         }
295     }
296 
297     /**
298      * Finds if we have this disc in the database by artist and disc name.
299      * <p>
300      * @param aArtistName the artist name to find
301      * @param aDiscName the disc name to find
302      * @return true if we already have this, false if not
303      */
304     private boolean isHashMatch(String aArtistName, String aDiscName) {
305         final String key = aArtistName + BREAK + aDiscName;
306         return mapDiscs.containsKey(key.toUpperCase(Locale.US));
307     }
308 
309     /**
310      * Builds the file selection panel.
311      * <p>
312      * @return the panel used to select the file.
313      */
314     private JComponent buildFilePanel() {
315         file = new JTextField();
316         ((JTextField)file).setColumns(50);
317         file.setText("");
318         final FormLayout layout = new FormLayout("right:max(14dlu;pref), 4dlu, left:min(60dlu;pref):grow, pref, 40dlu, ,pref, pref",
319                                            "p");
320         final PanelBuilder builder = new PanelBuilder(layout);
321         final CellConstraints cc = new CellConstraints();
322 
323         builder.addLabel("Select File:", cc.xy(1, 1));
324         builder.add(file, cc.xyw(3, 1, 3));
325         builder.add(ComponentFactory.createFileChooserButton(file, "Select Catalog", FilterFactory.textFileFilter()),
326                     cc.xy(6, 1));
327 
328         return builder.getPanel();
329     }
330 
331     /**
332      * Builds the message list panel.
333      * <p>
334      * @return the panel used to display messages
335      */
336     private JComponent buildListPanel() {
337         listModel = new DefaultListModel();
338         // Create the list and put it in a scroll pane.
339         list = new JList(listModel);
340         list.setFocusable(false);
341         list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
342         list.setSelectedIndex(0);
343         list.setCellRenderer(new MessageCellRenderer());
344         list.setVisibleRowCount(18);
345         return new JScrollPane(list);
346     }
347 
348     /**
349      * Builds the progressbar panel.
350      * <p>
351      * @return the panel used to select the directory.
352      */
353     private JComponent buildProgressPanel() {
354         progressBar = new JProgressBar();
355         progressBar.setIndeterminate(false);
356         progressBar.setStringPainted(true);
357         final FormLayout layout = new FormLayout("right:max(14dlu;pref), 4dlu, fill:pref:grow", "p"); 
358         final PanelBuilder builder = new PanelBuilder(layout);
359         final CellConstraints cc = new CellConstraints();
360 
361         builder.addLabel("Progress:", cc.xy(1, 1));
362         builder.add(progressBar, cc.xy(3, 1));
363 
364         return builder.getPanel();
365     }
366 
367     /**
368      * Builds the options panel.
369      * <p>
370      * @return the panel containing any options
371      */
372     private JComponent buildOptionsPanel() {
373         caseSensitive = new JCheckBox();
374         caseSensitive.setSelected(true);
375         final FormLayout layout = new FormLayout("right:max(14dlu;pref), 4dlu, fill:pref:grow", "p, 4px, p");    // extra bottom space for icons
376 
377         layout.setRowGroups(new int[][] {
378                                 { 1, 3 }
379                             });
380         final PanelBuilder builder = new PanelBuilder(layout);
381         final CellConstraints cc = new CellConstraints();
382 
383         builder.addLabel("Case Sensitive:", cc.xy(1, 1));
384         builder.add(caseSensitive, cc.xy(3, 1));
385 
386         return builder.getPanel();
387     }
388     
389     /**
390      * Compares the text catalog to the database for differences
391      * <p>
392      * @param aDirectory the file to recurse
393      * @throws InterruptedException if the thread is Interrupted stop processing
394      */
395     private void comparison(File aFile, String aCommand)
396                      throws InterruptedException {
397 
398         // build a hasmap of discs for fast searching
399         final Collection allDiscs = loadAllDiscs();
400         for (Iterator iter = allDiscs.iterator(); iter.hasNext();) {
401             final String key = (String)iter.next();
402             mapDiscs.put(key.toUpperCase(Locale.US), key);
403         }
404 
405         // load the file as Strings
406         Collection lines;
407         List unionList = new ArrayList();
408         try {
409             lines = FileUtils.readLines(aFile, null);
410             
411             if (!caseSensitive.isSelected()) {
412                List ucList = new ArrayList();
413                final Iterator itLines = lines.iterator();
414                while (itLines.hasNext()) {
415                   String element = (String) itLines.next();
416                   ucList.add(element.toUpperCase());
417                }
418                lines = ucList;
419             }
420 
421             int counter = 0;
422             resetProgressBar(lines.size());
423 
424             for (Iterator iter = lines.iterator(); iter.hasNext();) {
425                 counter++;
426                 String line = (String)iter.next();
427                 if (Thread.interrupted()) {
428                     LOG.debug("Thread interrupted.");
429                     throw new InterruptedException();
430                 }
431                 final String[] value = StringUtils.split(line, Resources.TAB);
432                 final String artist = value[0];
433                 final String disc = value[1];
434                 final String discString = artist + BREAK + disc;
435                 if ("Differences".equals(aCommand)) {
436                     if (!isHashMatch(artist, disc)) {
437                         final String output = "TAKE " + discString;
438                         final String message = output;
439                         final Severity severity = Severity.WARNING;
440                         updateList(message, severity, counter);
441                     }
442                 } else if ("Intersection".equals(aCommand)) {
443                     if (isHashMatch(artist, disc)) {
444                         final String message = discString;
445                         final Severity severity = Severity.OK;
446                         updateList(message, severity, counter);
447                     }
448                 } else if ("Union".equals(aCommand)) {
449                     unionList.add(discString);
450                 } else {
451                     throw new InterruptedException("Not a valid command button.");
452                 }
453             }
454             
455             progressBar.setValue(counter);
456 
457             // now get the differences the other way
458             if ("Differences".equals(aCommand)) {
459                 // loop through all discs in the catalog and look in file
460                 final Collection discs = loadAllDiscs();
461                 counter = 0;
462                 resetProgressBar(discs.size());
463                 for (Iterator iter = discs.iterator(); iter.hasNext();) {
464                     counter++;
465                     final String disc = (String)iter.next();
466                     String line = StringUtils.replace(disc, BREAK, Resources.TAB, 1);
467                     if (!lines.contains(line)) {
468                         final String message = "GIVE " + disc;
469                         final Severity severity = Severity.OK;
470                         updateList(message, severity, counter);
471                     }
472                 }
473                 progressBar.setValue(counter);
474             }
475 
476             // now union the current catalog into it
477             if ("Union".equals(aCommand)) {
478                 final Collection discs = loadAllDiscs();
479 
480                 for (Iterator iter = discs.iterator(); iter.hasNext();) {
481                     String disc = (String)iter.next();
482                     unionList.add(disc);
483                 }
484                 Collections.sort(unionList);
485                 final SetUniqueList uniqueList = SetUniqueList.decorate(unionList);
486                 counter = 0;
487                 resetProgressBar(uniqueList.size());
488                 for (Iterator iter = uniqueList.iterator(); iter.hasNext();) {
489                     counter++;
490                     String element = (String)iter.next();
491                     final Severity severity = Severity.OK;
492                     updateList(element, severity, counter);
493                 }
494                 progressBar.setValue(counter);
495             }
496             
497         } catch (IOException ex) {
498             final String error = "Error reading file: " + ex.getMessage();
499             LOG.error(error);
500             throw new InterruptedException(error);
501         } catch (InfrastructureException ex) {
502             final String error = "Error querying database: " + ex.getMessage();
503             LOG.error(error);
504             throw new InterruptedException(error);
505         } catch (Exception ex) {
506             final String error = "Error reading file: " + ex.getMessage();
507             LOG.error(error);
508             throw new InterruptedException(error);
509         }
510     }
511 
512     /**
513      * This method represents the application code that we'd like to
514      * run on a separate thread.
515      */
516     private Object doWork(String aCommand) {
517         Object result = null;
518 
519         try {
520 
521             // clear all old elements out
522             listModel.removeAllElements();
523 
524             // get the file from the text box
525             String filename = this.file.getText();
526             File file = new File(filename);
527             // make sure it is a file
528             if (!file.exists()) {
529                 LOG.error("Please select a valid file");
530                 throw new InterruptedException();
531             }
532 
533             // now do differences
534             comparison(file, aCommand);
535 
536             if (Thread.interrupted()) {
537                 LOG.debug("Thread interrupted.");
538                 throw new InterruptedException();
539             }
540 
541         } catch (InterruptedException e) {
542             return result;    // SwingWorker.get() returns this
543         }
544         return result;    // or this
545     }
546 
547     /**
548      * Speadily loads all discs sorted by artist and disc.  Puts them into a
549      * collection as "Artist - Disc".
550      * <p>
551      * @return the collection of discs strings
552      */
553     private Collection loadAllDiscs() {
554         ArrayList results = new ArrayList();
555         Collection discs = HibernateDao.findByQuery(Resources.getString("hql.export.catalog"));
556 
557         for (Iterator iter = discs.iterator(); iter.hasNext();) {
558             Object[] queryResult = (Object[])iter.next();
559 
560             // Artist - Disc
561             String disc = queryResult[0] + BREAK + queryResult[1];
562             if (!caseSensitive.isSelected()) {
563                disc = disc.toUpperCase();
564             } 
565             results.add(disc);
566         }
567         return results;
568     }
569 
570     /**
571      * Resets the progress bar and gives it a max progress.
572      * <p>
573      * @param aMaxProgress the maximum value of the progress bar.
574      */
575     private void resetProgressBar(int aMaxProgress) {
576         progressBar.setMaximum(aMaxProgress);
577         progressBar.setValue(0);
578         progressBar.setIndeterminate(false);
579     }
580 
581     /**
582      * When the thread is finished this method is called.
583      * <p>
584      * @param result the Object return from the doWork thread.
585      */
586     private void threadFinished(Object result) {
587     	if (LOG.isDebugEnabled()) {
588         	LOG.debug("Thread Finished");
589 			LOG.debug(result);
590 		}
591         GuiUtil.setBusyCursor(this, false);
592         buttonCancel.setEnabled(false);
593         buttonDiff.setEnabled(true);
594         buttonUnion.setEnabled(true);
595         buttonIntersection.setEnabled(true);
596         buttonClose.setEnabled(true);
597         buttonSave.setEnabled(true);
598     }
599 
600     /**
601      * When the worker needs to update the GUI we do so by queuing
602      * a Runnable for the event dispatching thread with
603      * SwingUtilities.invokeLater().  In this case we're just
604      * changing the progress bars value.
605      * @throws InvocationTargetException
606      * @throws InterruptedException
607      */
608     private void updateList(final String aMessage, final Severity aSeverity, final int aProgress)
609                      throws InterruptedException, InvocationTargetException {
610         Runnable updateList = new Runnable() {
611             public void run() {
612                 progressBar.setValue(aProgress);
613                 ValidationMessage message = new JukesValidationMessage(aMessage, aSeverity);
614                 listModel.addElement(message);
615                 int index = listModel.indexOf(message);
616                 list.setSelectedIndex(index);
617                 list.ensureIndexIsVisible(index);
618             }
619         };
620         EventQueue.invokeAndWait(updateList);
621     }
622 
623 }