changeset 1155:c70ab9ec00f5

Preliminary add export function to HeapDump review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-June/007216.html reviewed-by: vanaltj
author Mario Torre <neugens.limasoftware@gmail.com>
date Tue, 09 Jul 2013 15:10:03 +0200
parents ed7fab187035
children 2932f90a008f
files vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/HeapView.java vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpController.java vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/locale/LocaleResources.java vm-heap-analysis/client-core/src/main/resources/com/redhat/thermostat/vm/heap/analysis/client/locale/strings.properties vm-heap-analysis/client-core/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpControllerTest.java vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/HeapSwingView.java vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/ExportDumpEvent.java vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/ExportDumpListener.java vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/ExportDumpPopup.java vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/HeapSelectionEvent.java vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/OverlayComponent.java vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/StatsPanel.java vm-heap-analysis/client-swing/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/HeapSwingViewTest.java vm-heap-analysis/client-swing/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/OverlayComponentTest.java vm-heap-analysis/common/src/main/java/com/redhat/thermostat/vm/heap/analysis/common/DumpFile.java vm-heap-analysis/common/src/main/java/com/redhat/thermostat/vm/heap/analysis/common/HeapDump.java
diffstat 16 files changed, 680 insertions(+), 36 deletions(-) [+]
line wrap: on
line diff
--- a/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/HeapView.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/HeapView.java	Tue Jul 09 15:10:03 2013 +0200
@@ -44,6 +44,7 @@
 import com.redhat.thermostat.common.ActionNotifier;
 import com.redhat.thermostat.shared.locale.LocalizedString;
 import com.redhat.thermostat.vm.heap.analysis.client.core.chart.OverviewChart;
+import com.redhat.thermostat.vm.heap.analysis.common.DumpFile;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
 
 public abstract class HeapView extends BasicView implements UIComponent {
@@ -51,6 +52,8 @@
     public enum HeapDumperAction {
         DUMP_REQUESTED,
         ANALYSE,
+        REQUEST_EXPORT,
+        SAVE_HEAP_DUMP,
         REQUEST_ABORTED,
         REQUEST_DISPLAY_DUMP_LIST,
     }
@@ -92,5 +95,7 @@
     public abstract void displayWarning(LocalizedString string);
 
     public abstract void openDumpListView(HeapDumpListView childView);
+
+    public abstract void openExportDialog(DumpFile heapdDump);
 }
 
--- a/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpController.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpController.java	Tue Jul 09 15:10:03 2013 +0200
@@ -36,10 +36,18 @@
 
 package com.redhat.thermostat.vm.heap.analysis.client.core.internal;
 
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import com.redhat.thermostat.client.core.controllers.InformationServiceController;
 import com.redhat.thermostat.client.core.views.BasicView.Action;
@@ -67,6 +75,7 @@
 import com.redhat.thermostat.vm.heap.analysis.client.core.ObjectRootsViewProvider;
 import com.redhat.thermostat.vm.heap.analysis.client.core.chart.OverviewChart;
 import com.redhat.thermostat.vm.heap.analysis.client.locale.LocaleResources;
+import com.redhat.thermostat.vm.heap.analysis.common.DumpFile;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDAO;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
 import com.redhat.thermostat.vm.heap.analysis.common.model.HeapInfo;
@@ -201,6 +210,26 @@
                     dump = (HeapDump) actionEvent.getPayload();
                     analyseDump(dump);
                     break;
+                    
+                case REQUEST_EXPORT: {
+                    dump = (HeapDump) actionEvent.getPayload();
+                    DumpFile localHeapDump = new DumpFile();
+                    SimpleDateFormat format = new SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS");
+                    Date date = new Date(dump.getTimestamp());
+                    String timeStamp = format.format(date);
+                    String id = "heapdump-" + ref.getName() + "-" + timeStamp + "." + dump.getType();
+                    localHeapDump.setFile(new File(id));
+                    localHeapDump.setDump(dump);
+                    view.openExportDialog(localHeapDump);
+                } break;
+                
+                case SAVE_HEAP_DUMP: {
+                    // FIXME: we really need some indicator that something is
+                    // going on here, same for dumping requests
+                    DumpFile localHeapDump = (DumpFile) actionEvent.getPayload();
+                    saveHeapDump(localHeapDump);
+                } break;
+                
                 default:
                     break;
                 }
@@ -213,6 +242,31 @@
         }
     }
 
+    private void saveHeapDump(final DumpFile localHeapDump) {
+        appService.getApplicationExecutor().execute(new Runnable() {
+            @Override
+            public void run() {
+                HeapDump dump = localHeapDump.getDump();
+                File file = localHeapDump.getFile();
+                if (dump == null || file == null) {
+                    // this is here mainly for the tests, since we don't
+                    // expect files or dumps to be null
+                    return;
+                }
+                
+                try (InputStream in = heapDAO.getHeapDumpData(dump.getInfo())) {
+                    Files.copy(in, file.toPath());
+                    
+                } catch (IOException e) {
+                    LocalizedString message = translator.localize(LocaleResources.ERROR_EXPORTING_FILE);
+                    view.displayWarning(message);
+                    Logger.getLogger(HeapDumpController.class.getSimpleName()).
+                        log(Level.WARNING, message.getContents(), e);
+                }
+            }
+        });
+    }
+    
     private void openDumpList() {
         
         appService.getApplicationExecutor().execute(new Runnable() {
--- a/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/locale/LocaleResources.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/locale/LocaleResources.java	Tue Jul 09 15:10:03 2013 +0200
@@ -66,6 +66,9 @@
     HEAP_DUMP_OBJECT_BROWSE_REFERENCES,
     HEAP_DUMP_OBJECT_FIND_ROOT,
     
+    EXPORT_HEAP_DUMP_TO_FILE,
+    ERROR_EXPORTING_FILE,
+
     OBJECT_ROOTS_VIEW_TITLE,
 
     TRIGGER_HEAP_DUMP,
--- a/vm-heap-analysis/client-core/src/main/resources/com/redhat/thermostat/vm/heap/analysis/client/locale/strings.properties	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-core/src/main/resources/com/redhat/thermostat/vm/heap/analysis/client/locale/strings.properties	Tue Jul 09 15:10:03 2013 +0200
@@ -25,6 +25,9 @@
 HEAP_DUMP_OBJECT_BROWSE_REFERENCES = References
 HEAP_DUMP_OBJECT_FIND_ROOT = Find Root
 
+EXPORT_HEAP_DUMP_TO_FILE = Export...
+ERROR_EXPORTING_FILE = Error exporting Heap dump to file...
+
 OBJECT_ROOTS_VIEW_TITLE = Object Roots
 
 TRIGGER_HEAP_DUMP = Heap Dump
--- a/vm-heap-analysis/client-core/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpControllerTest.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-core/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpControllerTest.java	Tue Jul 09 15:10:03 2013 +0200
@@ -37,6 +37,8 @@
 package com.redhat.thermostat.vm.heap.analysis.client.core.internal;
 
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertEquals;
+
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anyLong;
 import static org.mockito.Matchers.isA;
@@ -48,6 +50,8 @@
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+import java.io.File;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -63,6 +67,7 @@
 import org.mockito.invocation.InvocationOnMock;
 import org.mockito.stubbing.Answer;
 
+import com.lowagie.text.pdf.codec.Base64.InputStream;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ApplicationCache;
@@ -88,6 +93,7 @@
 import com.redhat.thermostat.vm.heap.analysis.client.core.ObjectDetailsViewProvider;
 import com.redhat.thermostat.vm.heap.analysis.client.core.ObjectRootsView;
 import com.redhat.thermostat.vm.heap.analysis.client.core.ObjectRootsViewProvider;
+import com.redhat.thermostat.vm.heap.analysis.common.DumpFile;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDAO;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
 import com.redhat.thermostat.vm.heap.analysis.common.model.HeapInfo;
@@ -350,7 +356,55 @@
         verify(heapDumper).dump();
         verify(view).notifyHeapDumpComplete();
     }
+    
+    @Test
+    public void testRequestExport() throws CommandException, InterruptedException {
+        setUpListeners();
 
+        HeapDump dump = mock(HeapDump.class);
+        when(dump.getTimestamp()).thenReturn(1l);
+        when(dump.getType()).thenReturn("TEST");
+
+        ArgumentCaptor<DumpFile> localDumpCaptor = ArgumentCaptor.forClass(DumpFile.class);
+        
+        ActionEvent<HeapDumperAction> event = new ActionEvent<>(view, HeapDumperAction.REQUEST_EXPORT);
+        event.setPayload(dump);
+        
+        heapDumperListener.actionPerformed(event);
+        
+        verify(view).openExportDialog(localDumpCaptor.capture());
+        DumpFile localDump = localDumpCaptor.getValue();
+        
+        assertTrue(localDump.getFile().getName().endsWith(".TEST"));
+        assertEquals(dump, localDump.getDump());
+    }
+    
+    @Test
+    public void testExportLocation() throws CommandException, InterruptedException {
+        final CountDownLatch latch = new CountDownLatch(1);
+        mockExecutorService(latch);
+        
+        setUpListeners();
+
+        HeapInfo info = mock(HeapInfo.class);
+        HeapDump dump = mock(HeapDump.class);
+        when(dump.getInfo()).thenReturn(info);
+        DumpFile localDump = mock(DumpFile.class);
+        when(localDump.getDump()).thenReturn(dump);
+
+        InputStream stream = mock(InputStream.class);
+        when(heapDao.getHeapDumpData(any(HeapInfo.class))).thenReturn(stream);
+        
+        ActionEvent<HeapDumperAction> event = new ActionEvent<>(view, HeapDumperAction.SAVE_HEAP_DUMP);
+        event.setPayload(localDump);
+        
+        heapDumperListener.actionPerformed(event);
+        latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
+
+        verify(localDump).getFile();
+        verify(localDump).getDump();
+    }
+        
     @Test
     public void testRequestHeapDumpFails() throws CommandException, InterruptedException {
         final CountDownLatch latch = new CountDownLatch(1);
--- a/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/HeapSwingView.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/HeapSwingView.java	Tue Jul 09 15:10:03 2013 +0200
@@ -41,11 +41,13 @@
 import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.KeyEvent;
+import java.io.File;
 import java.util.List;
 
 import javax.swing.AbstractAction;
 import javax.swing.BoxLayout;
 import javax.swing.JComponent;
+import javax.swing.JFileChooser;
 import javax.swing.JOptionPane;
 import javax.swing.JPanel;
 import javax.swing.KeyStroke;
@@ -69,10 +71,13 @@
 import com.redhat.thermostat.vm.heap.analysis.client.core.HeapView;
 import com.redhat.thermostat.vm.heap.analysis.client.core.chart.OverviewChart;
 import com.redhat.thermostat.vm.heap.analysis.client.locale.LocaleResources;
+import com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats.ExportDumpEvent;
+import com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats.ExportDumpListener;
 import com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats.HeapChartPanel;
 import com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats.HeapDumpListener;
 import com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats.HeapSelectionEvent;
 import com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats.StatsPanel;
+import com.redhat.thermostat.vm.heap.analysis.common.DumpFile;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
 
 public class HeapSwingView extends HeapView implements SwingComponent {
@@ -92,6 +97,8 @@
     
     private JPanel stack;
     
+    private JFileChooser fileChooser;
+    
     public HeapSwingView() {
         stats = new StatsPanel();
         stats.addHeapDumperListener(new ActionListener() {
@@ -109,6 +116,14 @@
             }
         });
         
+        stats.addExportDumpListener(new ExportDumpListener() {
+            @Override
+            public void actionPerformed(ExportDumpEvent e) {
+                HeapDump dump = e.getSource();
+                heapDumperNotifier.fireAction(HeapDumperAction.REQUEST_EXPORT, dump);
+            }
+        });
+        
         visiblePane = new JPanel();
         visiblePane.setLayout(new BoxLayout(visiblePane, BoxLayout.X_AXIS));
         visiblePane.setName(HeapSwingView.class.getName());
@@ -173,6 +188,9 @@
         
         // at the beginning, only the overview is visible
         visiblePane.add(overview);
+        
+        fileChooser = new JFileChooser();
+        fileChooser.setName("EXPORT_HEAP_DUMP_FILE_CHOOSER");
     }
     
     private class ViewVisibleListener extends ComponentVisibleListener {
@@ -376,4 +394,20 @@
             });
         }
     }
+    
+    @Override
+    public void openExportDialog(final DumpFile heapDump) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                fileChooser.setSelectedFile(heapDump.getFile());
+                int result = fileChooser.showSaveDialog(HeapSwingView.this.getUiComponent());
+                if (result == JFileChooser.APPROVE_OPTION) {
+                    File file = fileChooser.getSelectedFile();
+                    heapDump.setFile(file);
+                    heapDumperNotifier.fireAction(HeapDumperAction.SAVE_HEAP_DUMP, heapDump);
+                }
+            }
+        });
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/ExportDumpEvent.java	Tue Jul 09 15:10:03 2013 +0200
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats;
+
+import java.util.EventObject;
+
+import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
+
+@SuppressWarnings("serial")
+public class ExportDumpEvent extends EventObject {
+
+    public ExportDumpEvent(HeapDump source) {
+        super(source);
+    }
+
+    @Override
+    public HeapDump getSource() {
+        return (HeapDump) super.getSource();
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/ExportDumpListener.java	Tue Jul 09 15:10:03 2013 +0200
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats;
+
+import java.util.EventListener;
+
+public interface ExportDumpListener extends EventListener {
+
+    public void actionPerformed(ExportDumpEvent e);
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/ExportDumpPopup.java	Tue Jul 09 15:10:03 2013 +0200
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ * 
+ * This file is part of Thermostat.
+ * 
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ * 
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ * 
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ * 
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ * 
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats;
+
+import java.awt.event.ActionListener;
+
+import javax.swing.JMenuItem;
+
+import com.redhat.thermostat.client.swing.components.ThermostatPopupMenu;
+import com.redhat.thermostat.shared.locale.Translate;
+import com.redhat.thermostat.vm.heap.analysis.client.locale.LocaleResources;
+
+@SuppressWarnings("serial")
+class ExportDumpPopup extends ThermostatPopupMenu {
+
+    private static final Translate<LocaleResources> translator = LocaleResources.createLocalizer();
+    private JMenuItem export;
+    
+    public ExportDumpPopup() {
+        setName(getClass().getName());
+        export = new JMenuItem(translator.localize(LocaleResources.EXPORT_HEAP_DUMP_TO_FILE).getContents());
+        add(export);
+    }
+    
+    public void addExportListener(ActionListener listener) {
+        export.addActionListener(listener);
+    }
+}
--- a/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/HeapSelectionEvent.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/HeapSelectionEvent.java	Tue Jul 09 15:10:03 2013 +0200
@@ -38,6 +38,7 @@
 
 import java.util.EventObject;
 
+@SuppressWarnings("serial")
 public class HeapSelectionEvent extends EventObject {
 
     public HeapSelectionEvent(OverlayComponent source) {
--- a/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/OverlayComponent.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/OverlayComponent.java	Tue Jul 09 15:10:03 2013 +0200
@@ -36,6 +36,18 @@
 
 package com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats;
 
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Image;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.image.BufferedImage;
+import java.beans.Transient;
+import java.util.Date;
+import java.util.Objects;
+
 import com.redhat.thermostat.client.swing.GraphicsUtils;
 import com.redhat.thermostat.client.swing.components.CompositeIcon;
 import com.redhat.thermostat.client.swing.components.Icon;
@@ -46,21 +58,6 @@
 import com.redhat.thermostat.vm.heap.analysis.client.core.HeapIconResources;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
 
-import java.awt.Color;
-import java.awt.Component;
-import java.awt.Dimension;
-import java.awt.GradientPaint;
-import java.awt.Graphics;
-import java.awt.Graphics2D;
-import java.awt.Image;
-import java.awt.Paint;
-import java.awt.event.MouseAdapter;
-import java.awt.event.MouseEvent;
-import java.awt.image.BufferedImage;
-import java.beans.Transient;
-import java.util.Date;
-import java.util.Objects;
-
 @SuppressWarnings("serial")
 public class OverlayComponent extends ShadowLabel {
 
@@ -68,7 +65,7 @@
     
     private boolean selected;
     private String _text;
-    
+        
     public class NiceIcon extends Icon {
         
         private Icon src;
@@ -115,27 +112,21 @@
         
         super(LocalizedString.EMPTY_STRING);
         
+        this.dump = dump;
+        this._text = new Date(dump.getTimestamp()).toString();
+        
         setOpaque(false);
-        
         setForeground(Palette.ROYAL_BLUE.getColor());
-        
         setName(OverlayComponent.class.getName());
-        
+        setFont(TimelineUtils.FONT);
+        setOpaque(false);
+        setToolTipText(_text);
+                
         Icon mask = new Icon(HeapIconResources.getIcon(HeapIconResources.PIN_MASK));
         Icon source = new NiceIcon(mask);
         
         setIcon(new CompositeIcon(mask, source));
         
-        this.dump = dump;
-
-        this._text = new Date(dump.getTimestamp()).toString();
-        
-        setFont(TimelineUtils.FONT);
-
-        setOpaque(false);
-
-        setToolTipText(_text);
-        
         addMouseListener(new MouseAdapter() {
             @Override
             public void mouseEntered(MouseEvent e) {
--- a/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/StatsPanel.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-swing/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/StatsPanel.java	Tue Jul 09 15:10:03 2013 +0200
@@ -37,6 +37,7 @@
 package com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats;
 
 import java.awt.BorderLayout;
+import java.awt.event.ActionEvent;
 import java.awt.event.ActionListener;
 import java.awt.event.MouseAdapter;
 import java.awt.event.MouseEvent;
@@ -53,11 +54,14 @@
 
     private HeapChartPanel heapPanel;
     private HeapDumperPopup popup;
-
+    
+    private ExportDumpPopup export;
     private List<OverlayComponent> overlays;
 
     private boolean canDump;
     
+    private HeapDump selectedDump;
+    
     public StatsPanel() {
         
         setName(StatsPanel.class.getName());
@@ -67,7 +71,17 @@
         overlays = new ArrayList<>();
         
         popup = new HeapDumperPopup();
-
+        
+        export = new ExportDumpPopup();
+        export.addExportListener(new ActionListener() {
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                if (selectedDump != null) {
+                    fireExportDumpEvent(selectedDump);
+                }
+            }
+        });
+        
         setLayout(new BorderLayout());
     }
     
@@ -75,10 +89,12 @@
         if (heapPanel != null) {
             remove(heapPanel);
         }
+        
         heapPanel = panel;
         for (OverlayComponent overlay : overlays) {
             heapPanel.add(overlay);
         }
+        
         heapPanel.addMouseListener(new MouseAdapter() {
             public void mousePressed(MouseEvent e)  {
                 checkPopup(e);
@@ -113,6 +129,10 @@
     public void addDumpListListener(HeapDumpListener listener) {
         listenerList.add(HeapDumpListener.class, listener);
     }
+
+    public void addExportDumpListener(ExportDumpListener listener) {
+        listenerList.add(ExportDumpListener.class, listener);
+    }
     
     public void disableHeapDumperControl(DumpDisabledReason reason) {
         canDump = false;
@@ -122,6 +142,18 @@
         canDump = true;
     }
 
+    private void fireExportDumpEvent(HeapDump source) {
+        Object[] listeners = listenerList.getListenerList();
+
+        ExportDumpEvent event = new ExportDumpEvent(source);
+        
+        for (int i = listeners.length - 2; i >= 0; i -= 2) {
+            if (listeners[i] == ExportDumpListener.class) {
+                ((ExportDumpListener) listeners[i + 1]).actionPerformed(event);
+            }
+        }
+    }
+    
     private void fireHeapDumpClicked(OverlayComponent component) {
         Object[] listeners = listenerList.getListenerList();
 
@@ -134,9 +166,10 @@
         }
     }
     
-    public void addDump(HeapDump dump) {
+    public void addDump(final HeapDump dump) {
         OverlayComponent dumpOverlay = new OverlayComponent(dump);
-        if (!overlays.contains(dumpOverlay)){
+        if (!overlays.contains(dumpOverlay)) {
+            dumpOverlay.setName(String.valueOf(dump.getTimestamp()));
             overlays.add(dumpOverlay);
             dumpOverlay.addMouseListener(new MouseAdapter() {
                 @Override
@@ -150,7 +183,23 @@
                         fireHeapDumpClicked(sourceOverlay);
                     }
                 }
+                
+                public void mousePressed(MouseEvent e)  {
+                    checkPopup(e);
+                }  
+
+                public void mouseReleased(MouseEvent e) {
+                    checkPopup(e);
+                }
+                
+                private void checkPopup(MouseEvent e) {
+                    if (e.isPopupTrigger()) {
+                        selectedDump = dump;
+                        export.show(e.getComponent(), e.getX(), e.getY());
+                    }
+                }
             });
+            
             if (heapPanel != null) {
                 heapPanel.add(dumpOverlay);
             }
@@ -184,4 +233,3 @@
         }
     }
 }
-
--- a/vm-heap-analysis/client-swing/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/HeapSwingViewTest.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/client-swing/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/HeapSwingViewTest.java	Tue Jul 09 15:10:03 2013 +0200
@@ -42,17 +42,21 @@
 
 import java.awt.Container;
 import java.awt.event.KeyEvent;
+import java.io.File;
 import java.util.concurrent.CountDownLatch;
 
 import net.java.openjdk.cacio.ctc.junit.CacioFESTRunner;
 
 import org.fest.swing.annotation.GUITest;
+import org.fest.swing.core.Robot;
 import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
 import org.fest.swing.edt.GuiActionRunner;
 import org.fest.swing.edt.GuiTask;
+import org.fest.swing.finder.JFileChooserFinder;
 import org.fest.swing.fixture.Containers;
 import org.fest.swing.fixture.FrameFixture;
 import org.fest.swing.fixture.JButtonFixture;
+import org.fest.swing.fixture.JFileChooserFixture;
 import org.fest.swing.fixture.JLabelFixture;
 import org.fest.swing.fixture.JPanelFixture;
 import org.fest.swing.fixture.JPopupMenuFixture;
@@ -63,7 +67,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import com.redhat.thermostat.client.swing.components.HeaderPanel;
 import com.redhat.thermostat.client.swing.components.OverlayPanel;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
@@ -72,6 +75,7 @@
 import com.redhat.thermostat.vm.heap.analysis.client.core.chart.OverviewChart;
 import com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats.HeapChartPanel;
 import com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats.OverlayComponent;
+import com.redhat.thermostat.vm.heap.analysis.common.DumpFile;
 import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
 import com.redhat.thermostat.vm.heap.analysis.common.model.HeapInfo;
 
@@ -213,7 +217,7 @@
         
         latch.await();
         
-        JLabelFixture overlay = panel.label(OverlayComponent.class.getName());
+        JLabelFixture overlay = panel.label(String.valueOf(dump.getTimestamp()));
         overlay.doubleClick();
         
         OverlayComponent overlayComponent = (OverlayComponent) overlay.component();
@@ -232,6 +236,93 @@
     
     @GUITest
     @Test
+    public void testExportStep1() {
+        
+        HeapInfo info = mock(HeapInfo.class);
+        when(info.getTimeStamp()).thenReturn(now);
+        HeapDump dump = new HeapDump(info, null);
+
+        view.addHeapDump(dump);
+        
+        HeapInfo info2 = mock(HeapInfo.class);
+        when(info2.getTimeStamp()).thenReturn(now + 1);
+        HeapDump dump2 = new HeapDump(info2, null);
+
+        view.addHeapDump(dump2);
+        
+        final HeapDump [] resultDump = new HeapDump[1];
+
+        view.addDumperListener(new ActionListener<HeapView.HeapDumperAction>() {
+            @Override
+            public void actionPerformed(ActionEvent<HeapDumperAction> actionEvent) {
+                if (actionEvent.getActionId() == HeapDumperAction.REQUEST_EXPORT) {
+                    resultDump[0] = (HeapDump) actionEvent.getPayload();
+                }
+            }
+        });
+        
+        frame.show();
+              
+        JLabelFixture overlayFixture = frame.label(String.valueOf(dump.getTimestamp()));
+        overlayFixture.requireVisible();
+      
+        JPopupMenuFixture popupFixture = overlayFixture.showPopupMenu();
+        popupFixture.click();
+        
+        assertNotNull(resultDump[0]);
+        assertEquals(dump, resultDump[0]);
+        
+        overlayFixture = frame.label(String.valueOf(dump2.getTimestamp()));
+        overlayFixture.requireVisible();
+      
+        popupFixture = overlayFixture.showPopupMenu();
+        popupFixture.click();
+        
+        assertNotNull(resultDump[0]);
+        assertEquals(dump2, resultDump[0]);
+    }
+    
+    @GUITest
+    @Test
+    public void testExportStep2() {
+        
+        HeapInfo info = mock(HeapInfo.class);
+        HeapDump dump = mock(HeapDump.class);
+        when(dump.getInfo()).thenReturn(info);
+        
+        DumpFile localDump = new DumpFile();
+        localDump.setDump(dump);
+        localDump.setFile(new File("TEST-1-.hprof"));
+        
+        final DumpFile [] resultDump = new DumpFile[1];
+
+        view.addDumperListener(new ActionListener<HeapView.HeapDumperAction>() {
+            @Override
+            public void actionPerformed(ActionEvent<HeapDumperAction> actionEvent) {
+                if (actionEvent.getActionId() == HeapDumperAction.SAVE_HEAP_DUMP) {
+                    resultDump[0] = (DumpFile) actionEvent.getPayload();
+                }
+            }
+        });
+        
+        frame.show();
+
+        view.openExportDialog(localDump);
+
+        JFileChooserFixture fileChooser =
+                JFileChooserFinder.findFileChooser("EXPORT_HEAP_DUMP_FILE_CHOOSER").using(frame.robot);
+        File destination = new File("TEST-2-.hprof");
+        fileChooser.selectFile(destination);
+        fileChooser.approve();
+        
+        assertNotNull(resultDump[0]);
+        assertEquals(destination.getName(), resultDump[0].getFile().getName());
+        assertEquals(dump, resultDump[0].getDump());
+
+    }
+    
+    @GUITest
+    @Test
     public void testHeapDumperActionFired() {
         
         final boolean [] result = new boolean[1];
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/client-swing/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/swing/internal/stats/OverlayComponentTest.java	Tue Jul 09 15:10:03 2013 +0200
@@ -0,0 +1,135 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.client.swing.internal.stats;
+
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.awt.Dimension;
+import java.util.Date;
+import java.util.Locale;
+
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+
+import net.java.openjdk.cacio.ctc.junit.CacioFESTRunner;
+
+import org.fest.swing.annotation.GUITest;
+import org.fest.swing.edt.FailOnThreadViolationRepaintManager;
+import org.fest.swing.edt.GuiActionRunner;
+import org.fest.swing.edt.GuiTask;
+import org.fest.swing.fixture.FrameFixture;
+import org.fest.swing.fixture.JLabelFixture;
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import com.redhat.thermostat.vm.heap.analysis.common.HeapDump;
+
+@RunWith(CacioFESTRunner.class)
+public class OverlayComponentTest {
+
+    private OverlayComponent overlay;
+
+    private JFrame frame;
+    private FrameFixture frameFixture;
+    
+    private static Locale defaultLocale;
+    
+    @BeforeClass
+    public static void setUpOnce() {
+        defaultLocale = Locale.getDefault();
+        Locale.setDefault(Locale.US);
+        FailOnThreadViolationRepaintManager.install();
+    }
+
+    @AfterClass
+    public static void tearDownUpOnce() {
+        Locale.setDefault(defaultLocale);
+    }
+    
+    @Before
+    public void setUp() throws Exception {
+        
+        GuiActionRunner.execute(new GuiTask() {
+            @Override
+            protected void executeInEDT() throws Throwable {
+                
+                HeapDump dump = mock(HeapDump.class);
+                when(dump.getTimestamp()).thenReturn(0l);
+                
+                overlay = new OverlayComponent(dump);
+                
+                frame = new JFrame();
+                frame.setMinimumSize(new Dimension(500, 500));
+                
+                JPanel contentPane = new JPanel();
+                contentPane.add(overlay);
+                
+                frame.add(contentPane);
+            }
+        });
+        
+        frameFixture = new FrameFixture(frame);
+    }
+    
+    @After
+    public void tearDown() {
+        frameFixture.cleanUp();
+    }
+    
+    @GUITest
+    @Test
+    public void testHover() {
+        frameFixture.show();
+        
+        JLabelFixture overlayFixture = frameFixture.label(OverlayComponent.class.getName());
+        overlayFixture.requireVisible();
+        
+        String text = overlayFixture.text();
+        assertEquals("", text);
+        
+        frameFixture.robot.moveMouse(overlayFixture.target);
+        
+        text = overlayFixture.text();
+        assertEquals(new Date(1l).toString(), text);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/vm-heap-analysis/common/src/main/java/com/redhat/thermostat/vm/heap/analysis/common/DumpFile.java	Tue Jul 09 15:10:03 2013 +0200
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2012, 2013 Red Hat, Inc.
+ *
+ * This file is part of Thermostat.
+ *
+ * Thermostat is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published
+ * by the Free Software Foundation; either version 2, or (at your
+ * option) any later version.
+ *
+ * Thermostat is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Thermostat; see the file COPYING.  If not see
+ * <http://www.gnu.org/licenses/>.
+ *
+ * Linking this code with other modules is making a combined work
+ * based on this code.  Thus, the terms and conditions of the GNU
+ * General Public License cover the whole combination.
+ *
+ * As a special exception, the copyright holders of this code give
+ * you permission to link this code with independent modules to
+ * produce an executable, regardless of the license terms of these
+ * independent modules, and to copy and distribute the resulting
+ * executable under terms of your choice, provided that you also
+ * meet, for each linked independent module, the terms and conditions
+ * of the license of that module.  An independent module is a module
+ * which is not derived from or based on this code.  If you modify
+ * this code, you may extend this exception to your version of the
+ * library, but you are not obligated to do so.  If you do not wish
+ * to do so, delete this exception statement from your version.
+ */
+
+package com.redhat.thermostat.vm.heap.analysis.common;
+
+import java.io.File;
+
+public class DumpFile {
+
+    private HeapDump dump;
+    private File location;
+    
+    public File getFile() {
+        return location;
+    }
+    
+    public HeapDump getDump() {
+        return dump;
+    }
+    
+    public void setDump(HeapDump dump) {
+        this.dump = dump;
+    }
+    
+    public void setFile(File location) {
+        this.location = location;
+    }
+}
--- a/vm-heap-analysis/common/src/main/java/com/redhat/thermostat/vm/heap/analysis/common/HeapDump.java	Tue Jul 09 15:09:58 2013 +0200
+++ b/vm-heap-analysis/common/src/main/java/com/redhat/thermostat/vm/heap/analysis/common/HeapDump.java	Tue Jul 09 15:10:03 2013 +0200
@@ -228,5 +228,9 @@
     public int hashCode() {
         return heapInfo.hashCode();
     }
+    
+    public String getType() {
+        return "hprof";
+    }
 }