Archive

Posts Tagged ‘IWorkspaceRoot’

Writing an Eclipse Plug-in (Part 19): A Quick Display Fix

February 28, 2010 2 comments

[I am deep into solving a CNF issue, but since I haven’t solved it yet you will have to settle for a bug fix.]

Random bug: when the cursor hovers over the custom navigator title bar a tooltip opens letting us know that the navigator can’t find a label for the root node. The full error message is Error: no label provider for R/. Tells you everything you need to know. Except what the problem is. Or how to fix it. Or, for Eclipse novices, what R/ means.

Luckily this is something that we are not afflicted with here at Hidden Clause. The message did tell us everything we needed to know. The Custom Navigator label provider is ignoring the root node used by the navigator (the R/ referred to in the error) and returning an empty string. The code for LabelProvider.getText() is:

   public String getText(Object element) {
        String text = ""; //$NON-NLS-1$
        if (ICustomProjectElement.class.isInstance(element)) {
            text = ((ICustomProjectElement)element).getText();
        }
        // else ignore the element

        return text;
    }

(Notice how it so brilliantly ignores everything except elements of type ICustomProjectElement.)

What the message also tells us, by not telling us, is that our zero-length string appears to be causing consternation in the navigator. It is causing so much consternation that the navigator thinks no label provider is available to supply it with a default label for the root node.

That something is easily fixed in the LabelProvider. I’m not sure why Eclipse does not default to no string for the root (damn, those double negatives!), but it does not so we have to assign something to it. Since the standard behavior for other navigator views is to use the name of the view, in this case Custom Plug-in Navigator, that is what we will do.

  1. Open LabelProvider.java
  2. Change getText() to include an else if:
       public String getText(Object element) {
            String text = ""; //$NON-NLS-1$
            if (ICustomProjectElement.class.isInstance(element)) {
                text = ((ICustomProjectElement)element).getText();
            } else if (IWorkspaceRoot.class.isInstance(element)) {
                text = "Custom Plug-in Navigator";
            }
            // else ignore the element
    
            return text;
        }
    
  3. Start the runtime workbench and behold the beauty of our new string.

For Those of You Who Care

I discovered the solution to the above by putting a breakpoint in LabelProvider.getText() and walking the call tree. NavigatorContentServiceLabelProvider.findStyledText() quite explicitly changed the original empty string returned by LabelProvider.getText() into a null which caused NavigatorContentServiceLabelProvider.getStyledText() to assign the error message to the navigator view. It makes sense, it just wasn’t what I wanted.

Extra credit: Run the Externalize String Wizard on LabelProvider.java and add the new string to the message.properties file.

What Just Happened?

Fixed a bug. Contain your excitement.

The cat was not impressed and refused to come out.

Code

messages.properties

CustomProjectParent_Project_Folder=icons/project-folder.png
CustomProjectSchema_Project_Schema=icons/project-schema.png
CustomProjectSchemaFilters_Project_Schema_Filters=icons/project-schema-filters.png
CustomProjectSchemaTables_Project_Schema_Tables=icons/project-schema-tables.png
CustomProjectSchemaViews_Project_Schema_Views=icons/project-schema-views.png
CustomProjectStoredProcedures_Project_Stored_Procedures=icons/project-stored-procedures.png
LabelProvider_Custom_Plugin_Navigator=Custom Plug-in Navigator

LabelProvider.java

/**
 * Coder beware: this code is not warranted to do anything.
 *
 * Copyright Oct 17, 2009 Carlos Valcarcel
 */
package customnavigator.navigator;

import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.ILabelProviderListener;
import org.eclipse.swt.graphics.Image;

/**
 * @author carlos
 *
 */
public class LabelProvider implements ILabelProvider {

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.ILabelProvider#getImage(java.lang.Object)
     */
    @Override
    public Image getImage(Object element) {
        Image image = null;
        
        if (ICustomProjectElement.class.isInstance(element)) {
            image = ((ICustomProjectElement)element).getImage();
        }
        // else ignore the element
        
        return image;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.ILabelProvider#getText(java.lang.Object)
     */
    @Override
    public String getText(Object element) {
        String text = ""; //$NON-NLS-1$
        if (ICustomProjectElement.class.isInstance(element)) {
            text = ((ICustomProjectElement)element).getText();
        } else if (IWorkspaceRoot.class.isInstance(element)) {
            text = Messages.LabelProvider_Custom_Plugin_Navigator;
        }
        // else ignore the element
        
        return text;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.IBaseLabelProvider#addListener(org.eclipse.jface.viewers.ILabelProviderListener)
     */
    @Override
    public void addListener(ILabelProviderListener listener) {
        // TODO Auto-generated method stub

    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.IBaseLabelProvider#dispose()
     */
    @Override
    public void dispose() {
        // TODO Auto-generated method stub

    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.IBaseLabelProvider#isLabelProperty(java.lang.Object, java.lang.String)
     */
    @Override
    public boolean isLabelProperty(Object element, String property) {
        // TODO Auto-generated method stub
        return false;
    }

    /* (non-Javadoc)
     * @see org.eclipse.jface.viewers.IBaseLabelProvider#removeListener(org.eclipse.jface.viewers.ILabelProviderListener)
     */
    @Override
    public void removeListener(ILabelProviderListener listener) {
        // TODO Auto-generated method stub

    }

}

Writing an Eclipse Plug-in (Part 13): Common Navigator: Adding Tests

December 8, 2009 Leave a comment

And now it is time for the mundane.

While I firmly believe in test-driven development I do not believe in test-driven learning; that means that while tests are great to insure that your software works as advertised (or at least as much of it as you could think of), testing is not a good way to learn implementation. I know the physicists out there will disagree with me, but learning the black box behavior of a system is quite different than learning how to build the actual clockwork mechanism that makes something go.

With that said, at some point we do need to refactor the code and we can’t safely refactor the code without some tests to prove that our refactoring hasn’t broken anything.

We have been coding without a net in the interest of keeping the learning as noise-free as possible. Now we return to the part of the coding that we would normally do as we developed the code.

In other words, time for code hygiene.

What to do

  1. Create a plug-in test project for the navigator
    1. Enter the following:
      • Project name: customnavigator.test
      • Eclipse version: 3.5
    2. Click Next
    3. Enter the following:
      • Version: 1.0.1.3 [Actually anything you want]
      • Name: Custom Navigator Test
    4. Click Finish
  2. Clean up MANIFEST.MF
    1. Click the MANIFEST.MF tab
    2. Move the cursor to line 1 and Press Ctrl+1
    3. Select Add Missing Packages
    4. Move the cursor to line 3 and Press Ctrl+1
    5. Select Externalize the Bundle-Name header
    6. Save the file
  3. Dependencies tab: Add org.junit4
  4. Dependencies tab: Add org.eclipse.core.resources
  5. Copy easymock.jar to your project. Add a lib folder under your test project folder, copy the easymock.jar file and add it to Runtime –> Classpath
  6. Open the customnavigator.test Properties dialog. In the Project References element put a check mark next to the customnavigator project. Click Finish.
  7. Implement customnavigator.navigator.ContentProviderTest in the customnavigator.test project
    1. Create a new JUnit class named customnavigator.navigator.ContentProviderTest
    2. Test getParent()
    3. Test getChildren()
    4. Test hasChildren()

One of the tests, getChildren(), pointed out a bug: when a project came in, custom or not, it was being wrapped and saved in the _wrapperCache. The only projects that should be in the wrapper cache are projects of type CustomProjectParents. While not fatal, it was still wrong. Not a bad catch.

Here is the corrected code.

ContentProvider.java

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;
        
        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = _wrapperCache.get(projects[i].getName()); 
            if (customProjectParent == null) {
                customProjectParent = createCustomProjectParent(projects[i]);
                if (customProjectParent != null) {
                    _wrapperCache.put(projects[i].getName(), customProjectParent);
                }
            }

            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }
        
        result = new Object[list.size()];
        list.toArray(result);
        
        return result;
    }

That brings the number of test projects up to 2.

Why did we do that?

Just as a review about TDD from my rather narrow/myopic perspective (and not necessarily in this order):

  • Don’t test the platform
  • Don’t test trivial logic (i.e. trivial getters and setters)
  • Test boundary conditions that will cause errors
  • Test success conditions

So, what kinds of tests do we need? Well, the easiest way is to pretend we know how to implement the behavior, but haven’t actually written it yet. That should give us a clarity of purpose known only to those who already know the answer.

What does Eclipse expect the content provider to provide? Well, content. In our case, the content is the custom project in its variations; no other project/content types need apply.

As ContentProvider is just another POJO we can test it in a pretty standalone way. Also, even though our content provider implements an interface that extends an interface that extends an interface, we really only care about the methods we overrode. Of course, when I made the following list to see which methods I care about it turns out I had to override them all:

public class ContentProvider implements ITreeContentProvider, IResourceChangeListener {
    // From ITreeContentProvider
    @Override
    public Object[] getChildren(Object parentElement) {
      ...
    }

    @Override
    public Object getParent(Object element) {
      ...
    }

    @Override
    public boolean hasChildren(Object element) {
      ...
    }

    // From IStructuredContentProvider
    @Override
    public Object[] getElements(Object inputElement) {
      ...
    }

    // From IContentProvider
    @Override
    public void dispose() {
      ...
    }

    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
      ...
    }

    // From IResourceChangeListener
    @Override
    public void resourceChanged(IResourceChangeEvent event) {
      ...
    }

}

The EasyMock framework will also make these tests a simpler to implement. I am not going to try to convince you one way or another to use EasyMock or any other mock object framework. Every time I use EasyMock my life is easier. If there is a simpler mock object framework let me know, otherwise pick one and get to work.

For example, when I thought about the tests for ContentProvider I wasn’t sure which I should write first so I took the path of least resistence:

  • getParent()
    • Input: IWorkspaceRoot, Output: null
    • Input: IProject, Output: non-null
    • Input: ICustomProjectElement, Output: non-null (could be an IWorkspaceRoot, or one of the CustomProject wrappers)
    • Input: anything else (including null), Output: zero length array
  • getChildren()
    • Input: IWorkspaceRoot, Output: null if no projects exist or if the projects are not of of the Custom Project nature.
    • Input: IWorkspaceRoot, Output: non-null if a Custom Project exists
    • Input: IWorkspaceRoot w/ 3 projects (1 non-custom, 1 custom, 1 non-custom), Output: an array with one custom project
    • Input: IWorkspaceRoot w/ 3 projects (1 custom, 1 non-custom, 1 custom), Output: an array with two custom projects
    • Input: IProject, Output: null (by defintion, if it were a CustomProject it would be wrapped already)
    • Input: ICustomProjectElement, Output: non-null unless if is a leaf child like CustomProjectSchemaFilters
    • Input: anything else (including null), Output: zero length array
  • hasChildren()
    • Input: IWorkspaceRoot, Output: false if the projects no proejcts exist or are not Custom Projects otherwise true
    • Input: ICustomProjectElement, Output: false if it is a leaf child like CustomProjectSchemaFilters, true otherwise
    • Input: anything else (including null), Output: false

Seems like a lot to think about doesn’t it? That is the whole idea. [Programming is no more about typing than writing is; in fact, programming is just as much about thinking as writing is.] Under what conditions can something fail? When it “succeeds” did it succeed properly? Some of the above I normally consider as I write the tests and others happen as I learn about the behavior as I implement. White boards are my friend.

Also, tests, like the ones for ICustomProjectElement, normally help you discover that you need data types like ICustomProjectElement. In this case, we skipped a few steps.

It’s okay; I forgive us.

Finally, I am not testing:

  • getElement(): since this calls getChildren() there is no reason to test this.
  • dispose(): I have no idea how I would do that. Sadly, I do have to make sure that I release any resources for which I am responsible, but I am not sure how I would do that except to simply remember that I need to do that in dispose() (can you say time bomb?). Also, it is trivial enough so I can safely ignore it for now.
  • inputChanged(): having implemented it I can safely say that testing an assignment at this point is…pointless.
  • resourceChanged(): This is purely GUI behavior. I suppose I could test it if the logic were complex, but for now it is not.

Being less than a TDD purist is hard to admit, but what the heck, I am not as much of a TDD purist as I would like folks to believe. Sometimes, I can’t come up with that perfect scenario that will light the way for me to create a host of absolutely incredible tests that will leave my code both bug-free and completely covered.

In any case, I am not going to go over every test or how I agonized over them or how much I drank to get through them. Red Bull is overrated.

In addition, clean up customnavigator.test.Activator:
– comment the empty constructor
– add @Override to start()
– add @Override to stop()

More True Confessions

And this is where we write all kinds of test code for the CustomNavigator; only CustomProjects should appear and their various nodes should stay open if they were open when we changed something or should stay closed when we changed something.

We could test things like:

  • a generic project – assert an empty custom navigator in a fresh workspace
  • a custom project – assert one project in the custom navigator
  • a generic project and a custom project – assert one project in the custom navigator

There is only one problem (or perhaps we should consider it an opportunity): that is testing the platform. Making sure that ContentProvider is called with an IWorkspaceRoot was a plugin.xml configuration, not code, so what are we testing anyway? Actually, we would be testing the ContentProvider! Again!

I know we had fun doing it the first time, but I’ll pass on doing it more than once.

I am also not going to write any tests for CustomProjectParent or any of the children that come from it. Why? They are simple. No point wasting time on them until the logic contained by them is complex enough to warrant it.

Kinda makes you wish we had refactored them earlier. No worries; we do that in the next post.

What Just Happened?

Some of you may look at the tests and wonder how does using EasyMock make the job any easier? It is not about EasyMock; it is about testing the expected behavior from the code regardless of what the actual input is.

For example, in testGetChildrenForICustomProjectElementWithNoChildren() and testGetChildrenForICustomProjectElementWithChildren() I tested for an ICustomProjectElement with children and with no children, but I did it without using the CustomProjectParent type or any of its children. The reason for that is both simple and important: I am not testing CustomProjectParent or its children; I am testing ContentProvider. By mixing the testing of ContentProvider and CustomProjectParent (or any of the children) I run the risk of testing something I don’t need to test, or worse, forgetting to test something I should have tested.

Next time: Now that the tests are mostly out of the way it is time to refactor the children.

The cat is tired.

Code

ContentProviderTest.java

/**
 * Coder beware: this code is not warranted to do anything.
 *
 * Copyright Dec 6, 2009 Carlos Valcarcel
 */
package customnavigator.navigator;

import org.easymock.EasyMock;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectNature;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.runtime.CoreException;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;


/**
 * @author carlos
 *
 */
public class ContentProviderTest {
    private static final String CUSTOMPLUGIN_PROJECT_NATURE = "customplugin.projectNature"; //$NON-NLS-1$
    
    private ContentProvider _contentProvider;

    @Test
    public void testGetParentForIWorkspaceRoot() {
        Object actual = null;
        
        IWorkspaceRoot workspaceRoot = EasyMock.createStrictMock(IWorkspaceRoot.class);
        actual = _contentProvider.getParent(workspaceRoot);
        
        Assert.assertNull(actual);
    }
    
    @Test
    public void testGetParentForNull() {
        Object actual = null;
        
        actual = _contentProvider.getParent(null);
        
        Assert.assertNull(actual);
    }
    
    @Test
    public void testGetParentForObject() {
        Object actual = null;
        
        actual = _contentProvider.getParent(new Object());
        
        Assert.assertNull(actual);
    }
    
    @Test
    public void testGetParentForIProject() {
        IWorkspaceRoot workspaceRoot = EasyMock.createStrictMock(IWorkspaceRoot.class);
        IWorkspace workspace = EasyMock.createStrictMock(IWorkspace.class);
        IProject project = EasyMock.createStrictMock(IProject.class);
        project.getWorkspace();
        EasyMock.expectLastCall().andReturn(workspace);
        workspace.getRoot();
        EasyMock.expectLastCall().andReturn(workspaceRoot);
        
        EasyMock.replay(workspaceRoot, workspace, project);
        
        Object actual = _contentProvider.getParent(project);
        Assert.assertNotNull(actual);
        
        EasyMock.verify(workspaceRoot, workspace, project);
    }
    
    @Test
    public void testGetParentForICustomProjectElement() {
        Object parent = EasyMock.createNiceControl();
        ICustomProjectElement customProjectElement = EasyMock.createStrictMock(ICustomProjectElement.class);
        customProjectElement.getParent();
        EasyMock.expectLastCall().andReturn(parent);
        
        EasyMock.replay(customProjectElement);
        
        Object actual = _contentProvider.getParent(customProjectElement);
        Assert.assertNotNull(actual);
        
        EasyMock.verify(customProjectElement);
    }

    @Test
    public void testGetChildrenForIWorkspaceRootWithNoProjects() {
        IProject [] projects = {};
        IWorkspaceRoot workspaceRoot = EasyMock.createStrictMock(IWorkspaceRoot.class);
        workspaceRoot.getProjects();
        EasyMock.expectLastCall().andReturn(projects);
        
        EasyMock.replay(workspaceRoot);
        
        Object [] actual = _contentProvider.getChildren(workspaceRoot);
        Assert.assertNotNull(actual);
        Assert.assertTrue(actual.length == 0);
        
        EasyMock.verify(workspaceRoot);
    }
    
    @Test
    public void testGetChildrenForIWorkspaceRootWithNoCustomProjects() throws CoreException {
        IProject [] projects = new IProject[1];
        IProject project = EasyMock.createStrictMock(IProject.class);
        
        projects[0] = project;
        
        IWorkspaceRoot workspaceRoot = EasyMock.createStrictMock(IWorkspaceRoot.class);
        workspaceRoot.getProjects();
        EasyMock.expectLastCall().andReturn(projects);
        
        project.getName();
        EasyMock.expectLastCall().andReturn("non-custom project"); //$NON-NLS-1$
        
        project.getNature(CUSTOMPLUGIN_PROJECT_NATURE);
        EasyMock.expectLastCall().andReturn(null);
        
        EasyMock.replay(workspaceRoot, project);
        
        Object [] actual = _contentProvider.getChildren(workspaceRoot);
        Assert.assertNotNull(actual);
        Assert.assertTrue(actual.length == 0);
        
        EasyMock.verify(workspaceRoot, project);
    }
    
    @Test
    public void testGetChildrenForIWorkspaceRootWithOneCustomProject() throws CoreException {
        IProject [] projects = new IProject[1];
        IProject project = EasyMock.createStrictMock(IProject.class);
        
        projects[0] = project;
        
        IWorkspaceRoot workspaceRoot = EasyMock.createStrictMock(IWorkspaceRoot.class);
        workspaceRoot.getProjects();
        EasyMock.expectLastCall().andReturn(projects);
        
        String projectName = "custom project"; //$NON-NLS-1$
        project.getName();
        EasyMock.expectLastCall().andReturn(projectName);
        
        project.getNature(CUSTOMPLUGIN_PROJECT_NATURE);
        EasyMock.expectLastCall().andReturn(EasyMock.createMock(IProjectNature.class));

        project.getName();
        EasyMock.expectLastCall().andReturn(projectName);
        
        EasyMock.replay(workspaceRoot, project);
        
        Object [] actual = _contentProvider.getChildren(workspaceRoot);
        Assert.assertNotNull(actual);
        Assert.assertTrue(actual.length == 1);
        Assert.assertEquals(project, ((CustomProjectParent)actual[0]).getProject());
        
        EasyMock.verify(workspaceRoot, project);
    }
    
    @Test
    public void testGetChildrenForIWorkspaceRootWithOneCustomProjectTwoNonCustomProjects() throws CoreException {
        IProject nonCustomProject1 = EasyMock.createStrictMock(IProject.class);
        IProject nonCustomProject2 = EasyMock.createStrictMock(IProject.class);
        IProject customProject = EasyMock.createStrictMock(IProject.class);
        
        IProject[] projects = {
                nonCustomProject1,
                customProject,
                nonCustomProject2
        };
        
        IWorkspaceRoot workspaceRoot = EasyMock.createStrictMock(IWorkspaceRoot.class);
        workspaceRoot.getProjects();
        EasyMock.expectLastCall().andReturn(projects);
        
        String bogusProjectName = "bogus project"; //$NON-NLS-1$
        String customProjectName = "custom project"; //$NON-NLS-1$
        nonCustomProject1.getName();
        EasyMock.expectLastCall().andReturn(bogusProjectName);
        
        nonCustomProject1.getNature(CUSTOMPLUGIN_PROJECT_NATURE);
        EasyMock.expectLastCall().andReturn(null);

        customProject.getName();
        EasyMock.expectLastCall().andReturn(customProjectName);
        
        customProject.getNature(CUSTOMPLUGIN_PROJECT_NATURE);
        EasyMock.expectLastCall().andReturn(EasyMock.createMock(IProjectNature.class));

        customProject.getName();
        EasyMock.expectLastCall().andReturn(customProjectName);
        
        nonCustomProject2.getName();
        EasyMock.expectLastCall().andReturn(bogusProjectName);
        
        nonCustomProject2.getNature(CUSTOMPLUGIN_PROJECT_NATURE);
        EasyMock.expectLastCall().andReturn(null);

        EasyMock.replay(workspaceRoot, nonCustomProject1, customProject, nonCustomProject2);
        
        Object [] actual = _contentProvider.getChildren(workspaceRoot);
        Assert.assertNotNull(actual);
        Assert.assertTrue(actual.length == 1);
        Assert.assertEquals(customProject, ((CustomProjectParent)actual[0]).getProject());
        
        EasyMock.verify(workspaceRoot, nonCustomProject1, nonCustomProject2, customProject);
    }

    @Test
    public void testGetChildrenForIWorkspaceRootWithOneNonCustomProjectTwoCustomProjects() throws CoreException {
        IProject customProject1 = EasyMock.createStrictMock(IProject.class);
        IProject customProject2 = EasyMock.createStrictMock(IProject.class);
        IProject nonCustomProject = EasyMock.createStrictMock(IProject.class);
        
        IProject[] projects = {
                customProject1,
                nonCustomProject,
                customProject2
        };
        
        IWorkspaceRoot workspaceRoot = EasyMock.createStrictMock(IWorkspaceRoot.class);
        workspaceRoot.getProjects();
        EasyMock.expectLastCall().andReturn(projects);
        
        String bogusProjectName = "bogus project"; //$NON-NLS-1$
        String customProjectName1 = "custom project 1"; //$NON-NLS-1$
        String customProjectName2 = "custom project 2"; //$NON-NLS-1$
        customProject1.getName();
        EasyMock.expectLastCall().andReturn(customProjectName1);
        
        customProject1.getNature(CUSTOMPLUGIN_PROJECT_NATURE);
        EasyMock.expectLastCall().andReturn(EasyMock.createMock(IProjectNature.class));

        customProject1.getName();
        EasyMock.expectLastCall().andReturn(customProjectName1);
        
        nonCustomProject.getName();
        EasyMock.expectLastCall().andReturn(bogusProjectName);
        
        nonCustomProject.getNature(CUSTOMPLUGIN_PROJECT_NATURE);
        EasyMock.expectLastCall().andReturn(null);

        customProject2.getName();
        EasyMock.expectLastCall().andReturn(customProjectName2);
        
        customProject2.getNature(CUSTOMPLUGIN_PROJECT_NATURE);
        EasyMock.expectLastCall().andReturn(EasyMock.createMock(IProjectNature.class));

        customProject2.getName();
        EasyMock.expectLastCall().andReturn(customProjectName2);
        
        EasyMock.replay(workspaceRoot, customProject1, nonCustomProject, customProject2);
        
        Object [] actual = _contentProvider.getChildren(workspaceRoot);
        Assert.assertNotNull(actual);
        Assert.assertTrue(actual.length == 2);
        Assert.assertEquals(customProject1, ((CustomProjectParent)actual[0]).getProject());
        Assert.assertEquals(customProject2, ((CustomProjectParent)actual[1]).getProject());
        
        EasyMock.verify(workspaceRoot, customProject1, nonCustomProject, customProject2);
    }

    /**
     * If a resource of type IProject comes in ignore it. If it were
     * a Custom Project it would be wrapped already.
     */
    @Test
    public void testGetChildrenForIProjectNotCustomProject() {
        IProject project = EasyMock.createStrictMock(IProject.class);
        
        Object [] actual = _contentProvider.getChildren(project);
        Assert.assertNotNull(actual);
        Assert.assertTrue(actual.length == 0);
    }
    
    @Test
    public void testGetChildrenForICustomProjectElementWithNoChildren() {
        assertChildrenFromICustomProjectElement(0);
    }

    /**
     * Check that an ICustomProjectElement returns some kids. Send back 5 to prove
     * the right method is called. 
     */
    @Test
    public void testGetChildrenForICustomProjectElementWithChildren() {
        assertChildrenFromICustomProjectElement(5);
    }
    
    @Before
    public void setUp() {
        _contentProvider = new ContentProvider();
    }

    private void assertChildrenFromICustomProjectElement(int childCount) {
        Object [] children = new Object[childCount];
        ICustomProjectElement customProjectElement = EasyMock.createStrictMock(ICustomProjectElement.class);
        
        customProjectElement.getChildren();
        EasyMock.expectLastCall().andReturn(children);

        EasyMock.replay(customProjectElement);
        
        Object [] actual = _contentProvider.getChildren(customProjectElement);
        Assert.assertNotNull(actual);
        Assert.assertTrue(actual.length == childCount);

        EasyMock.verify(customProjectElement);
    }

}

ContentProvider.java

/**
 * Coder beware: this code is not warranted to do anything.
 * Copyright Oct 17, 2009 Carlos Valcarcel
 */
package customnavigator.navigator;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.TreePath;
import org.eclipse.jface.viewers.TreeViewer;
import org.eclipse.jface.viewers.Viewer;

import customplugin.natures.ProjectNature;

/**
 * @author carlos
 */
public class ContentProvider implements ITreeContentProvider, IResourceChangeListener {

    private static final Object[]   NO_CHILDREN = {};
    private Map<String, Object> _wrapperCache = new HashMap<String, Object>();
    private Viewer _viewer;

    public ContentProvider() {
        ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
     */
    @Override
    public Object[] getChildren(Object parentElement) {
        Object[] children = null;
        if (IWorkspaceRoot.class.isInstance(parentElement)) {
            IProject[] projects = ((IWorkspaceRoot)parentElement).getProjects(); 
            children = createCustomProjectParents(projects);
        } else if (ICustomProjectElement.class.isInstance(parentElement)) {
            children = ((ICustomProjectElement) parentElement).getChildren();
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
     */
    @Override
    public Object getParent(Object element) {
        Object parent = null;
            
        if (IProject.class.isInstance(element)) {
            parent = ((IProject)element).getWorkspace().getRoot();
        } else if (ICustomProjectElement.class.isInstance(element)) {
            parent = ((ICustomProjectElement)element).getParent();
        } // else parent = null if IWorkspaceRoot or anything else
        
        return parent;
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
     */
    @Override
    public boolean hasChildren(Object element) {
        boolean hasChildren = false;

        if (IWorkspaceRoot.class.isInstance(element)) {
            hasChildren = ((IWorkspaceRoot)element).getProjects().length > 0;
        } else if (ICustomProjectElement.class.isInstance(element)) {
            hasChildren = ((ICustomProjectElement)element).hasChildren();
        }
        // else it is not one of these so return false
        
        return hasChildren;
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
     */
    @Override
    public Object[] getElements(Object inputElement) {
        // This is the same as getChildren() so we will call that instead
        return getChildren(inputElement);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.IContentProvider#dispose()
     */
    @Override
    public void dispose() {
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
     */
    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        _viewer = viewer;
    }

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        TreeViewer viewer = (TreeViewer) _viewer;
        TreePath[] treePaths = viewer.getExpandedTreePaths();
        viewer.refresh();
        viewer.setExpandedTreePaths(treePaths); 
    }

    private Object createCustomProjectParent(IProject parentElement) {

        Object result = null;
        try {
            if (parentElement.getNature(ProjectNature.NATURE_ID) != null) {
                result = new CustomProjectParent(parentElement);
            }
        } catch (CoreException e) {
            // Go to the next IProject
        }

        return result;
    }

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;
        
        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = _wrapperCache.get(projects[i].getName()); 
            if (customProjectParent == null) {
                customProjectParent = createCustomProjectParent(projects[i]);
                if (customProjectParent != null) {
                    _wrapperCache.put(projects[i].getName(), customProjectParent);
                }
            }

            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }
        
        result = new Object[list.size()];
        list.toArray(result);
        
        return result;
    }

}

Writing an Eclipse Plug-in (Part 11): Common Navigator: Displaying Custom Resources or Refresh Or Die or The Magic of navigatorContent

November 14, 2009 8 comments

Today’s tasks are:

  • Open the navigator in a perspective (doesn’t matter which one)
  • Create a Custom Project
  • Create a non-custom project. It should not appear in the custom navigator.

In breaking with tradition I am going to simply tell you what needs to be done (What to Do) and then I will rationalize the implementation for the work of art that it is (Why did we do it?).

What to Do

Let’s configure customnavigator’s plugin.xml:

  1. customnavigator –> plugin.xml –> org.eclipse.ui.views –> Custom Plug-in Navigator
  2. Change customnavigator.navigator.CustomNavigator to org.eclipse.ui.navigator.CommonNavigator
  3. Delete CustomNavigator.java (YAGNI for the foreseeable future)
  4. Change org.eclipse.ui.navigator.navigatorContent –> Custom Navigator Content –> (triggerPoints) –> (or) –> customnavigator.navigator.CustomProjectWorkbenchRoot to org.eclipse.ui.navigator.navigatorContent –> Custom Navigator Content –> (triggerPoints) –> org.eclipse.core.resources.IWorkspaceRoot

Change the ContentProvider code to reflect the following changes:

ContentProvider.getParent()

    @Override
    public Object getParent(Object element) {
        Object parent = null;

        if (IProject.class.isInstance(element)) {
            parent = ((IProject)element).getWorkspace().getRoot();
        } else if (ICustomProjectElement.class.isInstance(element)) {
            parent = ((ICustomProjectElement)element).getParent();
        } // else parent = null if IWorkspaceRoot or anything else

        return parent;
    }

ContentProvider.hasChildren()

    @Override
    public boolean hasChildren(Object element) {
        boolean hasChildren = false;

        if (IWorkspaceRoot.class.isInstance(element)) {
            hasChildren = ((IWorkspaceRoot)element).getProjects().length > 0;
        } else if (ICustomProjectElement.class.isInstance(element)) {
            hasChildren = ((ICustomProjectElement)element).hasChildren();
        }
        // else it is not one of these so return false

        return hasChildren;
    }

ContentProvider.getChildren()/createCustomProjectParents()/createCustomProjectParent()

    public Object[] getChildren(Object parentElement) {
        Object[] children = null;
        if (IWorkspaceRoot.class.isInstance(parentElement)) {
            IProject[] projects = ((IWorkspaceRoot)parentElement).getProjects();
            children = createCustomProjectParents(projects);
        } else if (ICustomProjectElement.class.isInstance(parentElement)) {
            children = ((ICustomProjectElement) parentElement).getChildren();
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

    private Object createCustomProjectParent(IProject parentElement) {

        Object result = null;
        try {
            if (parentElement.getNature(ProjectNature.NATURE_ID) != null) {
                result = new CustomProjectParent(parentElement);
            }
        } catch (CoreException e) {
            // Go to the next IProject
        }

        return result;
    }

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;

        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = createCustomProjectParent(projects[i]);
            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }

        result = new Object[list.size()];
        list.toArray(result);

        return result;
    }

ContentProvider.inputChanged()/ContentProvider constructor/dispose()

    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        _viewer = viewer;
    }

    public ContentProvider() {
        ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
    }

    @Override
    public void dispose() {
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    }

ContentProvider.resourceChanged()

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        _viewer.refresh();
    }

Why did we do it?

So, as we saw in a previous post, we can display a custom project that has specialized child nodes based on the kind of project we create; in this case a project with a nature of customplugin.projectNature.

Today’s task (a repeat of what I listed above) was:

  • Open the navigator in a perspective (doesn’t matter which one)
  • Create a Custom Project
  • Create a non-custom project. It should not appear in the custom navigator.

The challenge is: how to configure the Custom Navigator so that it will only display custom projects. In my never ending quest to write as little code as possible I will show you what to configure in plugin.xml and what code to add to ContentProvider to make it happen.

Supremely obvious point #1: without content a common navigator doesn’t display anything.
Supremely obvious point #2: you can configure a resource-based CNF navigator to do one of three things (if you can think of more, let me know or better yet write it up):

  1. Display only certain resources,
  2. Display resources in a custom way or
  3. Some combination of 1 and 2.

We want to do #3: the navigator should only display custom projects and resources related to custom projects should be displayed in a custom way.

If we had wanted to display all of the top level resources in the navigator we would just define a viewerContentBinding of org.eclipse.ui.navigator.resourceContent. Since that content binding is already defined our work would be done. That however would be too easy and wrong for what we want to do (it is amazing how often easy and wrong go together).

We know that we can display custom projects and related resources in a custom way (we did that, remember?). Now we want to make sure that no matter how many projects we create the navigator will only display our projects our way (like fast food without the fat).

In order to do this we are going to take a few steps back and a few steps forward. We are going to:

  • Delete our existing navigator class (back one step)
  • Replace the existing navigator class with a CommonNavigator (back another step)
  • Attach a content binding to the viewer (which is actually already there, but we are changing it; back another step)
  • Configure a navigatorContent (which is also already there)
    • Define an object type to send into the content provider (called the triggerPoint): IWorkspaceRoot
    • Implement the content provider code to wrap our project and its children
    • Register a resource change listener in the content provider to update the viewer when new projects are added

First the ending:
Let’s configure customnavigator’s plugin.xml:

  1. Go to customnavigator –> plugin.xml –> org.eclipse.ui.views –> Custom Plug-in Navigator
  2. Change customnavigator.navigator.CustomNavigator to org.eclipse.ui.navigator.CommonNavigator
  3. Delete CustomNavigator.java (YAGNI)
  4. Change org.eclipse.ui.navigator.navigatorContent –> Custom Navigator Content –> (triggerPoints) –> (or) –> customnavigator.navigator.CustomProjectWorkbenchRoot to org.eclipse.ui.navigator.navigatorContent –> Custom Navigator Content –> (triggerPoints) –> org.eclipse.core.resources.IWorkspaceRoot

Why change the triggerPoint from CustomProjectWorkbenchRoot to IWorkspaceRoot? Believe it or not, the CustomProjectWorkbenchRoot is overkill for what we want. In fact, using CustomProjectWorkbenchRoot will keep the navigator from working properly so in the tradition of YAGNI, we got rid of it. We may live to regret it depending on how complex this project gets, but for now out with the trash.

And now for the ContentProvider code:
ContentProvider.getParent()

    @Override
    public Object getParent(Object element) {
        Object parent = null;

        if (IProject.class.isInstance(element)) {
            parent = ((IProject)element).getWorkspace().getRoot();
        } else if (ICustomProjectElement.class.isInstance(element)) {
            parent = ((ICustomProjectElement)element).getParent();
        } // else parent = null if IWorkspaceRoot or anything else

        return parent;
    }

What is this code doing?

  • If an IWorkspaceRoot comes in return null as it has no parent.
  • If an IProject comes in, return the Workspace root.
  • If a node of type ICustomProjectElement, and all of the custom project wrappers are this type, ask the object for its parent. The project will return the Workspace root, the child nodes will return the project or its custom parent node.
  • For all others return null (no parent)

Since the “all others” and IWorkspaceRoot return null we just need to check for the IProject and ICustomProjectElement.

ContentProvider.hasChildren()

    @Override
    public boolean hasChildren(Object element) {
        boolean hasChildren = false;

        if (IWorkspaceRoot.class.isInstance(element)) {
            hasChildren = ((IWorkspaceRoot)element).getProjects().length > 0;
        } else if (ICustomProjectElement.class.isInstance(element)) {
            hasChildren = ((ICustomProjectElement)element).hasChildren();
        }
        // else it is not one of these so return false

        return hasChildren;
    }

So who has children? The only two types I care about are IWorkspaceRoot and ICustomProjectElement, otherwise return null.

  • If an IWorkspaceRoot return true if there are any projects. I know, I know, I should check if any of the projects have the custom project nature, but that is overhead I will safely ignore for now.
  • If an ICustomProjectElement comes in ask if it has any children.

Stay on target.

ContentProvider.getChildren()/createCustomProjectParents()/createCustomProjectParent()

    public Object[] getChildren(Object parentElement) {
        Object[] children = null;
        if (IWorkspaceRoot.class.isInstance(parentElement)) {
            IProject[] projects = ((IWorkspaceRoot)parentElement).getProjects();
            children = createCustomProjectParents(projects);
        } else if (ICustomProjectElement.class.isInstance(parentElement)) {
            children = ((ICustomProjectElement) parentElement).getChildren();
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

    private Object createCustomProjectParent(IProject parentElement) {

        Object result = null;
        try {
            if (parentElement.getNature(ProjectNature.NATURE_ID) != null) {
                result = new CustomProjectParent(parentElement);
            }
        } catch (CoreException e) {
            // Go to the next IProject
        }

        return result;
    }

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;

        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = createCustomProjectParent(projects[i]);
            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }

        result = new Object[list.size()];
        list.toArray(result);

        return result;
    }

In getChildren() all I care about is:

  • Returning wrapped custom project resources
  • Returning the children of a custom project
  • Returning an empty array If I don’t recognize the incoming element .

The createCustomProjectParents()/createCustomProjectParent() methods wrap my custom project IProjects; all other projects will be ignored.

ContentProvider.inputChanged()/ContentProvider constructor/dispose()

    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        _viewer = viewer;
    }

    public ContentProvider() {
        ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
    }

    @Override
    public void dispose() {
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    }

The order of the methods above are out of sequence for the class, but in sequence for this explanation.

In inputChanged() someone has done something that we need to pay attention to. In this case, a new project was added (we haven’t coded for deletions yet). Just save the viewer reference; we’ll need it later. However, if the input has changed we need a way to determine what that change was. Enter resource change listeners.

[If you don’t know anything about event handling in Java, much less Eclipse, the following explanation is probably not going to make a lot of sense.]

For now, the easiest place to register a listener is in the ContentProvider constructor so I placed it there. Of course, whatever we register we should unregister so in dispose() we remove the resource change listener.

Which leads us to the resource change listener itself.

ContentProvider.resourceChanged()

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        _viewer.refresh();
    }

Almost too easy. Our simple navigator just needs to be told to refresh itself when a change occurs.

To summarize: when our input changes we save the viewer; when a resource changes we call resourceChanged() which refreshes the viewer.

Life is beautiful.

What Just Happened?

Now it is time for me to admit something: I hate beets (that is: the vegetable).

Well, more significantly, I have to admit it took a while for me to figure out how simple this truly was. I looked at examples, read posts, drank plenty of Guinness. Finally, I woke up one morning and thought Oh! Is that all it is?

What I found was that the examples did not do what I was trying to do (don’t you hate when that happens?). They always seem to use the pre-existing resourceContent binding in addition to other stuff which didn’t help me at all (well, it did actually help, but kept me confused for too many nights).

The moral of the story: use resourceContent if you need to show the existing resources in a standard way or with exceptions.

Don’t use resourceContent if you want to take full control over the navigator.

That might be too general and all encompassing, but, what the hell, what isn’t?

And of course all this may change as I add more functionality to fill the custom project with custom content. Stay tuned, boys and girls.

Next time: keeping the nodes opened or closed when we add a resource.

References and Thanks

Building a Common Navigator based viewer, Part I: Defining the Viewer

Code

plugin.xml

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.ui.views">
      <category
            id="customnavigator.category"
            name="%category.name">
      </category>
      <view
            allowMultiple="false"
            category="customnavigator.category"
            class="org.eclipse.ui.navigator.CommonNavigator"
            icon="icons/navigator.png"
            id="customnavigator.navigator"
            name="%view.name">
      </view>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.viewer">
      <viewerActionBinding
            viewerId="customnavigator.navigator">
         <includes>
            <actionExtension
                  pattern="org.eclipse.ui.navigator.resources.*">
            </actionExtension>
         </includes>
      </viewerActionBinding>
      <viewerContentBinding
            viewerId="customnavigator.navigator">
         <includes>
            <contentExtension
                  pattern="customnavigator.navigatorContent">
            </contentExtension>
         </includes>
      </viewerContentBinding>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.navigatorContent">
      <navigatorContent
            activeByDefault="true"
            contentProvider="customnavigator.navigator.ContentProvider"
            id="customnavigator.navigatorContent"
            labelProvider="customnavigator.navigator.LabelProvider"
            name="%navigatorContent.name">
         <triggerPoints>
            <instanceof
                  value="org.eclipse.core.resources.IWorkspaceRoot">
            </instanceof>
         </triggerPoints>
         <commonSorter
               class="customnavigator.sorter.SchemaCategorySorter"
               id="customnavigator.sorter.schemacategorysorter">
            <parentExpression>
               <or>
                  <instanceof
                        value="customnavigator.navigator.CustomProjectSchema">
                  </instanceof>
               </or>
            </parentExpression>
         </commonSorter>
      </navigatorContent>
   </extension>

</plugin>
ContentProvider.java
/**
 * Coder beware: this code is not warranted to do anything.
 * Copyright Oct 17, 2009 Carlos Valcarcel
 */
package customnavigator.navigator;

import java.util.ArrayList;
import java.util.List;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResourceChangeEvent;
import org.eclipse.core.resources.IResourceChangeListener;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.jface.viewers.ITreeContentProvider;
import org.eclipse.jface.viewers.Viewer;

import customplugin.natures.ProjectNature;

/**
 * @author carlos and Hidden Clause
 */
public class ContentProvider implements ITreeContentProvider, IResourceChangeListener {

    private static final Object[]   NO_CHILDREN = {};
    Viewer _viewer;
    private int _count = 1;

    public ContentProvider() {
        ResourcesPlugin.getWorkspace().addResourceChangeListener(this, IResourceChangeEvent.POST_CHANGE);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.ITreeContentProvider#getChildren(java.lang.Object)
     */
    @Override
    public Object[] getChildren(Object parentElement) {
        System.out.println("ContentProvider.getChildren: " + parentElement.getClass().getName()); //$NON-NLS-1$
        Object[] children = null;
        if (IWorkspaceRoot.class.isInstance(parentElement)) {
            IProject[] projects = ((IWorkspaceRoot)parentElement).getProjects();
            children = createCustomProjectParents(projects);
        } else if (ICustomProjectElement.class.isInstance(parentElement)) {
            children = ((ICustomProjectElement) parentElement).getChildren();
        } else {
            children = NO_CHILDREN;
        }

        return children;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.ITreeContentProvider#getParent(java.lang.Object)
     */
    @Override
    public Object getParent(Object element) {
        System.out.println("ContentProvider.getParent: " + element.getClass().getName()); //$NON-NLS-1$
        Object parent = null;

        if (IProject.class.isInstance(element)) {
            parent = ((IProject)element).getWorkspace().getRoot();
        } else if (ICustomProjectElement.class.isInstance(element)) {
            parent = ((ICustomProjectElement)element).getParent();
        } // else parent = null if IWorkspaceRoot or anything else

        return parent;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.ITreeContentProvider#hasChildren(java.lang.Object)
     */
    @Override
    public boolean hasChildren(Object element) {
        System.out.println("ContentProvider.hasChildren: " + element.getClass().getName()); //$NON-NLS-1$
        boolean hasChildren = false;

        if (IWorkspaceRoot.class.isInstance(element)) {
            hasChildren = ((IWorkspaceRoot)element).getProjects().length > 0;
        } else if (ICustomProjectElement.class.isInstance(element)) {
            hasChildren = ((ICustomProjectElement)element).hasChildren();
        }
        // else it is not one of these so return false

        return hasChildren;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.IStructuredContentProvider#getElements(java.lang.Object)
     */
    @Override
    public Object[] getElements(Object inputElement) {
        // This is the same as getChildren() so we will call that instead
        System.out.println("ContentProvider.getElements: " + inputElement.getClass().getName()); //$NON-NLS-1$
        return getChildren(inputElement);
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.viewers.IContentProvider#dispose()
     */
    @Override
    public void dispose() {
        ResourcesPlugin.getWorkspace().removeResourceChangeListener(this);
    }

    /*
     * (non-Javadoc)
     * @see
     * org.eclipse.jface.viewers.IContentProvider#inputChanged(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object)
     */
    @Override
    public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
        _viewer = viewer;
    }

    @Override
    public void resourceChanged(IResourceChangeEvent event) {
        _viewer.refresh();
    }

    private Object createCustomProjectParent(IProject parentElement) {

        Object result = null;
        try {
            if (parentElement.getNature(ProjectNature.NATURE_ID) != null) {
                result = new CustomProjectParent(parentElement);
            }
        } catch (CoreException e) {
            // Go to the next IProject
        }

        return result;
    }

    private Object[] createCustomProjectParents(IProject[] projects) {
        Object[] result = null;

        List<Object> list = new ArrayList<Object>();
        for (int i = 0; i < projects.length; i++) {
            Object customProjectParent = createCustomProjectParent(projects[i]);
            if (customProjectParent != null) {
                list.add(customProjectParent);
            } // else ignore the project
        }

        result = new Object[list.size()];
        list.toArray(result);

        return result;
    }

}