changeset 632:5e8550c83f37

Merge
author Roman Kennke <rkennke@redhat.com>
date Thu, 20 Sep 2012 00:33:03 +0200
parents 9eed9fa986ad (current diff) fda64c331b67 (diff)
children c98de73fc86c
files client/killvm/src/main/java/com/redhat/thermostat/client/killvm/internal/VMKilledListener.java
diffstat 41 files changed, 1866 insertions(+), 157 deletions(-) [+]
line wrap: on
line diff
--- a/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestEncoder.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestEncoder.java	Thu Sep 20 00:33:03 2012 +0200
@@ -36,22 +36,27 @@
 
 package com.redhat.thermostat.client.command.internal;
 
+import static org.jboss.netty.buffer.ChannelBuffers.dynamicBuffer;
+import static org.jboss.netty.buffer.ChannelBuffers.wrappedBuffer;
+
 import java.util.Collection;
+import java.util.logging.Level;
+import java.util.logging.Logger;
 
 import org.jboss.netty.buffer.ChannelBuffer;
 import org.jboss.netty.channel.ChannelHandlerContext;
 import org.jboss.netty.channel.Channels;
+import org.jboss.netty.channel.ExceptionEvent;
 import org.jboss.netty.channel.MessageEvent;
 
 import com.redhat.thermostat.common.command.EncodingHelper;
 import com.redhat.thermostat.common.command.MessageEncoder;
 import com.redhat.thermostat.common.command.Request;
 
-import static org.jboss.netty.buffer.ChannelBuffers.dynamicBuffer;
-import static org.jboss.netty.buffer.ChannelBuffers.wrappedBuffer;
-
 class RequestEncoder extends MessageEncoder {
 
+    private static final Logger logger = Logger.getLogger(RequestEncoder.class.getName());
+    
     @Override
     public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) {
 
@@ -75,5 +80,12 @@
         ChannelBuffer buf = wrappedBuffer(typeBuffer, parmsBuffer);
         Channels.write(ctx, e.getFuture(), buf);
     }
-
+    
+    // This must be implemented, even though we are simply passing on the exception.  If
+    // not implemented, this exception ends up going uncaught which causes problems.
+    @Override
+    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
+        logger.log(Level.WARNING, "Forwarding exception ", e.getCause());
+        Channels.fireExceptionCaught(ctx, e.getCause());
+    }
 }
\ No newline at end of file
--- a/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/command/src/main/java/com/redhat/thermostat/client/command/internal/RequestQueueImpl.java	Thu Sep 20 00:33:03 2012 +0200
@@ -45,6 +45,9 @@
 
 import com.redhat.thermostat.client.command.RequestQueue;
 import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.RequestResponseListener;
+import com.redhat.thermostat.common.command.Response;
+import com.redhat.thermostat.common.command.Response.ResponseType;
 
 class RequestQueueImpl implements RequestQueue {
 
@@ -103,9 +106,17 @@
                 }
                 ChannelFuture f = ((ClientBootstrap) ctx.getBootstrap()).connect(request.getTarget());
                 f.awaitUninterruptibly();
-                Channel c = f.getChannel();
-                c.getPipeline().addLast("responseHandler", new ResponseHandler(request));
-                c.write(request);
+                if (f.isSuccess()) {
+                	Channel c = f.getChannel();
+                	c.getPipeline().addLast("responseHandler", new ResponseHandler(request));
+                	c.write(request);
+                } else {
+                	Response response  = new Response(ResponseType.ERROR);
+                	// TODO add more information once Response supports parameters.
+                	for (RequestResponseListener listener : request.getListeners()) {
+                		listener.fireComplete(request, response);
+                	}
+                }
             }
         }
     }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/command/src/test/java/com/redhat/thermostat/client/command/internal/RequestEncoderTest.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2012 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.client.command.internal;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.mockito.Matchers.isA;
+
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ExceptionEvent;
+import org.junit.Test;
+
+public class RequestEncoderTest {
+
+    /**
+     * This test verifies that exception events are forwarded upstream. This is to ensure that
+     * fireComplete() events get delivered in case of Exceptions.
+     * 
+     * @throws Exception
+     */
+    @Test
+    public void exceptionCaughtCallsFireComplete() throws Exception {
+        RequestEncoder encoder = new RequestEncoder();
+        ChannelHandlerContext ctx = mock(ChannelHandlerContext.class);
+        ExceptionEvent evt = mock(ExceptionEvent.class);
+        when(evt.getCause()).thenReturn(new Exception());
+        Channel channel = mock(Channel.class);
+        when(ctx.getChannel()).thenReturn(channel);
+        encoder.exceptionCaught(ctx, evt);
+        // Channels.fireExceptionCaught implicitly calls this
+        verify(ctx).sendUpstream(isA(ExceptionEvent.class));
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/command/src/test/java/com/redhat/thermostat/client/command/internal/ResponseHandlerTest.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2012 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.client.command.internal;
+
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.jboss.netty.channel.Channel;
+import org.jboss.netty.channel.ChannelHandlerContext;
+import org.jboss.netty.channel.ChannelPipeline;
+import org.jboss.netty.channel.ExceptionEvent;
+import org.jboss.netty.channel.MessageEvent;
+import org.junit.Test;
+
+import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.RequestResponseListener;
+import com.redhat.thermostat.common.command.Response;
+import com.redhat.thermostat.common.command.Response.ResponseType;
+
+public class ResponseHandlerTest {
+
+    @Test
+    public void messageReceivedCallsFireComplete() throws Exception {
+        Request req = mock(Request.class);
+        List<RequestResponseListener> listeners = new ArrayList<>();
+        ResponseListenerFixture fixture = new ResponseListenerFixture();
+        listeners.add(fixture);
+        when(req.getListeners()).thenReturn(listeners);
+        ResponseHandler handler = new ResponseHandler(req);
+        ChannelHandlerContext ctxt = mock(ChannelHandlerContext.class);
+        ChannelPipeline pipeline = mock(ChannelPipeline.class);
+        when(ctxt.getPipeline()).thenReturn(pipeline);
+        Channel channel = mock(Channel.class);
+        MessageEvent e = mock(MessageEvent.class);
+        when(e.getChannel()).thenReturn(channel);
+        Response response = mock(Response.class);
+        when(response.getType()).thenReturn(ResponseType.OK);
+        when(e.getMessage()).thenReturn(response);
+        
+        handler.messageReceived(ctxt, e);
+        assertTrue(fixture.isCalled());
+    }
+    
+    @Test
+    public void exceptionCaughtCallsFireComplete() throws Exception {
+        Request req = mock(Request.class);
+        List<RequestResponseListener> listeners = new ArrayList<>();
+        ResponseListenerFixture fixture = new ResponseListenerFixture();
+        listeners.add(fixture);
+        when(req.getListeners()).thenReturn(listeners);
+        ResponseHandler handler = new ResponseHandler(req);
+        handler.exceptionCaught(mock(ChannelHandlerContext.class), mock(ExceptionEvent.class));
+        assertTrue(fixture.isCalled());
+    }
+    
+    private class ResponseListenerFixture implements RequestResponseListener {
+
+        public boolean called = false;
+        
+        public boolean isCalled() {
+            return called;
+        }
+
+        @Override
+        public void fireComplete(Request request, Response response) {
+            called = true;
+        }
+        
+    }
+}
--- a/client/killvm/src/main/java/com/redhat/thermostat/client/killvm/internal/Activator.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/killvm/src/main/java/com/redhat/thermostat/client/killvm/internal/Activator.java	Thu Sep 20 00:33:03 2012 +0200
@@ -45,7 +45,7 @@
 
     @Override
     public void start(BundleContext context) throws Exception {
-        context.registerService(VMContextAction.class.getName(), new KillVMAction(), null);
+        context.registerService(VMContextAction.class.getName(), new KillVMAction(new SwingVMKilledListener()), null);
     }
 
     @Override
--- a/client/killvm/src/main/java/com/redhat/thermostat/client/killvm/internal/KillVMAction.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/killvm/src/main/java/com/redhat/thermostat/client/killvm/internal/KillVMAction.java	Thu Sep 20 00:33:03 2012 +0200
@@ -45,6 +45,7 @@
 import com.redhat.thermostat.common.appctx.ApplicationContext;
 import com.redhat.thermostat.common.command.Request;
 import com.redhat.thermostat.common.command.Request.RequestType;
+import com.redhat.thermostat.common.command.RequestResponseListener;
 import com.redhat.thermostat.common.dao.DAOFactory;
 import com.redhat.thermostat.common.dao.VmRef;
 import com.redhat.thermostat.common.locale.Translate;
@@ -60,10 +61,15 @@
     private static final String RECEIVER = "com.redhat.thermostat.agent.killvm.internal.KillVmReceiver";
     private final DAOFactory dao;
     private final Translate t;
+    private final RequestResponseListener listener;
 
-    public KillVMAction() {
+    public KillVMAction(RequestResponseListener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("Listener can't be null");
+        }
         this.dao = ApplicationContext.getInstance().getDAOFactory();
         this.t = LocaleResources.createLocalizer();
+        this.listener = listener;
     }
 
     @Override
@@ -85,7 +91,7 @@
         Request murderer = getKillRequest(target);
         murderer.setParameter("vm-id", reference.getIdString());
         murderer.setReceiver(RECEIVER);
-        murderer.addListener(new VMKilledListener());
+        murderer.addListener(listener);
 
         RequestQueue queue = OSGIUtils.getInstance().getService(RequestQueue.class);
         queue.putRequest(murderer);
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/killvm/src/main/java/com/redhat/thermostat/client/killvm/internal/SwingVMKilledListener.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2012 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.client.killvm.internal;
+
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.swing.JOptionPane;
+import javax.swing.SwingUtilities;
+
+import com.redhat.thermostat.client.killvm.locale.LocaleResources;
+import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.RequestResponseListener;
+import com.redhat.thermostat.common.command.Response;
+import com.redhat.thermostat.common.locale.Translate;
+
+public class SwingVMKilledListener implements RequestResponseListener {
+
+    private final Translate t;
+    private static final Logger logger = Logger
+            .getLogger(SwingVMKilledListener.class.getName());
+
+    public SwingVMKilledListener() {
+        this.t = LocaleResources.createLocalizer();
+    }
+    
+    @Override
+    public void fireComplete(Request request, Response response) {
+        switch (response.getType()) {
+        case EXCEPTION:
+            logger.log(Level.SEVERE,
+                    "Exception response from kill VM request. Command channel failure?");
+            showErrorMessage(t.localize(LocaleResources.KILL_ACTION_EXCEPTION_RESPONSE_MSG));
+            break;
+        case ERROR:
+            String vmId = request.getParameter("vm-id");
+            logger.log(Level.SEVERE,
+                    "Kill request error for VM ID "
+                            + vmId);
+            showErrorMessage(t.localize(LocaleResources.KILL_ACTION_ERROR_RESPONSE_MSG, vmId));
+            break;
+        case PONG: // fall-through, also OK :)
+        case OK:
+            logger.log(Level.INFO,
+                    "VM with id " + request.getParameter("vm-id")
+                            + " killed on host "
+                            + request.getTarget().toString());
+            break;
+        default:
+            logger.log(Level.WARNING, "Unknown result from KILL VM command.");
+            break;
+        }
+    }
+    
+    /* 
+     * protected for testing
+     * 
+     */
+    protected void showErrorMessage(final String message) {
+        SwingUtilities.invokeLater(new Runnable() {
+            @Override
+            public void run() {
+                JOptionPane.showMessageDialog(null, message, "",
+                        JOptionPane.ERROR_MESSAGE);
+            }
+        });
+    }
+}
--- a/client/killvm/src/main/java/com/redhat/thermostat/client/killvm/internal/VMKilledListener.java	Thu Sep 20 00:28:12 2012 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,76 +0,0 @@
-/*
- * Copyright 2012 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.client.killvm.internal;
-
-import java.util.logging.Level;
-import java.util.logging.Logger;
-
-import com.redhat.thermostat.common.command.Request;
-import com.redhat.thermostat.common.command.RequestResponseListener;
-import com.redhat.thermostat.common.command.Response;
-
-public class VMKilledListener implements RequestResponseListener {
-
-    private static final Logger logger = Logger
-            .getLogger(VMKilledListener.class.getName());
-
-    @Override
-    public void fireComplete(Request request, Response response) {
-        switch (response.getType()) {
-        case EXCEPTION:
-            logger.log(Level.SEVERE,
-                    "Exception response from kill VM request. Command channel failure?");
-            break;
-        case ERROR:
-            logger.log(Level.SEVERE,
-                    "Kill request error for VM ID "
-                            + request.getParameter("vm-id"));
-            break;
-        case PONG: // fall-through, also OK :)
-        case OK:
-            // TODO: Report this to user somehow (notification?)
-            logger.log(Level.INFO,
-                    "VM with id " + request.getParameter("vm-id")
-                            + " killed on host "
-                            + request.getTarget().toString());
-            break;
-        default:
-            logger.log(Level.WARNING, "Unknown result from KILL VM command.");
-            break;
-        }
-    }
-}
--- a/client/killvm/src/main/java/com/redhat/thermostat/client/killvm/locale/LocaleResources.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/killvm/src/main/java/com/redhat/thermostat/client/killvm/locale/LocaleResources.java	Thu Sep 20 00:33:03 2012 +0200
@@ -42,6 +42,8 @@
 
     ACTION_NAME,
     ACTION_DESCRIPTION,
+    KILL_ACTION_EXCEPTION_RESPONSE_MSG,
+    KILL_ACTION_ERROR_RESPONSE_MSG,
     MISSING_INFO;
 
     public static final String RESOURCE_BUNDLE =
--- a/client/killvm/src/main/resources/com/redhat/thermostat/client/killvm/locale/strings.properties	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/killvm/src/main/resources/com/redhat/thermostat/client/killvm/locale/strings.properties	Thu Sep 20 00:33:03 2012 +0200
@@ -1,3 +1,5 @@
 ACTION_NAME = Kill Application
 ACTION_DESCRIPTION = Kill the selected VM Process
-MISSING_INFO = Missing Information
\ No newline at end of file
+MISSING_INFO = Missing Information
+KILL_ACTION_EXCEPTION_RESPONSE_MSG = Channel Transport Error. Please check your configuration!
+KILL_ACTION_ERROR_RESPONSE_MSG = Failed to kill VM with pid {0}
\ No newline at end of file
--- a/client/killvm/src/test/java/com/redhat/thermostat/client/killvm/internal/KillVMActionTest.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/killvm/src/test/java/com/redhat/thermostat/client/killvm/internal/KillVMActionTest.java	Thu Sep 20 00:33:03 2012 +0200
@@ -83,7 +83,7 @@
         ApplicationContextUtil.resetApplicationContext();
         factory = mock(DAOFactory.class);
         ApplicationContext.getInstance().setDAOFactory(factory);
-        action = new KillVMAction();
+        action = new KillVMAction(new SwingVMKilledListener());
     }
 
     @After
@@ -124,7 +124,7 @@
         when(factory.getAgentInfoDAO()).thenReturn(agentDao);
 
         final Request req = mock(Request.class);
-        KillVMAction action = new KillVMAction() {
+        KillVMAction action = new KillVMAction(new SwingVMKilledListener()) {
             @Override
             Request getKillRequest(InetSocketAddress target) {
                 return req;
@@ -140,7 +140,7 @@
                 .forClass(String.class);
         verify(req).setParameter(vmIdParamCaptor.capture(), any(String.class));
         assertEquals("vm-id", vmIdParamCaptor.getValue());
-        verify(req).addListener(isA(VMKilledListener.class));
+        verify(req).addListener(isA(SwingVMKilledListener.class));
         ArgumentCaptor<String> receiverCaptor = ArgumentCaptor
                 .forClass(String.class);
         verify(req).setReceiver(receiverCaptor.capture());
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/killvm/src/test/java/com/redhat/thermostat/client/killvm/internal/SwingVMKilledListenerTest.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2012 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.client.killvm.internal;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.SocketAddress;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.common.command.Request;
+import com.redhat.thermostat.common.command.Response;
+import com.redhat.thermostat.common.command.Response.ResponseType;
+
+public class SwingVMKilledListenerTest {
+    
+    @Test
+    public void exceptionShowsError() {
+        ResponseActionListener listener = new ResponseActionListener();
+        Request request = mock(Request.class); 
+        Response resp = new Response(ResponseType.EXCEPTION);
+        listener.fireComplete(request, resp);
+        assertTrue(listener.isActionPerformed());
+    }
+    
+    @Test
+    public void errorShowsError() {
+        ResponseActionListener listener = new ResponseActionListener();
+        Request request = mock(Request.class); 
+        Response resp = new Response(ResponseType.ERROR);
+        listener.fireComplete(request, resp);
+        assertTrue(listener.isActionPerformed());
+    }
+    
+    @Test
+    public void okShowsNoMessage() {
+        ResponseActionListener listener = new ResponseActionListener();
+        Request request = mock(Request.class);
+        SocketAddress addr = mock(SocketAddress.class);
+        when(request.getTarget()).thenReturn(addr);
+        Response resp = new Response(ResponseType.OK);
+        listener.fireComplete(request, resp);
+        assertFalse(listener.isActionPerformed());
+    }
+    
+    private class ResponseActionListener extends SwingVMKilledListener {
+
+        private boolean actionPerformed = false;
+        
+        public boolean isActionPerformed() {
+            return actionPerformed;
+        }
+
+        @Override
+        protected void showErrorMessage(final String message) {
+            actionPerformed = true;
+        }
+        
+    }
+}
--- a/client/swing-components/src/main/java/com/redhat/thermostat/swing/ChartPanel.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/swing-components/src/main/java/com/redhat/thermostat/swing/ChartPanel.java	Thu Sep 20 00:33:03 2012 +0200
@@ -51,7 +51,7 @@
     private ChartRenderingInfo info;
     
     public ChartPanel(JFreeChart chart) {
-        this.chart = chart;
+        this(chart, new ChartRenderingInfo());
     }
     
     public ChartPanel(JFreeChart chart, ChartRenderingInfo info) {
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing-components/src/main/java/com/redhat/thermostat/swing/GradientRoundBorder.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012 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.swing;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.GradientPaint;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Shape;
+import java.io.Serializable;
+
+import javax.swing.UIManager;
+import javax.swing.border.AbstractBorder;
+import javax.swing.plaf.UIResource;
+
+@SuppressWarnings("serial")
+public class GradientRoundBorder extends AbstractBorder implements UIResource, Serializable {
+
+    @Override
+    public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
+        GraphicsUtils utils = GraphicsUtils.getInstance();
+        Graphics2D graphics = utils.createAAGraphics(g);
+        
+        Color highlight = UIManager.getColor("textHighlight");
+        if (highlight == null) {
+            highlight = Palette.EGYPTIAN_BLUE.getColor();
+        }
+        Paint paint = new GradientPaint(x, y, highlight, 0, height, c.getBackground());
+        graphics.setPaint(paint);
+        
+        graphics.translate(x, y);
+        
+        Shape shape = utils.getRoundShape(width, height);
+        graphics.draw(shape);
+        
+        graphics.dispose();
+    }
+}
--- a/client/swing-components/src/main/java/com/redhat/thermostat/swing/GraphicsUtils.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/swing-components/src/main/java/com/redhat/thermostat/swing/GraphicsUtils.java	Thu Sep 20 00:33:03 2012 +0200
@@ -85,7 +85,7 @@
     }
     
     public Shape getRoundShape(int width, int height) {
-        return new RoundRectangle2D.Double(0, 0, width - 2, height - 1, 4, 4);
+        return new RoundRectangle2D.Double(0, 0, width, height, 4, 4);
     }
     
     public void setGradientPaint(Graphics2D g, int x, int height, Color start, Color stop) {
--- a/client/swing-components/src/main/java/com/redhat/thermostat/swing/HeaderPanel.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/client/swing-components/src/main/java/com/redhat/thermostat/swing/HeaderPanel.java	Thu Sep 20 00:33:03 2012 +0200
@@ -43,8 +43,6 @@
 import java.lang.reflect.InvocationTargetException;
 
 import javax.swing.BoxLayout;
-import javax.swing.ImageIcon;
-import javax.swing.JButton;
 import javax.swing.JComponent;
 import javax.swing.JFrame;
 import javax.swing.JLabel;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing-components/src/main/java/com/redhat/thermostat/swing/models/LongRange.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2012 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.swing.models;
+
+public class LongRange {
+
+    long min;
+    long max;
+    
+    public void setMax(long max) {
+        this.max = max;
+    }
+    
+    public long getMax() {
+        return max;
+    }
+    
+    public void setMin(long min) {
+        this.min = min;
+    }
+    
+    
+    public long getMin() {
+        return min;
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing-components/src/main/java/com/redhat/thermostat/swing/models/LongRangeNormalizer.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2012 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.swing.models;
+
+public class LongRangeNormalizer {
+
+    private long minNormalized;
+    
+    private long maxNormalized;
+ 
+    private long value;
+
+    private LongRange range;
+    
+    public LongRangeNormalizer(LongRange range) {
+        this.range = range;
+    }
+
+    public void setMaxNormalized(long maxNormalized) {
+        this.maxNormalized = maxNormalized;
+    }
+    
+    public void setMinNormalized(long minNormalized) {
+        this.minNormalized = minNormalized;
+    }
+    
+    public long getValue() {
+        return value;
+    }
+
+    public void setValue(long newValue) {
+        this.value = newValue;
+    }
+    
+    public long getMaxNormalized() {
+        return maxNormalized;
+    }
+    
+    public long getMinNormalized() {
+        return minNormalized;
+    }
+    
+    public long getValueNormalized() {
+        double normalized = ((value - range.min) * (double)(maxNormalized - minNormalized)/(range.max - range.min)) + minNormalized;
+        return Math.round(normalized);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/client/swing-components/src/test/java/com/redhat/thermostat/swing/models/LongRangeNormalizerTest.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2012 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.swing.models;
+
+import static org.junit.Assert.*;
+import junit.framework.Assert;
+
+import org.junit.Test;
+
+public class LongRangeNormalizerTest {
+
+    @Test
+    public void testSameRange() {
+        
+        LongRange range = new LongRange();
+        range.setMax(10);
+        range.setMin(0);
+        
+        LongRangeNormalizer model = new LongRangeNormalizer(range);
+        
+        model.setValue(5);
+        
+        model.setMaxNormalized(10);
+        model.setMinNormalized(0);
+        
+        
+        Assert.assertEquals((int) model.getValue(), model.getValueNormalized());
+    }
+
+    @Test
+    public void testDoubleRange() {
+        
+        LongRange range = new LongRange();
+        range.setMax(10);
+        range.setMin(0);
+        
+        LongRangeNormalizer model = new LongRangeNormalizer(range);
+        
+        model.setValue(5);
+        
+        model.setMaxNormalized(20);
+        model.setMinNormalized(0);
+        
+        
+        Assert.assertEquals(10, model.getValueNormalized());
+    }
+    
+    @Test
+    public void testRanges() {
+        
+        LongRange range = new LongRange();
+        range.setMax(10);
+        range.setMin(0);
+        
+        LongRangeNormalizer model = new LongRangeNormalizer(range);
+        
+        model.setValue(5);
+        
+        model.setMaxNormalized(40);
+        model.setMinNormalized(0);
+                
+        Assert.assertEquals(20, model.getValueNormalized());
+        
+        model.setMaxNormalized(60);
+        model.setMinNormalized(0);
+                
+        Assert.assertEquals(30, model.getValueNormalized());
+                
+        model.setMaxNormalized(200);
+        model.setMinNormalized(100);
+                
+        Assert.assertEquals(150, model.getValueNormalized());
+        
+        range.setMax(100);
+        range.setMin(0);
+        model.setValue(50);
+        
+        model.setMaxNormalized(1);
+        model.setMinNormalized(0);
+                
+        Assert.assertEquals(1, model.getValueNormalized());
+        
+        model.setValue(49);
+        Assert.assertEquals(0, model.getValueNormalized());
+    }
+}
--- a/distribution/config/osgi-export.properties	Thu Sep 20 00:28:12 2012 +0200
+++ b/distribution/config/osgi-export.properties	Thu Sep 20 00:33:03 2012 +0200
@@ -50,6 +50,7 @@
 org.jfree.data.time
 org.jfree.data.xy
 org.jfree.ui
+org.jfree.data.gantt
 sun.swing
 sun.swing.table
 
--- a/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/launcher/src/main/java/com/redhat/thermostat/launcher/internal/LauncherImpl.java	Thu Sep 20 00:33:03 2012 +0200
@@ -196,8 +196,7 @@
             // If this happens we definitely need to do something about it, and the
             // trace will be immeasurably helpful in figuring out what is wrong.
             out.println("Could not load necessary bundles for: " + cmdName);
-            e.printStackTrace();
-            out.print(e.getStackTrace());
+            e.printStackTrace(out);
             return;
         }
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadTimelineBean.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,86 @@
+/*
+ * Copyright 2012 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.thread.client.common;
+
+import java.util.Date;
+
+public class ThreadTimelineBean {
+
+    private long startTime;
+    private long stopTime;
+    private String name;
+    private Thread.State state;
+    
+    public Thread.State getState() {
+        return state;
+    }
+    
+    public void setState(Thread.State state) {
+        this.state = state;
+    }
+    
+    public String getName() {
+        return name;
+    }
+    
+    public void setName(String name) {
+        this.name = name;
+    }
+    
+    public long getStartTime() {
+        return startTime;
+    }
+    
+    public void setStartTime(long startTime) {
+        this.startTime = startTime;
+    }
+    
+    public long getStopTime() {
+        return stopTime;
+    }
+    
+    public void setStopTime(long stopTime) {
+        this.stopTime = stopTime;
+    }
+
+    @Override
+    public String toString() {
+        return "ThreadTimelineBean [name=" + name + ", state=" + state
+                + ", startTime=" + startTime + " (" + new Date(startTime) + ")"
+                + ", stopTime=" + stopTime + " (" + new Date(stopTime) + ")]";
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadTimelineView.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2012 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.thread.client.common;
+
+import java.util.List;
+import java.util.Map;
+
+import com.redhat.thermostat.client.osgi.service.BasicView;
+import com.redhat.thermostat.thread.model.ThreadInfoData;
+
+public abstract class ThreadTimelineView extends BasicView {
+
+    public abstract void displayStats(Map<ThreadInfoData, List<ThreadTimelineBean>> timelines, long start, long stop);
+
+}
--- a/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadView.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/ThreadView.java	Thu Sep 20 00:33:03 2012 +0200
@@ -74,6 +74,7 @@
     
     public abstract VMThreadCapabilitiesView createVMThreadCapabilitiesView();
     public abstract ThreadTableView createThreadTableView();
+    public abstract ThreadTimelineView createThreadTimelineView();
     
     public abstract void displayWarning(String warning);
 
--- a/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/locale/LocaleResources.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-common/src/main/java/com/redhat/thermostat/thread/client/common/locale/LocaleResources.java	Thu Sep 20 00:33:03 2012 +0200
@@ -57,6 +57,7 @@
     VM_CAPABILITIES,
     TABLE,
     DETAILS,
+    TIMELINE,
     
     LIVE_THREADS,
     DAEMON_THREADS,
--- a/thread/client-common/src/main/resources/com/redhat/thermostat/thread/client/common/locale/strings.properties	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-common/src/main/resources/com/redhat/thermostat/thread/client/common/locale/strings.properties	Thu Sep 20 00:33:03 2012 +0200
@@ -16,6 +16,7 @@
 VM_CAPABILITIES = VM Capabilities
 TABLE = Table
 DETAILS = Details
+TIMELINE = Timeline
 
 LIVE_THREADS = Live Threads
 DAEMON_THREADS = Daemon Threads
--- a/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/CommonController.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/CommonController.java	Thu Sep 20 00:33:03 2012 +0200
@@ -36,6 +36,47 @@
 
 package com.redhat.thermostat.thread.client.controller.impl;
 
-public interface CommonController {
-    void initialize();
+import java.util.concurrent.TimeUnit;
+
+import com.redhat.thermostat.client.osgi.service.BasicView;
+import com.redhat.thermostat.client.osgi.service.BasicView.Action;
+import com.redhat.thermostat.common.ActionEvent;
+import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.Timer;
+import com.redhat.thermostat.common.Timer.SchedulingType;
+
+public abstract class CommonController {
+    
+    protected Timer timer;
+    protected BasicView view;
+    
+    public CommonController(Timer timer, BasicView view) {
+        this.view = view;
+        this.timer = timer;
+    }
+    
+    void initialize() {
+        timer.setInitialDelay(0);
+        timer.setDelay(1000);
+        timer.setTimeUnit(TimeUnit.MILLISECONDS);
+        timer.setSchedulingType(SchedulingType.FIXED_RATE);
+        
+        view.addActionListener(new ActionListener<Action>() {
+            @Override
+            public void actionPerformed(ActionEvent<Action> actionEvent) {
+                switch (actionEvent.getActionId()) {
+                case VISIBLE:
+                    timer.start();
+                    break;
+
+                case HIDDEN:
+                    timer.stop();
+                    break;
+
+                default:
+                    break;
+                }
+            }
+        });
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/ThreadInfoHelper.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2012 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.thread.client.controller.impl;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import com.redhat.thermostat.thread.model.ThreadInfoData;
+
+public class ThreadInfoHelper {
+
+    /**
+     * Creates a {@link Map} whose keys are {@link ThreadInfoData} in the input
+     * list and whose values are all the {@link ThreadInfoData} equals to the
+     * key.
+     *
+     * <br /><br />
+     * 
+     * Preserves the order of the input list.
+     * 
+     * <br /><br />
+     * 
+     * <strong>NOTE</strong>: The current invariant is that
+     * {@link ThreadInfoData} are equals if they have same thread id and name.
+     */
+    public static Map<ThreadInfoData, List<ThreadInfoData>> getThreadInfoDataMap(List<ThreadInfoData> infos) {
+        Map<ThreadInfoData, List<ThreadInfoData>> stats = new HashMap<>();
+        for (ThreadInfoData info : infos) {
+            List<ThreadInfoData> beanList = stats.get(info);
+            if (beanList == null) {
+                beanList = new ArrayList<ThreadInfoData>();
+                stats.put(info, beanList);
+            }                    
+            beanList.add(info);
+        }
+        return stats;
+    }
+}
--- a/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/ThreadInformationController.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/ThreadInformationController.java	Thu Sep 20 00:33:03 2012 +0200
@@ -197,7 +197,8 @@
     }
     
     private void initControllers() {
-        CommonController capsController =
+        
+        VMThreadCapabilitiesController capsController =
                 new VMThreadCapabilitiesController(view.createVMThreadCapabilitiesView(), collector);
         capsController.initialize();
         
@@ -207,5 +208,10 @@
                 new ThreadTableController(threadTableView, collector,
                                           ApplicationContext.getInstance().getTimerFactory().createTimer());
         threadTableController.initialize();
+        
+        CommonController threadTimeline =
+                new ThreadTimelineController(view.createThreadTimelineView(), collector,
+                                             ApplicationContext.getInstance().getTimerFactory().createTimer());
+        threadTimeline.initialize();
     }
 }
--- a/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/ThreadTableController.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/ThreadTableController.java	Thu Sep 20 00:33:03 2012 +0200
@@ -41,60 +41,29 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.TimeUnit;
 
-import com.redhat.thermostat.client.osgi.service.BasicView.Action;
-import com.redhat.thermostat.common.ActionEvent;
-import com.redhat.thermostat.common.ActionListener;
 import com.redhat.thermostat.common.Timer;
-import com.redhat.thermostat.common.Timer.SchedulingType;
 import com.redhat.thermostat.thread.client.common.ThreadTableBean;
 import com.redhat.thermostat.thread.client.common.ThreadTableView;
 import com.redhat.thermostat.thread.client.common.collector.ThreadCollector;
 import com.redhat.thermostat.thread.model.ThreadInfoData;
 
-public class ThreadTableController implements CommonController {
+public class ThreadTableController extends CommonController {
     
     private ThreadTableView threadTableView;
     private ThreadCollector collector;
-    private Timer timer;
     
     public ThreadTableController(ThreadTableView threadTableView,
                                  ThreadCollector collector,
                                  Timer timer)
     {
+        super(timer, threadTableView);
+        timer.setAction(new ThreadTableControllerAction());
+
         this.collector = collector;
         this.threadTableView = threadTableView;
-        this.timer = timer;
     }
 
-    @Override
-    public void initialize() {
-        
-        timer.setInitialDelay(0);
-        timer.setDelay(1000);
-        timer.setTimeUnit(TimeUnit.MILLISECONDS);
-        timer.setSchedulingType(SchedulingType.FIXED_RATE);
-        timer.setAction(new ThreadTableControllerAction());
-        
-        threadTableView.addActionListener(new ActionListener<Action>() {
-            @Override
-            public void actionPerformed(ActionEvent<Action> actionEvent) {
-                switch (actionEvent.getActionId()) {
-                case VISIBLE:
-                    timer.start();
-                    break;
-
-                case HIDDEN:
-                    timer.stop();
-                    break;
-
-                default:
-                    break;
-                }
-            }
-        });
-    }
     
     private class ThreadTableControllerAction implements Runnable {
         @Override
@@ -110,15 +79,8 @@
                 // first, get a map of all threads with the respective info
                 // the list will contain an ordered-by-timestamp list
                 // with the known history for each thread
-                Map<ThreadInfoData, List<ThreadInfoData>> stats = new HashMap<>();
-                for (ThreadInfoData info : infos) {
-                    List<ThreadInfoData> beanList = stats.get(info);
-                    if (beanList == null) {
-                        beanList = new ArrayList<ThreadInfoData>();
-                        stats.put(info, beanList);
-                    }                    
-                    beanList.add(info);
-                }
+                Map<ThreadInfoData, List<ThreadInfoData>> stats =
+                        ThreadInfoHelper.getThreadInfoDataMap(infos);
                 
                 List<ThreadTableBean> tableBeans = new ArrayList<>();
                 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/ThreadTimelineController.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2012 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.thread.client.controller.impl;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Stack;
+
+import com.redhat.thermostat.common.Timer;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineBean;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineView;
+import com.redhat.thermostat.thread.client.common.collector.ThreadCollector;
+import com.redhat.thermostat.thread.model.ThreadInfoData;
+
+public class ThreadTimelineController extends CommonController {
+
+    private ThreadTimelineView view;
+    private ThreadCollector collector;
+    
+    public ThreadTimelineController(ThreadTimelineView view, ThreadCollector collector, Timer timer) {
+        super(timer, view);
+        timer.setAction(new ThreadTimelineControllerAction());
+        this.view = view;
+        this.collector = collector;
+    }
+
+    private class ThreadTimelineControllerAction implements Runnable {
+        @Override
+        public void run() {
+            List<ThreadInfoData> infos = collector.getThreadInfo();
+            if(infos.size() > 0) {
+                
+                Map<ThreadInfoData, List<ThreadTimelineBean>> timelines = new HashMap<>();
+                
+                Map<ThreadInfoData, List<ThreadInfoData>> stats =
+                        ThreadInfoHelper.getThreadInfoDataMap(infos);
+                for (List<ThreadInfoData> beanList : stats.values()) {
+                    
+                    // the list is ordered in most recent first
+                    // the first element is the latest sample we have of this
+                    // thread, so we use it as stop time. 
+                    
+                    ThreadInfoData lastThreadInfo = beanList.get(beanList.size() - 1);
+                    Thread.State lastState = lastThreadInfo.getState();
+                    
+                    ThreadTimelineBean timeline = new ThreadTimelineBean();
+                    timeline.setName(lastThreadInfo.getThreadName());
+                    timeline.setState(lastThreadInfo.getState());
+                    timeline.setStartTime(lastThreadInfo.getTimeStamp()); 
+                    
+                    long stopTime = beanList.get(0).getTimeStamp();
+                    timeline.setStopTime(stopTime);
+                    
+                    Stack<ThreadTimelineBean> threadTimelines = new Stack<ThreadTimelineBean>();
+                    timelines.put(lastThreadInfo, threadTimelines);
+                    threadTimelines.push(timeline);
+                    
+                    for (int i = beanList.size() - 1; i >= 0; i--) {
+                        ThreadInfoData threadInfo = beanList.get(i);
+                        
+                        Thread.State currentState = threadInfo.getState();
+                        if (currentState != lastState) {
+                            lastState = currentState;
+                            
+                            timeline = threadTimelines.pop();
+                            timeline.setStopTime(threadInfo.getTimeStamp());
+                            
+                            threadTimelines.push(timeline);
+                            
+                            timeline = new ThreadTimelineBean();
+                            timeline.setName(threadInfo.getThreadName());
+                            timeline.setState(threadInfo.getState());
+                            timeline.setStartTime(threadInfo.getTimeStamp());
+                            timeline.setStopTime(stopTime);
+
+                            lastThreadInfo = threadInfo;
+                            lastState = currentState;
+                            
+                            // add the new thread stat
+                            threadTimelines.push(timeline);
+                        }
+                    }
+                }
+                
+                view.displayStats(timelines, infos.get(infos.size() - 1).getTimeStamp(), infos.get(0).getTimeStamp());
+            }
+        }
+    }
+}
--- a/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/VMThreadCapabilitiesController.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-controllers/src/main/java/com/redhat/thermostat/thread/client/controller/impl/VMThreadCapabilitiesController.java	Thu Sep 20 00:33:03 2012 +0200
@@ -43,7 +43,7 @@
 import com.redhat.thermostat.thread.client.common.collector.ThreadCollector;
 import com.redhat.thermostat.thread.model.VMThreadCapabilities;
 
-public class VMThreadCapabilitiesController implements CommonController {
+public class VMThreadCapabilitiesController {
 
     private ThreadCollector collector;
     private VMThreadCapabilitiesView view;
@@ -53,7 +53,6 @@
         this.collector = collector;
     }
     
-    @Override
     public void initialize() {
         view.addActionListener(new ActionListener<Action>() {
             @Override
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-controllers/src/test/java/com/redhat/thermostat/thread/client/controller/impl/ThreadInfoHelperTest.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,94 @@
+/*
+ * Copyright 2012 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.thread.client.controller.impl;
+
+import static org.junit.Assert.*;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Test;
+
+import com.redhat.thermostat.thread.model.ThreadInfoData;
+
+public class ThreadInfoHelperTest {
+
+    @Test
+    public void verifyMap() {
+        ThreadInfoData data1 = new ThreadInfoData();
+        data1.setThreadName("test1");
+        data1.setThreadId(1);
+        data1.setState(Thread.State.RUNNABLE);
+        
+        ThreadInfoData data2 = new ThreadInfoData();
+        data2.setThreadName("test2");
+        data2.setThreadId(2);
+        data2.setState(Thread.State.BLOCKED);
+        
+        ThreadInfoData data3 = new ThreadInfoData();
+        data3.setThreadName("test1");
+        data3.setThreadId(1);
+        data3.setState(Thread.State.TIMED_WAITING);
+        
+        ThreadInfoData data4 = new ThreadInfoData();
+        data4.setThreadName("test2");
+        data4.setThreadId(2);
+        data4.setState(Thread.State.RUNNABLE);
+        
+        List<ThreadInfoData> infos = new ArrayList<>();
+        infos.add(data1);
+        infos.add(data2);
+        infos.add(data3);
+        infos.add(data4);
+        
+        Map<ThreadInfoData, List<ThreadInfoData>> result = ThreadInfoHelper.getThreadInfoDataMap(infos);
+        assertEquals(2, result.size());
+        
+        assertTrue(result.containsKey(data1));
+        assertTrue(result.containsKey(data2));
+        
+        assertEquals(2, result.get(data1).size());
+        assertEquals(2, result.get(data2).size());
+        
+        assertEquals(Thread.State.RUNNABLE, result.get(data1).get(0).getState());
+        assertEquals(Thread.State.TIMED_WAITING, result.get(data1).get(1).getState());       
+
+        assertEquals(Thread.State.BLOCKED, result.get(data2).get(0).getState());
+        assertEquals(Thread.State.RUNNABLE, result.get(data2).get(1).getState());
+    }
+}
--- a/thread/client-controllers/src/test/java/com/redhat/thermostat/thread/client/controller/impl/ThreadInformationControllerTest.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-controllers/src/test/java/com/redhat/thermostat/thread/client/controller/impl/ThreadInformationControllerTest.java	Thu Sep 20 00:33:03 2012 +0200
@@ -60,6 +60,7 @@
 import com.redhat.thermostat.thread.client.common.ThreadTableBean;
 import com.redhat.thermostat.thread.client.common.ThreadTableView;
 import com.redhat.thermostat.thread.client.common.ThreadTableView.ThreadSelectionAction;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineView;
 import com.redhat.thermostat.thread.client.common.ThreadView;
 import com.redhat.thermostat.thread.client.common.ThreadViewProvider;
 import com.redhat.thermostat.thread.client.common.VMThreadCapabilitiesView;
@@ -80,6 +81,7 @@
 
     private ThreadTableView threadTableView;
     private VMThreadCapabilitiesView threadCapsView;
+    private ThreadTimelineView threadTimelineView;
     
     @Before
     public void setUp() {
@@ -96,13 +98,15 @@
     private void setUpView() {
         threadCapsView = mock(VMThreadCapabilitiesView.class);
         threadTableView = mock(ThreadTableView.class);
-
+        threadTimelineView = mock(ThreadTimelineView.class);
+        
         view = mock(ThreadView.class);
         viewFactory = mock(ThreadViewProvider.class);
         when(viewFactory.createView()).thenReturn(view);
         
         when(view.createVMThreadCapabilitiesView()).thenReturn(threadCapsView);
         when(view.createThreadTableView()).thenReturn(threadTableView);
+        when(view.createThreadTimelineView()).thenReturn(threadTimelineView);
     }
     
     private void setUpTimers() {
@@ -152,6 +156,7 @@
         
         verify(view).createThreadTableView();
         verify(view).createVMThreadCapabilitiesView();
+        verify(view).createThreadTimelineView();
     }
     
     @Test
--- a/thread/client-controllers/src/test/java/com/redhat/thermostat/thread/client/controller/impl/ThreadTableControllerTest.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-controllers/src/test/java/com/redhat/thermostat/thread/client/controller/impl/ThreadTableControllerTest.java	Thu Sep 20 00:33:03 2012 +0200
@@ -68,7 +68,7 @@
         collector = mock(ThreadCollector.class);
         
         timer = mock(Timer.class);
-                
+
         view = mock(ThreadTableView.class);
         
         setUpTimers();
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-controllers/src/test/java/com/redhat/thermostat/thread/client/controller/impl/ThreadTimelineControllerTest.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2012 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.thread.client.controller.impl;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.anyLong;
+import static org.mockito.Mockito.when;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+
+import com.redhat.thermostat.client.osgi.service.BasicView;
+import com.redhat.thermostat.common.ActionEvent;
+import com.redhat.thermostat.common.ActionListener;
+import com.redhat.thermostat.common.Timer;
+import com.redhat.thermostat.common.appctx.ApplicationContextUtil;
+import com.redhat.thermostat.thread.client.common.ThreadTableView;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineBean;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineView;
+import com.redhat.thermostat.thread.client.common.collector.ThreadCollector;
+import com.redhat.thermostat.thread.model.ThreadInfoData;
+
+public class ThreadTimelineControllerTest {
+
+    private ThreadTimelineView view;
+    private ThreadCollector collector;
+    
+    private Timer timer;
+    
+    private ActionListener<ThreadTableView.Action> actionListener;
+    ArgumentCaptor<Runnable> timerActionCaptor;
+    
+    @Before
+    public void setUp() {
+        ApplicationContextUtil.resetApplicationContext();
+        collector = mock(ThreadCollector.class);
+        
+        timer = mock(Timer.class);
+        
+        view = mock(ThreadTimelineView.class);
+        
+        setUpTimers();
+    }
+    
+    private void setUpTimers() {
+        timer = mock(Timer.class);
+        timerActionCaptor = ArgumentCaptor.forClass(Runnable.class);
+        doNothing().when(timer).setAction(timerActionCaptor.capture());
+    }
+    
+    @Test
+    public void testDisplayStats() {
+        
+        ThreadInfoData data1 = new ThreadInfoData();
+        data1.setThreadName("test1");
+        data1.setThreadId(1);
+        data1.setState(Thread.State.RUNNABLE);
+        data1.setTimeStamp(100);
+        
+        ThreadInfoData data2 = new ThreadInfoData();
+        data2.setThreadName("test2");
+        data2.setThreadId(2);
+        data2.setTimeStamp(1000);
+        data2.setState(Thread.State.BLOCKED);
+        
+        ThreadInfoData data3 = new ThreadInfoData();
+        data3.setThreadName("test1");
+        data3.setThreadId(1);
+        data3.setState(Thread.State.TIMED_WAITING);
+        data3.setTimeStamp(200);
+        
+        ThreadInfoData data4 = new ThreadInfoData();
+        data4.setThreadName("test2");
+        data4.setThreadId(2);
+        data4.setState(Thread.State.BLOCKED);
+        data4.setTimeStamp(2000);
+        
+        ThreadInfoData data5 = new ThreadInfoData();
+        data5.setThreadName("test2");
+        data5.setThreadId(2);
+        data5.setState(Thread.State.RUNNABLE);
+        data5.setTimeStamp(3000);
+        
+        List<ThreadInfoData> infos = new ArrayList<>();
+        // descending order
+        infos.add(data5);
+        infos.add(data4);
+        infos.add(data2);
+        infos.add(data3);
+        infos.add(data1);
+
+        when(collector.getThreadInfo()).thenReturn(infos);
+        
+        ArgumentCaptor<Runnable> timerCaptor = ArgumentCaptor.forClass(Runnable.class);
+        doNothing().when(timer).setAction(timerCaptor.capture());
+        
+        ThreadTimelineController controller = new ThreadTimelineController(view, collector, timer);
+        controller.initialize();
+        
+        Runnable action = timerCaptor.getValue();
+        action.run();
+        
+        ArgumentCaptor<Map> mapCaptor = ArgumentCaptor.forClass(Map.class);
+        
+        verify(view).displayStats(mapCaptor.capture(), anyLong(), anyLong());
+        
+        Map viewResult = mapCaptor.getValue();
+        assertEquals(2, viewResult.size());
+        
+        List<ThreadTimelineBean> beanList = (List<ThreadTimelineBean>) viewResult.get(data1);
+        
+        assertEquals(2, beanList.size());
+        
+        assertEquals("test1", beanList.get(0).getName());
+        assertEquals("test1", beanList.get(1).getName());
+
+        beanList = (List<ThreadTimelineBean>) viewResult.get(data2);
+        assertEquals(2, beanList.size());
+        
+        assertEquals("test2", beanList.get(0).getName());
+        assertEquals("test2", beanList.get(1).getName());
+
+        assertEquals(1000, beanList.get(0).getStartTime());
+        assertEquals(3000, beanList.get(0).getStopTime());
+        assertEquals(Thread.State.BLOCKED, beanList.get(0).getState());
+
+        assertEquals(3000, beanList.get(1).getStartTime());
+        assertEquals(Thread.State.BLOCKED, beanList.get(0).getState());        
+    }
+
+    @Test
+    public void testStartThreadTimelineController() {
+        
+        ArgumentCaptor<ActionListener> viewArgumentCaptor = ArgumentCaptor.forClass(ActionListener.class);
+        doNothing().when(view).addActionListener(viewArgumentCaptor.capture());
+        
+        ThreadTimelineController controller = new ThreadTimelineController(view, collector, timer);
+        controller.initialize();
+
+        actionListener = viewArgumentCaptor.getValue();
+        actionListener.actionPerformed(new ActionEvent<>(view, BasicView.Action.VISIBLE));
+        
+        verify(timer).start();
+        
+        actionListener.actionPerformed(new ActionEvent<>(view, BasicView.Action.HIDDEN));
+
+        verify(timer).stop();
+    }
+}
--- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadDetailsView.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadDetailsView.java	Thu Sep 20 00:33:03 2012 +0200
@@ -36,8 +36,11 @@
 
 package com.redhat.thermostat.thread.client.swing.impl;
 
+import java.awt.BorderLayout;
 import java.awt.Component;
 
+import javax.swing.ImageIcon;
+import javax.swing.JLabel;
 import javax.swing.JPanel;
 
 import com.redhat.thermostat.client.ui.SwingComponent;
@@ -48,15 +51,6 @@
 import com.redhat.thermostat.thread.client.common.chart.ThreadDeatailsPieChart;
 import com.redhat.thermostat.thread.client.common.locale.LocaleResources;
 
-import javax.swing.GroupLayout;
-import javax.swing.GroupLayout.Alignment;
-import javax.swing.ImageIcon;
-import javax.swing.JLabel;
-import java.awt.GridBagLayout;
-import java.awt.GridBagConstraints;
-import java.awt.BorderLayout;
-import java.awt.GridLayout;
-
 public class SwingThreadDetailsView extends ThreadDetailsView implements SwingComponent {
 
     private JPanel details;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineChart.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2012 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.thread.client.swing.impl;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.GradientPaint;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Paint;
+import java.awt.Shape;
+import java.awt.geom.Area;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+
+import com.redhat.thermostat.swing.GradientRoundBorder;
+import com.redhat.thermostat.swing.GraphicsUtils;
+import com.redhat.thermostat.swing.Palette;
+import com.redhat.thermostat.swing.models.LongRange;
+import com.redhat.thermostat.swing.models.LongRangeNormalizer;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineBean;
+
+@SuppressWarnings("serial")
+public class SwingThreadTimelineChart extends JPanel {
+
+    private List<ThreadTimelineBean> timeline;
+    
+    private LongRangeNormalizer normalizer;
+
+    public SwingThreadTimelineChart(List<ThreadTimelineBean> timeline, long rangeStart, long rangeStop) {
+        this.timeline = timeline;
+    
+        LongRange range = new LongRange();
+        range.setMin(rangeStart);
+        range.setMax(rangeStop);
+        
+        setBorder(new GradientRoundBorder());
+        normalizer = new LongRangeNormalizer(range);
+    }
+    
+    @Override
+    protected void paintComponent(Graphics g) {
+        
+        normalizer.setMinNormalized(0);
+        normalizer.setMaxNormalized(getWidth() - 3);
+        
+        GraphicsUtils utils = GraphicsUtils.getInstance();
+        
+        Graphics2D graphics = utils.createAAGraphics(g);
+        
+        int y = getHeight()/3;
+        graphics.clearRect(0, 0, getWidth(), getHeight());
+        graphics.drawString(timeline.get(0).getName(), 2, y);
+        
+        y = getHeight()/2;
+        
+        int w = getWidth() - 4;
+        int h = 10;
+        
+        Shape shape = utils.getRoundShape(w, h);
+        
+        graphics.translate(2, y);
+
+        Paint paint = new GradientPaint(0, 0, Palette.DARK_GRAY.getColor(), 0, h, Palette.WHITE.getColor());
+        graphics.setPaint(paint);        
+        graphics.fill(shape);        
+
+        for (ThreadTimelineBean thread : timeline) {
+            normalizer.setValue(thread.getStartTime());
+            int x0 = (int) normalizer.getValueNormalized();
+
+            normalizer.setValue(thread.getStopTime());
+            int x1 = (int) normalizer.getValueNormalized();
+
+            graphics.setColor(getColor(thread));
+            graphics.fillRect(x0, 1, x1 - x0, h - 1);
+        }
+        
+        graphics.setColor(getColor(timeline.get(timeline.size() - 1)));
+        graphics.draw(shape);        
+        graphics.dispose();
+    }
+
+    private Color getColor(ThreadTimelineBean thread) {
+        Color result = null;
+        
+        switch (thread.getState()) {
+        case TIMED_WAITING:
+            result = Palette.PALE_RED.getColor();
+            break;
+            
+        case NEW:
+            result = Palette.POMP_AND_POWER_VIOLET.getColor();
+            break;
+
+        case RUNNABLE:
+            result = Palette.PRUSSIAN_BLUE.getColor();
+            break;
+
+        case TERMINATED:
+            result = Palette.GRAY.getColor();
+            break;
+
+        case BLOCKED:
+            result = Palette.RED.getColor();            
+            break;
+
+        case WAITING:
+            result = Palette.GRANITA_ORANGE.getColor();            
+            break;
+
+        default:
+            result = Color.BLACK;            
+            break;
+        }
+        return result;
+    }
+    
+    public static void main(String[] args) {
+        SwingUtilities.invokeLater(new Runnable() {
+            
+            @Override
+            public void run() {
+                JFrame frame = new JFrame();
+                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+                
+                ThreadTimelineBean bean1 = new ThreadTimelineBean();
+                bean1.setName("test");
+                bean1.setStartTime(0);
+                bean1.setStopTime(1000);
+                bean1.setState(Thread.State.BLOCKED);
+                
+                ThreadTimelineBean bean2 = new ThreadTimelineBean();
+                bean2.setName("test");
+                bean2.setStartTime(1000);
+                bean2.setStopTime(2000);
+                bean2.setState(Thread.State.RUNNABLE);
+                
+                ThreadTimelineBean bean3 = new ThreadTimelineBean();
+                bean3.setName("test");
+                bean3.setStartTime(2000);
+                bean3.setStopTime(2100);
+                bean3.setState(Thread.State.TIMED_WAITING);
+                
+                List<ThreadTimelineBean> timeline = new ArrayList<>();
+                timeline.add(bean1);
+                timeline.add(bean2);
+                timeline.add(bean3);
+
+                SwingThreadTimelineChart chart = new SwingThreadTimelineChart(timeline, 0, 2500);
+                
+                frame.add(chart);
+                frame.setSize(800, 150);
+                
+                frame.setVisible(true);
+            }
+        });
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadTimelineView.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2012 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.thread.client.swing.impl;
+
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.util.List;
+import java.util.Map;
+
+import javax.swing.DefaultListModel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ListCellRenderer;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingUtilities;
+
+import com.redhat.thermostat.client.ui.ComponentVisibleListener;
+import com.redhat.thermostat.client.ui.SwingComponent;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineBean;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineView;
+import com.redhat.thermostat.thread.model.ThreadInfoData;
+
+public class SwingThreadTimelineView extends ThreadTimelineView  implements SwingComponent  {
+
+    private JPanel timeLinePanel;
+    private DefaultListModel<SwingThreadTimelineChart> model;
+    
+    public SwingThreadTimelineView() {
+        timeLinePanel = new JPanel();
+        timeLinePanel.addHierarchyListener(new ComponentVisibleListener() {
+            @Override
+            public void componentShown(Component component) {
+                SwingThreadTimelineView.this.notify(Action.VISIBLE);
+            }
+            
+            @Override
+            public void componentHidden(Component component) {
+                SwingThreadTimelineView.this.notify(Action.HIDDEN);
+            }
+        });
+        
+        timeLinePanel.setLayout(new BorderLayout(0, 0));
+        model = new DefaultListModel<>();
+        JList<SwingThreadTimelineChart> chartList = new JList<>(model);
+        
+        chartList.setLayoutOrientation(JList.VERTICAL);
+        chartList.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
+        JScrollPane scrollPane = new JScrollPane(chartList);
+        scrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
+        timeLinePanel.add(scrollPane);
+        chartList.setCellRenderer(new ListCellRenderer<SwingThreadTimelineChart>() {
+            @Override
+            public Component getListCellRendererComponent(
+                    JList<? extends SwingThreadTimelineChart> list, SwingThreadTimelineChart value,
+                    int index, boolean isSelected, boolean cellHasFocus) {
+                return value;
+            }
+        });
+    }
+    
+    @Override
+    public void displayStats(final Map<ThreadInfoData, List<ThreadTimelineBean>> timelines, final long start, final long stop) {
+
+        SwingUtilities.invokeLater(new Runnable() {
+            
+            @Override
+            public void run() {
+                model.clear();
+                for (List<ThreadTimelineBean> timeline : timelines.values()) {
+                    SwingThreadTimelineChart panel = new SwingThreadTimelineChart(timeline, start, stop);
+                    panel.setPreferredSize(new Dimension(timeLinePanel.getWidth(), 50));
+                    model.addElement(panel);
+                }
+            }
+        });
+    }
+    
+    @Override
+    public Component getUiComponent() {
+        return timeLinePanel;
+    }
+}
--- a/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadView.java	Thu Sep 20 00:28:12 2012 +0200
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/SwingThreadView.java	Thu Sep 20 00:33:03 2012 +0200
@@ -56,6 +56,7 @@
 import com.redhat.thermostat.swing.ChartPanel;
 import com.redhat.thermostat.thread.client.common.ThreadTableBean;
 import com.redhat.thermostat.thread.client.common.ThreadTableView;
+import com.redhat.thermostat.thread.client.common.ThreadTimelineView;
 import com.redhat.thermostat.thread.client.common.ThreadView;
 import com.redhat.thermostat.thread.client.common.VMThreadCapabilitiesView;
 import com.redhat.thermostat.thread.client.common.chart.LivingDaemonThreadDifferenceChart;
@@ -70,8 +71,9 @@
     
     private SwingThreadTableView threadTableView;
     private SwingVMThreadCapabilitiesView vmCapsView;
+    private SwingThreadTimelineView threadTimelineView;
     private SwingThreadDetailsView threadDetailsView;
-    
+
     private JTabbedPane pane;
     
     private static final Translate t = LocaleResources.createLocalizer();
@@ -152,6 +154,9 @@
         pane.addTab(t.localize(LocaleResources.DETAILS), threadDetailsView.getUiComponent());
         threadDetailsPaneID = 2;
         
+        threadTimelineView = new SwingThreadTimelineView();
+        pane.addTab(t.localize(LocaleResources.TIMELINE), threadTimelineView.getUiComponent());
+        
         panel.getSplitPane().setBottomComponent(pane);
     }
     
@@ -261,4 +266,9 @@
             }
         });
     }
+    
+    @Override
+    public ThreadTimelineView createThreadTimelineView() {
+        return threadTimelineView;
+    }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/thread/client-swing/src/main/java/com/redhat/thermostat/thread/client/swing/impl/ThreadTimelineChart.java	Thu Sep 20 00:33:03 2012 +0200
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2012 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.thread.client.swing.impl;
+
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.SwingUtilities;
+import javax.swing.plaf.ColorUIResource;
+
+import com.redhat.thermostat.swing.GraphicsUtils;
+
+@SuppressWarnings("serial")
+public class ThreadTimelineChart extends JPanel {
+
+    private static final ColorUIResource TICK_COLOR = new ColorUIResource(0xa8aca8);
+
+    public ThreadTimelineChart() {
+    }
+    
+    @Override
+    protected void paintComponent(Graphics g) {
+        Graphics2D graphics = GraphicsUtils.getInstance().createAAGraphics(g);
+        
+        graphics.setColor(TICK_COLOR);
+        
+        int x = 1;
+        int y = 2;
+        int w = getWidth() - 2;
+        int h = getHeight();
+        
+        graphics.drawLine(x, y, w, y);
+        
+        graphics.dispose();
+    }
+    
+    public static void main(String[] args) {
+        SwingUtilities.invokeLater(new Runnable() {
+            
+            @Override
+            public void run() {
+                JFrame frame = new JFrame();
+                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+                
+                ThreadTimelineChart chart = new ThreadTimelineChart();
+                
+                frame.add(chart);
+                frame.setSize(500, 500);
+                frame.setVisible(true);
+            }
+        });
+    }
+}