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