changeset 1208:3d2faeefba5f

Progress API second part review-thread: http://icedtea.classpath.org/pipermail/thermostat/2013-August/007760.html reviewed-by:
author Mario Torre <neugens.limasoftware@gmail.com>
date Mon, 05 Aug 2013 11:56:20 +0200
parents 27c09390b3ce
children d1f55aa2b083
files client/core/src/main/java/com/redhat/thermostat/client/core/progress/ProgressHandle.java client/core/src/test/java/com/redhat/thermostat/client/core/progress/ProgressHandleTest.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/progress/AggregateProgressComponent.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/progress/ProgressNotificationArea.java client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/progress/SwingProgressNotifier.java client/swing/src/test/java/com/redhat/thermostat/client/swing/internal/progress/SwingProgressNotifierTest.java vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/Activator.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/core/internal/HeapDumperServiceImpl.java vm-heap-analysis/client-core/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/ActivatorTest.java vm-heap-analysis/client-core/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpControllerTest.java
diffstat 11 files changed, 481 insertions(+), 46 deletions(-) [+]
line wrap: on
line diff
--- a/client/core/src/main/java/com/redhat/thermostat/client/core/progress/ProgressHandle.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/client/core/src/main/java/com/redhat/thermostat/client/core/progress/ProgressHandle.java	Mon Aug 05 11:56:20 2013 +0200
@@ -38,40 +38,127 @@
 
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ActionNotifier;
+import com.redhat.thermostat.common.model.Range;
+import com.redhat.thermostat.shared.locale.LocalizedString;
+
 import java.util.Objects;
 import java.util.UUID;
 
 /**
+ * Handle that represents the current progress in performing a certain task.
+ * The progress can be undefined or defined.
+ * 
+ * <br /><br />
+ * 
+ * A {@link ProgressHandle} has a name property and a task property. the name
+ * is fixed for the whole running time of the progress and identify the
+ * {@link ProgressHandle} action as a whole. Task name represent a single step
+ * and can be changed.
+ * 
+ * For example, a UI client that downloads a file over the network and then
+ * copy its content into a local database may set the {@link ProgressHandle}
+ * UI clients as "Performing File Copy" and the task as "Downloading file".
+ * Once the first task is complete, it may then change the task to
+ * "Copying file".
+ * 
+ * <br /><br />
+ * 
+ * UI clients are free to decide how to use this information to better match
+ * their User Interface Framework specification, for example the task or even
+ * progress may be hidden until the user expand the status notification area.  
  */
 public class ProgressHandle {
 
     public enum Status {
         STARTED,
         STOPPED,
+        TASK_CHANGED,
+        DETERMINATE_STATUS_CHANGED,
+        PROGRESS_CHANGED,
+        BOUNDS_CHANGED,
     }
     
     private UUID id;
     
     private final ActionNotifier<ProgressHandle.Status> notifier;
     
-    private String name;
+    private LocalizedString name;
+    private LocalizedString task;
+    
     private boolean indeterminate;
     
-    public ProgressHandle(String name) {
+    private int currentProgress;
+    private Range<Integer> range;
+    
+    /**
+     * Create a new {@link ProgressHandle} with the given name and task set
+     * as {@link LocalizedString#EMPTY_STRING}.
+     */
+    public ProgressHandle(LocalizedString name) {
         id = UUID.randomUUID();
+        
         this.name = name;
+        this.task = LocalizedString.EMPTY_STRING;
+        this.range = new Range<>(0, 100);
+        this.indeterminate = true;
+        this.currentProgress = 0;
+        
         notifier = new ActionNotifier<>(this);
     }
 
+    /**
+     * Gets the task {@link LocalizedString} currently associated with this
+     * {@link ProgressHandle}.
+     */
+    public LocalizedString getTask() {
+        return task;
+    }
+    
+    /**
+     * Sets the task currently associated 
+     */
+    public void setTask(LocalizedString task) {
+        this.task = task;
+        notifier.fireAction(Status.TASK_CHANGED, task);
+    }
+    
     public void setIndeterminate(boolean indeterminate) {
         this.indeterminate = indeterminate;
+        notifier.fireAction(Status.DETERMINATE_STATUS_CHANGED, Boolean.valueOf(this.indeterminate));
     }
 
+    public Range<Integer> getRange() {
+        return range;
+    }
+    
+    public void setRange(Range<Integer> range) {
+        this.range = range;
+        notifier.fireAction(Status.BOUNDS_CHANGED, this.range);
+    }
+    
+    public void setProgress(int currentProgress) {
+        int min = range.getMin().intValue();
+        int max = range.getMax().intValue();
+        
+        if (currentProgress < min) {
+            currentProgress = min;
+        } else if (currentProgress > max) {
+            currentProgress = max;
+        }
+        
+        this.currentProgress = currentProgress;
+        notifier.fireAction(Status.PROGRESS_CHANGED, Integer.valueOf(this.currentProgress));
+    }
+    
     public boolean isIndeterminate() {
         return indeterminate;
     }
 
-    public String getName() {
+    public int getProgress() {
+        return currentProgress;
+    }
+    
+    public LocalizedString getName() {
         return name;
     }
 
@@ -82,7 +169,7 @@
     public void stop() {
         notifier.fireAction(Status.STOPPED);
     }
-        
+
     public void addProgressListener(ActionListener<ProgressHandle.Status> listener) {
         notifier.addActionListener(listener);
     }
@@ -93,7 +180,7 @@
 
     @Override
     public String toString() {
-        return name;
+        return name.getContents();
     }
 
     @Override
--- a/client/core/src/test/java/com/redhat/thermostat/client/core/progress/ProgressHandleTest.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/client/core/src/test/java/com/redhat/thermostat/client/core/progress/ProgressHandleTest.java	Mon Aug 05 11:56:20 2013 +0200
@@ -39,23 +39,30 @@
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.times;
-
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.assertFalse;
 
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.model.Range;
+import com.redhat.thermostat.shared.locale.LocalizedString;
 
 public class ProgressHandleTest {
 
     @SuppressWarnings({ "rawtypes", "unchecked" })
     @Test
-    public void testProgressHandle() {
+    public void testProgressHandleStartStop() {
         ActionListener listener = mock(ActionListener.class);
         
-        ProgressHandle handle = new ProgressHandle("test #1");
+        LocalizedString name = new LocalizedString("test #1");
+        ProgressHandle handle = new ProgressHandle(name);
+        
+        assertEquals(name, handle.getName());
+        
         handle.addProgressListener(listener);
 
         ArgumentCaptor<ActionEvent> captor =
@@ -74,4 +81,110 @@
         event = captor.getValue();
         assertEquals(ProgressHandle.Status.STOPPED, event.getActionId());
     }
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Test
+    public void testProgressHandleStatusChange() {
+        ActionListener listener = mock(ActionListener.class);
+        
+        LocalizedString name = new LocalizedString("test #1");
+        ProgressHandle handle = new ProgressHandle(name);
+        
+        assertEquals(name, handle.getName());
+        
+        handle.addProgressListener(listener);
+
+        ArgumentCaptor<ActionEvent> captor =
+                ArgumentCaptor.forClass(ActionEvent.class);        
+        
+        handle.setIndeterminate(true);
+
+        verify(listener).actionPerformed(captor.capture());
+        
+        ActionEvent event = captor.getValue();
+        assertEquals(ProgressHandle.Status.DETERMINATE_STATUS_CHANGED, event.getActionId());
+        assertTrue(handle.isIndeterminate());
+        assertEquals(Boolean.TRUE, event.getPayload());
+
+        handle.setIndeterminate(false);
+
+        verify(listener, times(2)).actionPerformed(captor.capture());
+        
+        event = captor.getValue();
+        assertEquals(ProgressHandle.Status.DETERMINATE_STATUS_CHANGED, event.getActionId());
+        assertFalse(handle.isIndeterminate());
+        assertEquals(Boolean.FALSE, event.getPayload());
+    }
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Test
+    public void testProgressHandleProgressChange() {
+        ActionListener listener = mock(ActionListener.class);
+        
+        LocalizedString name = new LocalizedString("test #1");
+        ProgressHandle handle = new ProgressHandle(name);
+        
+        assertEquals(name, handle.getName());
+        
+        handle.addProgressListener(listener);
+
+        ArgumentCaptor<ActionEvent> captor =
+                ArgumentCaptor.forClass(ActionEvent.class);        
+        
+        handle.setProgress(5);
+
+        verify(listener).actionPerformed(captor.capture());
+        
+        ActionEvent event = captor.getValue();
+        assertEquals(ProgressHandle.Status.PROGRESS_CHANGED, event.getActionId());
+        assertEquals(5, handle.getProgress());
+        assertEquals(Integer.valueOf(5), event.getPayload());
+
+        handle.setProgress(15);
+
+        verify(listener, times(2)).actionPerformed(captor.capture());
+        
+        event = captor.getValue();
+        assertEquals(ProgressHandle.Status.PROGRESS_CHANGED, event.getActionId());
+        assertEquals(15, handle.getProgress());
+        assertEquals(Integer.valueOf(15), event.getPayload());
+    }
+    
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Test
+    public void testProgressHandleBoundsChange() {
+        ActionListener listener = mock(ActionListener.class);
+        
+        LocalizedString name = new LocalizedString("test #1");
+        ProgressHandle handle = new ProgressHandle(name);
+        
+        assertEquals(name, handle.getName());
+        
+        handle.addProgressListener(listener);
+
+        ArgumentCaptor<ActionEvent> captor =
+                ArgumentCaptor.forClass(ActionEvent.class);        
+        
+        Range<Integer> range = new Range<Integer>(10, 20);
+        
+        handle.setRange(range);
+
+        verify(listener).actionPerformed(captor.capture());
+        
+        ActionEvent event = captor.getValue();
+        assertEquals(ProgressHandle.Status.BOUNDS_CHANGED, event.getActionId());
+        assertEquals(range, handle.getRange());
+        assertEquals(range, event.getPayload());
+
+        range = new Range<Integer>(0xCAFE, 42);
+        handle.setRange(range);
+
+        verify(listener, times(2)).actionPerformed(captor.capture());
+        
+        event = captor.getValue();
+        assertEquals(ProgressHandle.Status.BOUNDS_CHANGED, event.getActionId());
+        assertEquals(range, handle.getRange());
+        assertEquals(range, event.getPayload());
+
+    }
 }
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/progress/AggregateProgressComponent.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/progress/AggregateProgressComponent.java	Mon Aug 05 11:56:20 2013 +0200
@@ -37,11 +37,14 @@
 package com.redhat.thermostat.client.swing.internal.progress;
 
 import java.awt.BasicStroke;
+import java.awt.BorderLayout;
 import java.awt.Component;
+import java.awt.Font;
 import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.GridLayout;
 
+import javax.swing.JLabel;
 import javax.swing.JPanel;
 import javax.swing.JProgressBar;
 
@@ -51,30 +54,53 @@
 import com.redhat.thermostat.client.swing.components.GradientPanel;
 import com.redhat.thermostat.client.swing.components.ShadowLabel;
 import com.redhat.thermostat.client.ui.Palette;
-import com.redhat.thermostat.shared.locale.LocalizedString;
 
 @SuppressWarnings("serial")
-public class AggregateProgressComponent extends GradientPanel {
-    
+class AggregateProgressComponent extends GradientPanel {
+
+    private JProgressBar progressBar;
+    private ShadowLabel taskStatus;
     public AggregateProgressComponent(ProgressHandle handle) {
         
         super(Palette.WHITE.getColor(), Palette.PALE_GRAY.getColor());
         
+        setLayout(new BorderLayout());
+        
         setBorder(new AggregateProgressComponentBorder());
         
         JPanel panel = new JPanel(new GridLayout());
         panel.setOpaque(false);
-        add(panel);
+        add(panel, BorderLayout.CENTER);
         
-        ShadowLabel text = new ShadowLabel(new LocalizedString(handle.getName()));
+        ShadowLabel text = new ShadowLabel(handle.getName());
         panel.add(text);
 
-        JProgressBar progressBar = new JProgressBar();
-        progressBar.setString(handle.getName());
-        progressBar.setStringPainted(true);
+        progressBar = new JProgressBar();
+        progressBar.setName(handle.getName().getContents());
+        progressBar.setStringPainted(false);
       
         progressBar.setIndeterminate(handle.isIndeterminate());
         panel.add(progressBar);
+        
+        JPanel currentTaskStatusPane = new JPanel(new GridLayout());
+        currentTaskStatusPane.setOpaque(false);
+        
+        taskStatus = new ShadowLabel();
+
+        Font defaultFont = taskStatus.getFont();
+        taskStatus.setFont(defaultFont.deriveFont(defaultFont.getSize2D() - 2.5f));
+        taskStatus.setText(handle.getTask().getContents());
+        currentTaskStatusPane.add(taskStatus);
+        
+        add(currentTaskStatusPane, BorderLayout.SOUTH);
+    }
+    
+    public JProgressBar getProgressBar() {
+        return progressBar;
+    }
+    
+    public JLabel getTaskStatus() {
+        return taskStatus;
     }
     
     private static class AggregateProgressComponentBorder extends DebugBorder {
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/progress/ProgressNotificationArea.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/progress/ProgressNotificationArea.java	Mon Aug 05 11:56:20 2013 +0200
@@ -41,11 +41,16 @@
 import javax.swing.JPanel;
 import javax.swing.JProgressBar;
 import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
 
 import com.redhat.thermostat.client.core.progress.ProgressHandle;
+import com.redhat.thermostat.client.core.progress.ProgressHandle.Status;
 import com.redhat.thermostat.client.swing.components.FontAwesomeIcon;
 import com.redhat.thermostat.client.swing.components.Icon;
 import com.redhat.thermostat.client.swing.components.ShadowLabel;
+import com.redhat.thermostat.common.ActionEvent;
+import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.model.Range;
 
 @SuppressWarnings("serial")
 public class ProgressNotificationArea extends JPanel {
@@ -66,10 +71,33 @@
         moreTasksIcon = new FontAwesomeIcon('\uf0d8', 12);
     }
 
+    private void handleAction(ActionEvent<Status> actionEvent, JProgressBar progressBar) {
+        switch(actionEvent.getActionId()) {
+        case DETERMINATE_STATUS_CHANGED:
+            progressBar.setIndeterminate(((Boolean) actionEvent.getPayload()).booleanValue());
+            break;
+
+        case BOUNDS_CHANGED: {
+            @SuppressWarnings("unchecked")
+            Range<Integer> range = (Range<Integer>) actionEvent.getPayload();
+            progressBar.setMinimum(range.getMin().intValue());
+            progressBar.setMaximum(range.getMax().intValue());
+            
+        } break;
+        
+        case PROGRESS_CHANGED:
+            progressBar.setValue(((Integer) actionEvent.getPayload()).intValue());
+            break;
+            
+        default:
+            break;
+        }
+    }
+    
     public void setRunningTask(final ProgressHandle handle) {
         removeAll();
-        
-        taskLabel.setText(handle.getName());
+                
+        taskLabel.setText(handle.getName().getContents());
         if (hasMore) {
             taskLabel.setIcon(moreTasksIcon);
         } else {
@@ -78,11 +106,23 @@
         
         add(taskLabel, BorderLayout.CENTER);
         
-        JProgressBar progressBar = new JProgressBar();
+        final JProgressBar progressBar = new JProgressBar();
         progressBar.setIndeterminate(handle.isIndeterminate());
         add(progressBar, BorderLayout.EAST);
 
-        progressBar.setName(handle.getName());
+        progressBar.setName(handle.getName().getContents());
+        
+        handle.addProgressListener(new ActionListener<ProgressHandle.Status>() {
+            @Override
+            public void actionPerformed(final ActionEvent<Status> actionEvent) {
+                SwingUtilities.invokeLater(new Runnable() {
+                    @Override
+                    public void run() {
+                        handleAction(actionEvent, progressBar);
+                    }
+                });
+            }
+        });
         
         runningTask = handle;
         
--- a/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/progress/SwingProgressNotifier.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/client/swing/src/main/java/com/redhat/thermostat/client/swing/internal/progress/SwingProgressNotifier.java	Mon Aug 05 11:56:20 2013 +0200
@@ -36,17 +36,19 @@
 
 package com.redhat.thermostat.client.swing.internal.progress;
 
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import javax.swing.SwingUtilities;
+
 import com.redhat.thermostat.client.core.progress.ProgressHandle;
 import com.redhat.thermostat.client.core.progress.ProgressNotifier;
 import com.redhat.thermostat.client.swing.SwingComponent;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ActionNotifier;
-
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-
-import javax.swing.SwingUtilities;
+import com.redhat.thermostat.common.model.Range;
+import com.redhat.thermostat.shared.locale.LocalizedString;
 
 public class SwingProgressNotifier implements ProgressNotifier, SwingComponent {
 
@@ -79,6 +81,10 @@
         this(aggregateNotificationArea, notificationArea, glassPane, true);
     }
     
+    /**
+     * For test only, allows to build a notifier that runs update outside
+     * the EDT.
+     */
     SwingProgressNotifier(AggregateNotificationPanel aggregateNotificationArea,
                           ProgressNotificationArea notificationArea,
                           ThermostatGlassPane glassPane, boolean runInEDT)
@@ -92,6 +98,14 @@
         this.runInEDT = runInEDT;
     }
     
+    /**
+     * For test only, access the internal map containing handles and
+     * progress components currently tracked by this notifier.
+     */
+    Map<ProgressHandle, AggregateProgressComponent> __getTasks() {
+        return tasks;
+    }
+    
     private void handleTask(ActionEvent<ProgressHandle.Status> status, ProgressHandle handle) {
         switch (status.getActionId()) {
         case STARTED: {
@@ -107,7 +121,9 @@
 
         case STOPPED: {
             AggregateProgressComponent progressBar = tasks.remove(handle);
-            aggregateNotificationArea.removeProgress(progressBar);
+            if (progressBar != null) {
+                aggregateNotificationArea.removeProgress(progressBar);                
+            }
             
             if (tasks.isEmpty()) {
                 notificationArea.reset();
@@ -125,9 +141,46 @@
             }
             
         } break;
+        
+        case TASK_CHANGED: {
+            AggregateProgressComponent progressBar = tasks.get(handle);
+            if (progressBar != null) {
+                String text = ((LocalizedString) status.getPayload()).getContents();
+                progressBar.getTaskStatus().setText(text);
+            }
+        
+        } break;
+        
+        case DETERMINATE_STATUS_CHANGED: {
+            AggregateProgressComponent progressBar = tasks.get(handle);
+            if (progressBar != null) {
+                boolean state = ((Boolean) status.getPayload()).booleanValue();
+                progressBar.getProgressBar().setIndeterminate(state);                
+            }
+        } break;
+
+        case BOUNDS_CHANGED: {
+            AggregateProgressComponent progressBar = tasks.get(handle);
+            if (progressBar != null) {
+                
+                @SuppressWarnings("unchecked")
+                Range<Integer> range = (Range<Integer>) status.getPayload();
+                progressBar.getProgressBar().setMinimum(range.getMin().intValue());
+                progressBar.getProgressBar().setMaximum(range.getMax().intValue());
+            }
+            
+        } break;
+        
+        case PROGRESS_CHANGED: {
+            AggregateProgressComponent progressBar = tasks.get(handle);
+            if (progressBar != null) {
+                int value = ((Integer) status.getPayload()).intValue();
+                progressBar.getProgressBar().setValue(value);
+            }
+        } break;
             
         default:
-            throw new UnsupportedOperationException("Case not implemented");
+            // nothing here
         }
     }
     
--- a/client/swing/src/test/java/com/redhat/thermostat/client/swing/internal/progress/SwingProgressNotifierTest.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/client/swing/src/test/java/com/redhat/thermostat/client/swing/internal/progress/SwingProgressNotifierTest.java	Mon Aug 05 11:56:20 2013 +0200
@@ -37,9 +37,12 @@
 package com.redhat.thermostat.client.swing.internal.progress;
 
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 import static org.mockito.Mockito.verify;
 import static org.junit.Assert.assertTrue;
 
+import javax.swing.JLabel;
+import javax.swing.JProgressBar;
 import javax.swing.RepaintManager;
 
 import org.junit.Before;
@@ -53,6 +56,8 @@
 import com.redhat.thermostat.client.swing.internal.progress.SwingProgressNotifier.PropertyChange;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.model.Range;
+import com.redhat.thermostat.shared.locale.LocalizedString;
 
 public class SwingProgressNotifierTest {
 
@@ -61,7 +66,7 @@
     private ThermostatGlassPane glassPane;
     
     @BeforeClass
-    public void setUpOnce() {
+    public static void setUpOnce() {
         // This is needed because some other test may have installed the
         // EDT violation checker repaint manager.
         // We don't need this check here, since we are not testing Swing
@@ -78,12 +83,14 @@
     
     @SuppressWarnings({ "unchecked", "rawtypes" })
     @Test
-    public void testNotifier() throws InterruptedException {
+    public void testNotifierStartStop() throws InterruptedException {
         ProgressNotifier notifier =
                 new SwingProgressNotifier(aggregateNotificationArea,
                                           notificationArea, glassPane, false);
         
         ProgressHandle handle = mock(ProgressHandle.class);
+        when(handle.getName()).thenReturn(LocalizedString.EMPTY_STRING);
+        when(handle.getTask()).thenReturn(LocalizedString.EMPTY_STRING);
         notifier.register(handle);
         
         final boolean [] result = new boolean[1];
@@ -112,15 +119,89 @@
         
         assertTrue(notifier.hasTasks());
         
-        AggregateProgressComponent aggregateComponent = notificationAreaCaptor.getValue();
-        
         event = new ActionEvent<ProgressHandle.Status>(handle, Status.STOPPED);
         listener.actionPerformed(event);
         
+        AggregateProgressComponent aggregateComponent = notificationAreaCaptor.getValue();
         verify(aggregateNotificationArea).removeProgress(aggregateComponent);
         verify(notificationArea).reset();
         verify(notificationArea).setHasMore(false);
         
         assertTrue(result[0]);
     }
+    
+    @SuppressWarnings({ "unchecked", "rawtypes" })
+    @Test
+    public void testHandleStatusChanges() throws InterruptedException {
+        SwingProgressNotifier notifier =
+                new SwingProgressNotifier(aggregateNotificationArea,
+                                          notificationArea, glassPane, false);
+
+        JProgressBar progressBar = mock(JProgressBar.class);
+        JLabel label = mock(JLabel.class);
+        AggregateProgressComponent progressComponent =
+                mock(AggregateProgressComponent.class);
+        when(progressComponent.getProgressBar()).thenReturn(progressBar);
+        when(progressComponent.getTaskStatus()).thenReturn(label);
+
+        ProgressHandle handle = mock(ProgressHandle.class);
+        when(handle.getName()).thenReturn(LocalizedString.EMPTY_STRING);
+        when(handle.getTask()).thenReturn(LocalizedString.EMPTY_STRING);
+        notifier.register(handle);
+
+        ArgumentCaptor<ActionListener> captor =
+                ArgumentCaptor.forClass(ActionListener.class);
+        verify(handle).addProgressListener(captor.capture());
+        
+        notifier.__getTasks().put(handle, progressComponent);
+        ActionListener listener = captor.getValue();
+        
+        LocalizedString textPayload = new LocalizedString("test");
+        ActionEvent<ProgressHandle.Status> event =
+                new ActionEvent<ProgressHandle.Status>(handle, Status.TASK_CHANGED);
+        event.setPayload(textPayload);
+        listener.actionPerformed(event);
+
+        verify(label).setText(textPayload.getContents());
+
+        event = new ActionEvent<ProgressHandle.Status>(handle, Status.DETERMINATE_STATUS_CHANGED);
+        event.setPayload(Boolean.TRUE);
+        listener.actionPerformed(event);
+        
+        verify(progressBar).setIndeterminate(true);
+
+        event = new ActionEvent<ProgressHandle.Status>(handle, Status.DETERMINATE_STATUS_CHANGED);
+        event.setPayload(Boolean.FALSE);
+        listener.actionPerformed(event);
+        
+        verify(progressBar).setIndeterminate(false);
+        
+        event = new ActionEvent<ProgressHandle.Status>(handle, Status.PROGRESS_CHANGED);
+        event.setPayload(Integer.valueOf(10));
+        listener.actionPerformed(event);
+        
+        verify(progressBar).setValue(10);
+        
+        event = new ActionEvent<ProgressHandle.Status>(handle, Status.PROGRESS_CHANGED);
+        event.setPayload(Integer.valueOf(99));
+        listener.actionPerformed(event);
+        
+        verify(progressBar).setValue(99);
+        
+        Range<Integer> range = new Range<Integer>(5, 20);
+        event = new ActionEvent<ProgressHandle.Status>(handle, Status.BOUNDS_CHANGED);
+        event.setPayload(range);
+        listener.actionPerformed(event);
+        
+        verify(progressBar).setMinimum(5);
+        verify(progressBar).setMaximum(20);
+        
+        range = new Range<Integer>(99, 101);
+        event = new ActionEvent<ProgressHandle.Status>(handle, Status.BOUNDS_CHANGED);
+        event.setPayload(range);
+        listener.actionPerformed(event);
+        
+        verify(progressBar).setMinimum(99);
+        verify(progressBar).setMaximum(101);
+    }
 }
--- a/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/Activator.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/Activator.java	Mon Aug 05 11:56:20 2013 +0200
@@ -46,6 +46,7 @@
 import org.osgi.framework.ServiceRegistration;
 
 import com.redhat.thermostat.client.core.InformationService;
+import com.redhat.thermostat.client.core.progress.ProgressNotifier;
 import com.redhat.thermostat.common.ApplicationService;
 import com.redhat.thermostat.common.Constants;
 import com.redhat.thermostat.common.MultipleServiceTracker;
@@ -80,12 +81,17 @@
             ObjectDetailsViewProvider.class,
             ObjectRootsViewProvider.class,
             HeapDumpListViewProvider.class,
+            ProgressNotifier.class,
         };
 
         tracker = new MultipleServiceTracker(context, deps, new Action() {
             
             @Override
             public void dependenciesAvailable(Map<String, Object> services) {
+                
+                ProgressNotifier notifier = (ProgressNotifier) services.get(ProgressNotifier.class.getName());
+                Objects.requireNonNull(notifier);
+                
                 ApplicationService appSvc = (ApplicationService) services.get(ApplicationService.class.getName());
                 Objects.requireNonNull(appSvc);
                 VmInfoDAO vmInfoDao = Objects.requireNonNull((VmInfoDAO) services.get(VmInfoDAO.class.getName()));
@@ -115,7 +121,7 @@
                         vmInfoDao, vmMemoryStatDao, heapDao, viewProvider,
                         detailsViewProvider, histogramViewProvider,
                         objectDetailsViewProvider, objectRootsViewProvider,
-                        heapDumpListViewProvider);
+                        heapDumpListViewProvider, notifier);
                 Dictionary<String, String> properties = new Hashtable<>();
                 properties.put(Constants.GENERIC_SERVICE_CLASSNAME, VmRef.class.getName());
                 properties.put(InformationService.KEY_SERVICE_ID, HeapDumperService.SERVICE_ID);
--- a/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpController.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpController.java	Mon Aug 05 11:56:20 2013 +0200
@@ -50,6 +50,8 @@
 import java.util.logging.Logger;
 
 import com.redhat.thermostat.client.core.controllers.InformationServiceController;
+import com.redhat.thermostat.client.core.progress.ProgressHandle;
+import com.redhat.thermostat.client.core.progress.ProgressNotifier;
 import com.redhat.thermostat.client.core.views.BasicView.Action;
 import com.redhat.thermostat.client.core.views.UIComponent;
 import com.redhat.thermostat.common.ActionEvent;
@@ -104,6 +106,8 @@
     private ObjectRootsViewProvider objectRootsViewProvider;
     private HeapDumpListViewProvider heapDumpListViewProvider;
 
+    private ProgressNotifier notifier;
+    
     public HeapDumpController(final VmMemoryStatDAO vmMemoryStatDao,
                               final VmInfoDAO vmInfoDao,
                               final HeapDAO heapDao, final VmRef ref,
@@ -112,11 +116,13 @@
                               HeapHistogramViewProvider histogramProvider,
                               ObjectDetailsViewProvider objectDetailsProvider,
                               ObjectRootsViewProvider objectRootsProvider,
-                              HeapDumpListViewProvider heapDumpListViewProvider)
+                              HeapDumpListViewProvider heapDumpListViewProvider,
+                              ProgressNotifier notifier)
     {
         this(vmMemoryStatDao, vmInfoDao, heapDao, ref, appService, viewProvider,
              detailsViewProvider, histogramProvider, objectDetailsProvider,
-             objectRootsProvider, heapDumpListViewProvider, new HeapDumper(ref));
+             objectRootsProvider, heapDumpListViewProvider, new HeapDumper(ref),
+             notifier);
     }
 
     HeapDumpController(final VmMemoryStatDAO vmMemoryStatDao,
@@ -129,8 +135,10 @@
                        ObjectDetailsViewProvider objectDetailsProvider,
                        ObjectRootsViewProvider objectRootsProvider,
                        HeapDumpListViewProvider heapDumpListViewProvider,
-                       final HeapDumper heapDumper)
+                       final HeapDumper heapDumper,
+                       ProgressNotifier notifier)
     {
+        this.notifier = notifier;
         this.objectDetailsViewProvider = objectDetailsProvider;
         this.objectRootsViewProvider = objectRootsProvider;
         this.histogramViewProvider = histogramProvider;
@@ -223,9 +231,7 @@
                     view.openExportDialog(localHeapDump);
                 } break;
                 
-                case SAVE_HEAP_DUMP: {
-                    // FIXME: we really need some indicator that something is
-                    // going on here, same for dumping requests
+                case SAVE_HEAP_DUMP: {                    
                     DumpFile localHeapDump = (DumpFile) actionEvent.getPayload();
                     saveHeapDump(localHeapDump);
                 } break;
@@ -246,6 +252,14 @@
         appService.getApplicationExecutor().execute(new Runnable() {
             @Override
             public void run() {
+
+                LocalizedString taskName = translator.localize(LocaleResources.HEAP_DUMP_IN_PROGRESS);
+                
+                final ProgressHandle handle = new ProgressHandle(taskName);
+                handle.setTask(taskName);
+                handle.setIndeterminate(true);
+                notifier.register(handle);
+
                 HeapDump dump = localHeapDump.getDump();
                 File file = localHeapDump.getFile();
                 if (dump == null || file == null) {
@@ -254,6 +268,7 @@
                     return;
                 }
                 
+                handle.start();
                 try (InputStream in = heapDAO.getHeapDumpData(dump.getInfo())) {
                     Files.copy(in, file.toPath());
                     
@@ -262,6 +277,8 @@
                     view.displayWarning(message);
                     Logger.getLogger(HeapDumpController.class.getSimpleName()).
                         log(Level.WARNING, message.getContents(), e);
+                } finally {
+                    handle.stop();
                 }
             }
         });
--- a/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumperServiceImpl.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/vm-heap-analysis/client-core/src/main/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumperServiceImpl.java	Mon Aug 05 11:56:20 2013 +0200
@@ -39,6 +39,7 @@
 import com.redhat.thermostat.client.core.Filter;
 import com.redhat.thermostat.client.core.NameMatchingRefFilter;
 import com.redhat.thermostat.client.core.controllers.InformationServiceController;
+import com.redhat.thermostat.client.core.progress.ProgressNotifier;
 import com.redhat.thermostat.common.ApplicationService;
 import com.redhat.thermostat.storage.core.VmRef;
 import com.redhat.thermostat.storage.dao.VmInfoDAO;
@@ -68,6 +69,8 @@
     private ObjectDetailsViewProvider objectDetailsViewProvider;
     private ObjectRootsViewProvider objectRootsViewProvider;
 
+    private ProgressNotifier notifier;
+    
     private HeapDumpListViewProvider heapDumpListViewProvider;
     
     public HeapDumperServiceImpl(ApplicationService appService,
@@ -77,7 +80,8 @@
             HeapHistogramViewProvider histogramViewProvider,
             ObjectDetailsViewProvider objectDetailsViewProvider,
             ObjectRootsViewProvider objectRootsViewProvider,
-            HeapDumpListViewProvider heapDumpListViewProvider) {
+            HeapDumpListViewProvider heapDumpListViewProvider,
+            ProgressNotifier notifier) {
         this.vmInfoDao = vmInfoDao;
         this.vmMemoryStatDao = vmMemoryStatDao;
         this.heapDao = heapDao;
@@ -88,13 +92,14 @@
         this.objectDetailsViewProvider = objectDetailsViewProvider;
         this.objectRootsViewProvider = objectRootsViewProvider;
         this.heapDumpListViewProvider = heapDumpListViewProvider;
+        this.notifier = notifier;
     }
 
     @Override
     public InformationServiceController<VmRef> getInformationServiceController(VmRef ref) {
         return new HeapDumpController(vmMemoryStatDao, vmInfoDao, heapDao, ref, appService,
                 viewProvider, detailsViewProvider, histogramViewProvider, objectDetailsViewProvider,
-                objectRootsViewProvider, heapDumpListViewProvider);
+                objectRootsViewProvider, heapDumpListViewProvider, notifier);
     }
 
     @Override
--- a/vm-heap-analysis/client-core/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/ActivatorTest.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/vm-heap-analysis/client-core/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/ActivatorTest.java	Mon Aug 05 11:56:20 2013 +0200
@@ -44,6 +44,7 @@
 import org.junit.Test;
 
 import com.redhat.thermostat.client.core.InformationService;
+import com.redhat.thermostat.client.core.progress.ProgressNotifier;
 import com.redhat.thermostat.common.ApplicationService;
 import com.redhat.thermostat.storage.dao.VmInfoDAO;
 import com.redhat.thermostat.testutils.StubBundleContext;
@@ -88,6 +89,7 @@
         ObjectDetailsViewProvider objectDetailsViewProvider = mock(ObjectDetailsViewProvider.class);
         ObjectRootsViewProvider objectRootsViewProvider = mock(ObjectRootsViewProvider.class);
         HeapDumpListViewProvider heapDumpListViewProvider = mock(HeapDumpListViewProvider.class);
+        ProgressNotifier progressNotifier = mock(ProgressNotifier.class);
 
         context.registerService(VmInfoDAO.class, vmInfoDao, null);
         context.registerService(VmMemoryStatDAO.class, vmMemoryStatDAO, null);
@@ -100,6 +102,7 @@
         context.registerService(ObjectDetailsViewProvider.class, objectDetailsViewProvider, null);
         context.registerService(ObjectRootsViewProvider.class, objectRootsViewProvider, null);
         context.registerService(HeapDumpListViewProvider.class, heapDumpListViewProvider, null);
+        context.registerService(ProgressNotifier.class, progressNotifier, null);
 
         Activator activator = new Activator();
 
@@ -110,7 +113,7 @@
         activator.stop(context);
 
         assertEquals(0, context.getServiceListeners().size());
-        assertEquals(10, context.getAllServices().size());
+        assertEquals(11, context.getAllServices().size());
     }
 
 }
--- a/vm-heap-analysis/client-core/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpControllerTest.java	Mon Aug 05 11:56:19 2013 +0200
+++ b/vm-heap-analysis/client-core/src/test/java/com/redhat/thermostat/vm/heap/analysis/client/core/internal/HeapDumpControllerTest.java	Mon Aug 05 11:56:20 2013 +0200
@@ -38,7 +38,6 @@
 
 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;
@@ -68,6 +67,7 @@
 import org.mockito.stubbing.Answer;
 
 import com.lowagie.text.pdf.codec.Base64.InputStream;
+import com.redhat.thermostat.client.core.progress.ProgressNotifier;
 import com.redhat.thermostat.common.ActionEvent;
 import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.ApplicationCache;
@@ -130,6 +130,8 @@
 
     private HeapDumpListViewProvider heapDumpListViewProvider;
     
+    private ProgressNotifier notifier;
+    
     @Before
     public void setUp() {
         heapDao = mock(HeapDAO.class);
@@ -141,6 +143,8 @@
         appService = mock(ApplicationService.class);
         heapDumper = mock(HeapDumper.class);
 
+        notifier = mock(ProgressNotifier.class);
+        
         heapDumpListViewProvider = mock(HeapDumpListViewProvider.class);
         
         setUpView();
@@ -207,7 +211,7 @@
         controller = new HeapDumpController(vmDao, vmInfoDao, heapDao, ref, appService,
                 viewProvider, detailsViewProvider, histogramProvider,
                 objectDetailsProvider, objectRootsProvider, heapDumpListViewProvider,
-                heapDumper);
+                heapDumper, notifier);
     }
     
     @After
@@ -292,7 +296,7 @@
         controller = new HeapDumpController(vmDao, vmInfoDao, heapDao, ref, appService,
                 viewProvider, detailsViewProvider, histogramProvider,
                 objectDetailsProvider, objectRootsProvider, heapDumpListViewProvider,
-                heapDumper);
+                heapDumper, notifier);
         
         verify(view, times(1)).setChildView(any(HeapView.class));
         verify(view, times(1)).openDumpView();
@@ -318,7 +322,7 @@
         controller = new HeapDumpController(vmDao, vmInfoDao, heapDao, ref, appService,
                 viewProvider, detailsViewProvider, histogramProvider,
                 objectDetailsProvider, objectRootsProvider, heapDumpListViewProvider,
-                heapDumper);
+                heapDumper, notifier);
         
         verify(view, times(0)).openDumpView();
     }
@@ -339,7 +343,7 @@
         controller = new HeapDumpController(vmDao, vmInfoDao, heapDao, ref, appService,
                 viewProvider, detailsViewProvider, histogramProvider,
                 objectDetailsProvider, objectRootsProvider, heapDumpListViewProvider,
-                heapDumper);
+                heapDumper, notifier);
 
         verify(view).disableHeapDumping(DumpDisabledReason.PROCESS_DEAD);
     }