Archive

Posts Tagged ‘plug-ins’

Writing an Eclipse Plug-in (Part 23): Common Navigator: Rewriting History

August 14, 2010 3 comments


[I am now using Git for my source control using the EGit plug-in. Of course it is only partially working. One of my projects has fully committed and the others say they are in staging no matter how many times I tell EGit to Commit. Sigh.

Also, starting with this post I am also going to make the code in this convoluted journey available for download in each post as well as the Missing Zip Files page. It will always be available in the format of whatever Eclipse I happen to be using at the time (7/18/10: Eclipse 3.6 Helios Release) so don’t blame me if you are using an older version and something doesn’t work the way I describe it. If you follow me you walk the edge. Of course, in switching to EGit I have no idea where the code for Parts 21 and 22 have gone. I hate when that happens.

Don’t forget to add your favorite plug-ins: in my case that means EGit, EclEmma, Eclipse-CS, and UMLet]

[Woo hoo! Eclipse 3.6 is finally released! I can’t wait to be one of the first to download it! Hey! Where is everybody? Oh, it was released June 23? Really? I hate when I miss an opening party…by almost two months…but it was because I was busy…in Miami…meeting with Michael Westin…]

Well, long time no hear! Yes, I am trying to write these posts a little more often than I have been, but it is amazing how real life gets in the way…what with the cat coming in and out of the box and the squirrels distracting me to no end (don’t get me started on the platypus). I guess I may be stuck with only one post per month (maybe less).

I promise not to beat myself up over it.

Speaking of which: when I started this post the sun was out scorching everything, and I was doing everything I could to stay out of its path. After a failed attempt at getting back into running (you know, diet and exercise will help you live forever, unless you exercise wrong thereby screwing up your leg muscles making it almost impossible to walk), but after a successful attempt to eat better (salad and seafood, anyone?) it is time to pay attention to the things that keep us getting up in the morning and make life worth living (no, not sex, drugs, and rock and roll, though they help): Eclipse plug-ins.

I was going to write a post on genetic programming, but I suspect the cat hid my Koza book because it thought I was going to write a fitness function to force it to choose one state or another. I’ll do that on my next visit to Copenhagen.

What I will post about is, well, fixing the past. Usually, that is quite difficult, but we will make an exception and pretend that we can fix what we did, not because it was wrong, but because our needs have changed (that’s my story and I’m sticking to it).
Read more…

Advertisements

Writing an Eclipse Plug-in (Part 15): Custom Project: Customizing the Perspective Menus (Main menu)

December 19, 2009 5 comments

Ah! Nothing like returning to the scene of the crime.

When we were last at the crime scene we were displaying projects in the Custom Navigator in various states of openness and closedness. What could possibly be next? Well, there are a few choices:

  • Customize the Custom Perspective so our current capabilities are available in the main workspace menu, toolbar and Customize Perspective window.
  • Add navigator popup menus to do things like New, Copy and Properties
  • Display information in the project structure

Even though I expect to create a Form-based editor to hide the ugliness of an XML file that is not necessarily the task of greatest import. In this post I am going to show how to add menu items to our Custom Perspective; we will customize the Custom Navigator popup menu in a future post.

We should always be implementing with the end in mind as a way of keeping extraneous features to a minimum anyway. At least that’s my story.

What (are we doing?)

There are about 7 ways to do almost anything in Eclipse. For example, if you want to open the New Wizard you could go about doing that in the following ways:

  1. Ctrl+N
  2. Main menu: File –> New
  3. Shift+Alt+N – opens the popup menu; select New
  4. Right click on a Project and select New
  5. Right click on a Folder and select New
  6. Toolbar: New button
  7. Toolbar: Java Class button: New: JUnit Test, Class, Interface, Enum, Annotation

And those were just the ones I thought of off the top of my head (okay, so maybe I tried them all first…).

So, in order to compete with all of the other plug-ins out there a plug-in developer has to make sure there are at least a minimum of ways to activate their plug-in: CRUD functionality (New, Open, Save, Delete), opening editor(s) and view(s), open the Properties window, etc.

The good news: Ctrl+N and Shift+Alt+N open the New Wizard window in every case (unless you change the key bindings) so we can safely ignore them.

The bad news: we only have a New Wizard for Custom Projects and two file types. This means that the only way to create a custom resource is either from the main menu (File –> New –> Other), Ctrl+N, or Shift+Alt+N. Since all three will activate the default New Wizard we have not gained anything.

The lesson to learn here is when you add something to the New Wizard your task list should include updating your perspective to support the:

  • Main Menu File menu
  • Toolbar
  • Customize Perspective window

Notice how the only thing this will do is make your existing behavior available in more places. Not a bad thing, just kinda extraneous; convenient for the user, feels like busy work for the developer.

You could also decide to add your GUI functionality to all of the perspectives, but beware: each perspective is specific to the task at hand. Adding the ability to do random things in arbitrary perspectives is bad form. Add functionality to specific perspectives as appropriate (what that means will vary with the capability you are implementing). Adding plugin.xml to a COBOL project doesn’t really mean anything. The road to menu pollution is paved with good intentions. Don’t be afraid to create custom perspectives where you can just go to town adding whatever you want with impunity.

So the tasks for the next few blogs are to add:

  • In the main menu: add Custom Projects, Schema, and Deployment files to File –> New
  • In the Toolbar: add a toolbar group for the above 3 items
  • In Customize Perspective: add the ability to enable/disable all of the above

In the Customize Perspective window adding the ability to enable/disable the above capabilities means:

  • Toolbar Visibility: Custom Project Element Creation (enable by default)
    • Custom Project
    • Schema File
    • Deployment File
  • Menu Visibility: File –> New, (already available, enable by default)
    • Custom Project
    • Schema File
    • Deployment File
  • Command Group Availability: Custom Project Element Creation (enable by default)
  • Shortcuts (affects Menu Visibility; enable by default)
    • New
      • Custom Project
      • Schema File
      • Deployment File
    • Open Perspective (Affects main menu Windows –> Open Perspective)
      • Resource (available, but not enabled)
    • Show View (Affects main menu Windows –> Open View)
      • Custom Plug-in Navigator (available, but not enabled)

How (are we doing it?)

In the main menu: add Custom Projects, Schema, and Deployment files to File –> New

Adding New Wizard entries onto the menu menu is done completely by configuration (my favorite).

  1. Open up plugin.xml for customplugin
  2. Go to Extensions–> Add –> perspectiveExtension and click Finish (yes, you could skip this step and use the existing perspectiveExtension entry)
  3. Change
    • targetID: *

    to

    • targetID: customplugin.perspective
  4. Right click on customplugin.perspective (perspectiveExtension) –> new –> newWizardShortcut
  5. Select newWizardShortcut and enter:
    • ID: customplugin.wizard.new.custom
    • The above is the id of the New Custom Project Wizard entry under org.eclipse.ui,newWizards –> Custom Project (wizard)
  6. Perform steps 4 and 5 for the Schema File (wizard) and Deployment File (wizard)
  7. Save plugin.xml

To make them appear in all of the perspectives change (do not try this at home. I am a trained professional):

  • Extensions –> perspectiveExtension –> targetID: customplugin.perspective

to

  • Extensions –> perspectiveExtension –> targetID: *

Remember, only you can prevent menu pollution.

As a wonderful side-effect the Customize Perspective window has the three New wizard entries entered automatically. Start the runtime workbench, open the Customize Perspective window (Windows –> Customize Perspective), select Menu Visibility and open File –> New.

In addition select the Shortcuts tab of the Customize Perspective window and see that for Submenu New the Shortcut Category has Custom Wizards selected and the three wizards are already checked.

The Toolbar tab and the Command Groups Availability tab are both devoid of entries for our Custom Project. Are we going to take care of that now? Well…no. Next time. Really. I know you’re disappointed, but if you push me I’ll make sure you get a lump of coal.

What Just Happened?

Configuration. Nothing like it for tedious tasks.

How much code did we write: none. It is going to be a good holiday.

Well, that’s it for this entry. It is Sunday, the holidays are getting closer and I was lucky to get this post out.

Next time: Adding the New Wizard functionality to the Toolbar. Maybe. If I get a Sega R-360.

Yuletide Felicitations!

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 10): Custom Project: Creating a Custom File Type

October 31, 2009 7 comments

Happy Halloween! Trick or treat!

Ah! I love the smell of plug-in progress in the morning. Who knows? Somewhere around Part 42 there might be enough done to actually accomplish something.

Speaking of which: our custom project doesn’t do very much at this point. While there is plenty to add to the custom navigator I fear that the customplugin project has been feeling neglected.

I have a few things to add to the customplugin before returning to the navigator; the navigator will continue to burn a significant amount of my attention…unless I decide to go back to my genetic programming examples. Maybe I’ll toss a coin to see which I decide to do next. Or maybe I’ll just check on the cat.

Anyway, for this post we have a few simple things to do:

  • Add the schema file type to the Custom Wizards category
  • Add the deployment file type to the Custom Wizards category
  • Implement the schema file creation code
  • Implement the deployment file creation code

Adding A New Schema File Wizard

You know the drill. Follow the yellow brick road:

  1. customplugin –> plugin.xml –> Extensions –> org.eclipse.ui.newWizards –> new –> wizard
    • id: customplugin.wizard.file.schema
    • name: Schema File
    • class: customplugin.wizards.CustomProjectNewSchemaFile
    • icon: icons/schema-file_16x16.png [copy another image to your icons folder and point to it here]
    • category: customplugin.category.wizards
    • descriptionImage: icons/schema-file_32x32.png [copy another image to your icons folder and point to it here]
  2. Click the class link for CustomProjectNewSchemaFile
  3. Everything looks good
  4. Click Finish to close the New Java Class Wizard
  5. [If you must: start the runtime workbench, press Ctrl+N and look in the Custom Wizards folder; the Schema File and icon should be visible. Exit the runtime workbench.]

Now that we have a schema file creation wizard defined let’s implement some code.

First the constructor so the wizard window has a title:

CustomProjectNewSchemaFile.java

public class CustomProjectNewSchemaFile extends Wizard implements INewWizard {

    public CustomProjectNewSchemaFile() {
        setWindowTitle("New Schema File");
    }

    ...
}

[Update: In doing the above I remembered that I never got around to doing this for the New Custom Project Wizard. The New Custom Project Wizard post, and the New Custom Project Wizard refactoring post have been updated.]

Next, in init() we save the incoming selection values as we will need them later for the constructor of the file creation page:

    private IWorkbench _workbench;
    private IStructuredSelection _selection;

    ...

    /* (non-Javadoc)
     * @see org.eclipse.ui.IWorkbenchWizard#init(org.eclipse.ui.IWorkbench, org.eclipse.jface.viewers.IStructuredSelection)
     */
    @Override
    public void init(IWorkbench workbench, IStructuredSelection selection) {
        _workbench = workbench;
        _selection = selection;
    }

    ...

In addPages() we call a class we haven’t created yet: WizardSchemaNewFileCreationPage. It subclasses WizardNewFileCreationPage which does exactly what we want: create a file for us. This is just another reason why it is so important to know what pieces are available within Eclipse for your use.

    private WizardNewFileCreationPage _pageOne;

    ...

    @Override
    public void addPages() {
        super.addPages();

        _pageOne = new WizardSchemaNewFileCreationPage(_selection);

        addPage(_pageOne);
    }

    ...

The variable declaration for _pageOne is purposely declared as WizardNewFileCreationPage even though the call in addPages() is to WizardSchemaNewFileCreationPage. We are only overriding method calls to facilitate the initialization of the object. Create the new class: press Ctrl+1 on the line in addPages() and select Create Class WizardSchemaNewFileCreationPage. It should automatically select WizardNewFileCreationPage as the superclass. Click Finish to create the new class.

Implement WizardSchemaNewFileCreationPage with:

public class WizardSchemaNewFileCreationPage extends WizardNewFileCreationPage {

    public WizardSchemaNewFileCreationPage(IStructuredSelection selection) {
        super("Custom Plug-in Schema File Wizard", selection);

        setTitle("Schema File Wizard");
        setDescription("Create a Schema File");
        setFileExtension("xml");
    }

    @Override
    protected InputStream getInitialContents() {
        String xmlTemplate = "<hc-schema>\n"
            + "  <tables></tables>\n"
            + "  <filters></filters>\n"
            + "  <views></views>\n"
            + "</hc-schema>\n";
        return new ByteArrayInputStream(xmlTemplate.getBytes());
    }
}

The constructor above speaks for itself. The getInitialContents() not so much. This method is used by the wizard to find default contents for the new file if one is created. Since this is my schema file I now have a test version of the file to display. Since this will be refactored into another file we are safe hard-coding it for now.

Returning to CustomProjectNewSchemaFile: Add the following to performFinish():

    @Override
    public boolean performFinish() {
        boolean result = false;

        IFile file = _pageOne.createNewFile();
        result = file != null;

        if (result) {
            try {
                IDE.openEditor(_workbench.getActiveWorkbenchWindow().getActivePage(), file);
            } catch (PartInitException e) {
                e.printStackTrace();
            }
        } // else no file created...result == false

        return result;
    }

The code is simple and self explanatory except for the call to openEditor();it automatically opens the editor for our XML file using whatever editor has been configured for XML files or the default editor if an XML editor is not available. The first argument to openEditor() needs a reference to IWorkbenchPage and we get it by asking the workbench for it. A small price to pay to have the editor opened automatically for us.

Go ahead and try the runtime workbench to check out your handiwork. You should be able to create a plain vanilla resource project and create a schema file to go with it. Life is good.

With that test out of the way we are now emboldened to use a template file instead of a hard coded XML string.

Create a folder named templates under your customplugin folder. In the templates folder create a file named schema-template.xml. Insert the following XML into the file.

<hc-schema>
    <tables>
    </tables>
    <views>
    </views>
    <filters>
    </filters>
</hc-schema>

With the above in place update getInitialContents():

WizardSchemaNewFileCreationPage.java
    protected InputStream getInitialContents() {
        String templateFilePath = "/templates/schema-template.xml";
        InputStream inputStream = null;
        try {
            inputStream = Activator.getDefault().getBundle().getEntry(templateFilePath).openStream();
        } catch (IOException e) {
            // send back null
        }

        return inputStream;
    }

Confirm nothing is broken by starting the runtime workbench.

Adding A New Deployment File Wizard

Now let’s do the same for the deployment file:

  1. customplugin –> plugin.xml –> Extensions –> org.eclipse.ui.newWizards –> new –> wizard
    • id: customplugin.wizard.file.deployment
    • name: Deployment File
    • class: customplugin.wizards.CustomProjectNewDeploymentFile
    • icon: icons/deployment-file_16x16.png [copy another image to your icons folder and point to it here]
    • category: customplugin.category.wizards
    • descriptionImage: icons/deployment-file_32x32.png [copy another image to your icons folder and point to it here]
      • Click the class link for CustomProjectNewDeploymentFile
      • Everything looks good
      • Click Finish to close the New Java Class Wizard
      • [Again, quick test: start the runtime workbench, press Ctrl+N and look in the Custom Wizards folder; the Deployment File and icon should be visible. Exit the runtime workbench.]

          Since we already know that we are using the WizardNewFileCreationPage as a subclass for the page that will create our file let’s create it upfront.

          public class WizardDeploymentNewFileCreationPage extends WizardNewFileCreationPage {
          
              public WizardDeploymentNewFileCreationPage(IStructuredSelection selection) {
                  super("Custom Plug-in Deployment File Wizard", selection);
                  
                  setTitle("Deployment File Wizard");
                  setDescription("Create a Deployment File");
                  setFileExtension("xml");
              }
          
              @Override
              protected InputStream getInitialContents() {
                  String templateFilePath = "/templates/deployment-template.xml";
                  InputStream inputStream = null;
                  try {
                      inputStream = Activator.getDefault().getBundle().getEntry(templateFilePath).openStream();
                  } catch (IOException e) {
                      // send back null
                  }
          
                  return inputStream;
              }
          }
          

          Create another template file in the templates folder named deployment-schema.xml. Contents follow:

          <hc-deployment>
          </hc-deployment>
          

          The actual wizard code for the CustomProjectNewDeploymentFile also looks suspiciously like the code for the schema file:

          public abstract class CustomProjectNewDeploymentFile extends Wizard implements INewWizard {
          
              private WizardNewFileCreationPage _pageOne;
              private IWorkbench                _workbench;
              private IStructuredSelection      _selection;
          
              public CustomProjectNewDeploymentFile() {
                  setWindowTitle("New Deployment File");
              }
          
              @Override
              public void addPages() {
                  super.addPages();
          
                  _pageOne = new WizardDeploymentNewFileCreationPage(_selection);
          
                  addPage(_pageOne);
              }
              @Override
              public boolean performFinish() {
                  boolean result = false;
          
                  IFile file = _pageOne.createNewFile();
                  result = file != null;
          
                  if (result) {
                      try {
                          IDE.openEditor(_workbench.getActiveWorkbenchWindow().getActivePage(), file);
                      } catch (PartInitException e) {
                          e.printStackTrace();
                      }
                  } // else no file created...result == false
          
                  return result;
              }
          
              @Override
              public void init(IWorkbench workbench, IStructuredSelection selection) {
                  _workbench = workbench;
                  _selection = selection;
              }
          }
          

          After all of the above in the runtime workbench you should be able to create a schema file and a deployment file and the default text editor should open once each file is defined in the New File Wizard.

          Run a quick test. From the runtime workbench create a project, create a schema file and deployment file.

          Woo hoo! All done!

          Oh…wait…time to refactor a few things.

          Refactor Strings

          Time to wash up!

          Once again, go to the plugin.xml Overview tab of the customplugin and run the Externalize String Wizard to externalize the two new strings we added. Change:

          • wizard.name.0 to wizard.name.schema
          • wizard.name.1 to wizard.name.deployment

          In the schema file related files refactor:

          • the wizard window title to WIZARD_NAME and add $NON-NLS$ using the Quick Fix functionality.
          • the WizardSchemaNewFileCreationPage title to PAGE_NAME.

          In WizardSchemaNewFileCreationPage:

          • Open the Externalize String wizard (one way: hover over one of the strings to open the wizard)
          • Change WizardSchemaNewFileCreationPage_0 to WizardSchemaNewFileCreationPage_Schema_File_Wizard by clicking in the desired field and typing Schema_File_Wizard
          • Change WizardSchemaNewFileCreationPage_1 to WizardSchemaNewFileCreationPage_Create_a_Schema_File by clicking in the desired field and typing Create_a_Schema_File
          • Change WizardSchemaNewFileCreationPage_2 to WizardSchemaNewFileCreationPage_Schema_File_Extension by clicking in the desired field and typing Schema_File_Extension
          • Change WizardSchemaNewFileCreationPage_3 to WizardSchemaNewFileCreationPage_Schema_Template_Location by clicking in the desired field and typing Schema_Template_Location
          • Click Configure in the Accessor class section at the bottom of the window.
          • Click Browse for Class Name and select NewWizardMessages; might as well put these strings with the others.
          • Click OK
          • Click Next and take a look at the various changes that are about to take place
          • Click Finish

          Do the same for the WizardDeploymentNewFileCreationPage only use the word Deployment instead of Schema.

          A question I have asked myself is: Should the WIZARD_NAME and PAGE_NAME also be externalized? While these posts have been examples of how to do things, with a mix of useful and useless things, the answer is…it depends. If i18n is important to the ultimate user of the plug-in then yes, externalize everything, otherwise don’t sweat it.

          [If I were being paid to do this I would fight tooth-and-nail to externalize every last string. It makes absolutely no sense to have to recompile one or more files just because a string has changed.]

          Refactor Code

          Both CustomProjectNewSchemaFile and CustomProjectNewDeploymentFile have the same constructor, performFinish() and init() methods. Let’s create a new parent class and move everything, but addPages() into it (it is the only method with custom code).

          The following will do the trick:

          1. Right click in the editor for CustomProjectNewSchemaFile and select Refactor –> Extract Superclass
            • Superclass Name: CustomProjectNewFile
          2. In Types to Extract a Superclass From click Add and add CustomProjectNewDeploymentFile
          3. In Specify Actions for Members check:
            • _pageOne
            • _workbench
            • _selection
            • performFinish()
            • init()
          4. Click Next
          5. In Subtypes of Type select init() and performFinish() to be removed from both subclasses. Notice that the constructors have not been recognized as having common behavior.
          6. Click Finish.

          Oddly enough, when the refactoring is over the superclass has a compile error! How could that be? Well, CustomProjectNewFile is missing the INewWizard interface that is needed to properly find init(). The refactoring missed it, but the compiler didn’t. Manually (yuck) add it to CustomProjectNewFile and remove it from CustomProjectNewSchemaFile and CustomProjectNewDeploymentFile (if you don’t remove it nothing will happen, but your code might develop cooties).

          Let’s change the constructors for CustomProjectNewSchemaFile and CustomProjectNewDeploymentFile. Change:

          CustomProjectNewSchemaFile.java
          
              public CustomProjectNewSchemaFile() {
                  setWindowTitle(WIZARD_NAME);
              }
          

          to:

          CustomProjectNewSchemaFile.java
          
              public CustomProjectNewSchemaFile() {
                  super(WIZARD_NAME);
              }
          

          Change CustomProjectNewDeploymentFile from:

          CustomProjectNewDeploymentFile.java
          
              public CustomProjectNewDeploymentFile() {
                  setWindowTitle(WIZARD_NAME);
              }
          

          to:

          CustomProjectNewDeploymentFile.java
          
              public CustomProjectNewDeploymentFile() {
                  super(WIZARD_NAME);
              }
          

          Use quick fix to create the new constructor in the CustomProjectNewFile parent class:

              public CustomProjectNewFile(String wizardName) {
                  setWindowTitle(wizardName);
              }
          

          Not too shabby.

          Run a quick test if you are so inclined.

          customplugin-part-10-custom-file-types-in-editor

          What just happened?

          Well, this was an interesting visit. We certainly coded more than usual for the customplugin, but it was all pretty trivial.

          We created two new wizards based on the built-in file creation page and create two templates so our custom files have a good starting point.

          The refactoring will make changing labels and such easier.

          The body count so far:
          customplugin: 11 classes, one properties file
          customnavigator: 13 classes

          Next post: back to the Custom Navigator, probably to display the Stored Procedure category with a Java file as an entry. The deployment file is not displayed, but will be found in the deployment-files folder.

          Unless I decide to go back to GP for a while.

          In celebration of this religious holiday go have some candy.

          (Anybody seen a cat?)

          References and Thanks

          How to create a new File Wizard? for reminding me about bundle functionality.

          Code

          CustomProjectNewFile.java

          /**
           * Coder beware: this code is not warranted to do anything.
           *
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          import org.eclipse.core.resources.IFile;
          import org.eclipse.jface.viewers.IStructuredSelection;
          import org.eclipse.jface.wizard.Wizard;
          import org.eclipse.ui.INewWizard;
          import org.eclipse.ui.IWorkbench;
          import org.eclipse.ui.PartInitException;
          import org.eclipse.ui.dialogs.WizardNewFileCreationPage;
          import org.eclipse.ui.ide.IDE;
          
          public abstract class CustomProjectNewFile extends Wizard implements INewWizard {
          
              protected WizardNewFileCreationPage _pageOne;
              private IWorkbench _workbench;
              protected IStructuredSelection _selection;
          
              public CustomProjectNewFile() {
                  super();
              }
          
              @Override
              public boolean performFinish() {
                  boolean result = false;
                  
                  IFile file = _pageOne.createNewFile();
                  result = file != null;
              
                  if (result) {
                      try {
                          IDE.openEditor(_workbench.getActiveWorkbenchWindow().getActivePage(), file);
                      } catch (PartInitException e) {
                          e.printStackTrace();
                      }
                  } // else no file created...result == false
                  
                  return result;
              }
          
              @Override
              public void init(IWorkbench workbench, IStructuredSelection selection) {
                  _workbench = workbench;
                  _selection = selection;
              }
          
          }
          
          

          CustomProjectNewSchemaFile.java

          /**
           * Coder beware: this code is not warranted to do anything.
           *
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          
          /**
           * @author carlos
           *
           */
          public class CustomProjectNewSchemaFile extends CustomProjectNewFile {
          
              private static final String WIZARD_NAME = "New Schema File"; //$NON-NLS-1$
              /**
               * 
               */
              public CustomProjectNewSchemaFile() {
                  setWindowTitle(WIZARD_NAME);
              }
          
              @Override
              public void addPages() {
                  super.addPages();
          
                  _pageOne = new WizardSchemaNewFileCreationPage(_selection);
          
                  addPage(_pageOne);
              }
          }
          
          

          CustomProjectNewDeploymentFile.java

          /**
           * Coder beware: this code is not warranted to do anything.
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          
          /**
           * @author carlos
           */
          public class CustomProjectNewDeploymentFile extends CustomProjectNewFile {
              private static final String WIZARD_NAME = "New Deployment File"; //$NON-NLS-1$
          
              public CustomProjectNewDeploymentFile() {
                  setWindowTitle(WIZARD_NAME);
              }
          
              @Override
              public void addPages() {
                  super.addPages();
          
                  _pageOne = new WizardDeploymentNewFileCreationPage(_selection);
          
                  addPage(_pageOne);
              }
          
          }
          

          WizardSchemaNewFileCreationPage.java

          /**
           * Coder beware: this code is not warranted to do anything.
           *
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          import java.io.IOException;
          import java.io.InputStream;
          
          import org.eclipse.jface.viewers.IStructuredSelection;
          import org.eclipse.ui.dialogs.WizardNewFileCreationPage;
          
          import customplugin.Activator;
          
          /**
           * @author carlos
           *
           */
          public class WizardSchemaNewFileCreationPage extends WizardNewFileCreationPage {
          
              private static final String PAGE_NAME = "Custom Plug-in Schema File Wizard"; //$NON-NLS-1$
          
              public WizardSchemaNewFileCreationPage(IStructuredSelection selection) {
                  super(PAGE_NAME, selection);
                  
                  setTitle(NewWizardMessages.WizardSchemaNewFileCreationPage_Schema_File_Wizard);
                  setDescription(NewWizardMessages.WizardSchemaNewFileCreationPage_Create_a_Schema_File);
                  setFileExtension(NewWizardMessages.WizardSchemaNewFileCreationPage_Schema_File_Extension);
              }
          
              @Override
              protected InputStream getInitialContents() {
                  String templateFilePath = NewWizardMessages.WizardSchemaNewFileCreationPage_Schema_Template_Location;
                  InputStream inputStream = null;
                  try {
                      inputStream = Activator.getDefault().getBundle().getEntry(templateFilePath).openStream();
                  } catch (IOException e) {
                      // send back null
                  }
          
                  return inputStream;
              }
          
          }
          

          WizardDeploymentNewFileCreationPage.java

          /**
           * Coder beware: this code is not warranted to do anything.
           *
           * Copyright Oct 31, 2009 Carlos Valcarcel
           */
          package customplugin.wizards;
          
          import java.io.IOException;
          import java.io.InputStream;
          
          import org.eclipse.jface.viewers.IStructuredSelection;
          import org.eclipse.ui.dialogs.WizardNewFileCreationPage;
          
          import customplugin.Activator;
          
          /**
           * @author carlos
           *
           */
          public class WizardDeploymentNewFileCreationPage extends WizardNewFileCreationPage {
          
              private static final String PAGE_NAME = "Custom Plug-in Deployment File Wizard"; //$NON-NLS-1$
          
              public WizardDeploymentNewFileCreationPage(IStructuredSelection selection) {
                  super(PAGE_NAME, selection);
                  
                  setTitle(NewWizardMessages.WizardDeploymentNewFileCreationPage_Deployment_File_Wizard);
                  setDescription(NewWizardMessages.WizardDeploymentNewFileCreationPage_Create_a_Deployment_File);
                  setFileExtension(NewWizardMessages.WizardDeploymentNewFileCreationPage_Deployment_File_Extension);
              }
          
              @Override
              protected InputStream getInitialContents() {
                  String templateFilePath = NewWizardMessages.WizardDeploymentNewFileCreationPage_Deployment_Template_Location;
                  InputStream inputStream = null;
                  try {
                      inputStream = Activator.getDefault().getBundle().getEntry(templateFilePath).openStream();
                  } catch (IOException e) {
                      // send back null
                  }
          
                  return inputStream;
              }
          
          }
          

Writing an Eclipse Plug-in (Part 4): Create a Custom Project in Eclipse – New Project Wizard: the Behavior

July 26, 2009 50 comments

In a previous post I showed how to get the GUI aspect of a New Wizard for the creation of a new project type up and running rather quickly. Even I was surprised; so surprised that I did it 2 more times just to make sure I wasn’t cheating somehow.

One of my side goals was to write the least amount of code and to let the plug-in extensions handle most of the integration of the various components.

With the GUI displaying the minimum expected GUI behavior it is now time to add minimum project-creation behavior.

The GUI counts as the platform so I won’t test it unless there is some strange behavior that I can’t explain. The creation of the project itself needs to be tested as I am adding a folder structure and a nature and I want to make sure that works. The test will also make it easier to extend my project structure in a controlled way.

Here are the steps:

  1. Create a new plug-in project, I have named it customplugin.test.
  2. plugin.xml –> Dependencies: Add org.junit4. This is required by the runtime workbench. If you see the dreaded No Runnable Methods message then you forgot to do this.
  3. Download dom4j from http://sourceforge.net/project/showfiles.php?group_id=16035&package_id=14121&release_id=328664. Extract the zip someplace safe; you will need two of the jar files in the next step.
  4. Create a folder named lib directly under customplugin.test and copy dom4j-1.6.1.jar and jaxen-1.1-beta-6.jar into customplugin.test/lib. One of the tests will open the .project file and check that the nature has been added. Using dom4j will make that much easier.
  5. In the customplugin.test plugin.xml file:
    • Runtime –> Classpath: Add the two jar files in the lib folder.
  6. Open the project Properties dialog. In the Project References element put a check mark next to the customplugin project (if you have been downloading the zip files then put a check mark next to customplugin_1.0.0.3).

Now the fun part: what should I test? Well, to create the simplest project within Eclipse involves only two things:

  • The name of the project
  • The location of the project

Truth be told you only need the project name. If null is given as the location name Eclipse uses the default workspace as the project destination.

I came up with only 4 tests:

  • Good project name, default location (null)
  • Good project name, different location
  • Null project name
  • Empty project name

The good test, regardless of workspace location, has to check that:

  • The project returned is non-null
  • The Custom nature was added
  • The .project file was created properly
  • The custom folder structure was created

The concept of a custom nature has finally appeared. Though a nature is typically used to tie a builder together to a project type, natures are also flags. If you get an IProject object looking at its nature or natures is a great way to determine what kind of project you are dealing with.

Add a nature by:

  • Opening your plugin.xml file
  • Going to the Extensions tab
  • Clicking Add
  • Finding and selecting the org.eclipse.core.resources.natures
  • Clicking Finish

First, select org.eclipse.core.resources.natures and enter in the ID field customplugin.projectNature. Next, open the (runtime) node, select the (run) node and enter a class name of customplugin.natures.ProjectNature. Click on the class link and click Finish on the New Java Class dialog.

I added the nature id as a string constant to make it easier to use in various parts of the code that will be implemented.

package customplugin.natures;

import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectNature;

import org.eclipse.core.runtime.CoreException;

public class ProjectNature implements IProjectNature {

    public static final String NATURE_ID = "customplugin.projectNature"; //$NON-NLS-1$

    @Override
    public void configure() throws CoreException {
        // TODO Auto-generated method stub
    }

    @Override
    public void deconfigure() throws CoreException {
        // TODO Auto-generated method stub
    }

    @Override
    public IProject getProject() {
        // TODO Auto-generated method stub
        return null;
    }

    @Override
    public void setProject(IProject project) {
        // TODO Auto-generated method stub
    }

}

For now, you don’t need more than that so feel free to close the Java editor on ProjectNature after you take a quick look at the generated code.

The following code went through a few iterations before it came to look like this, but it didn’t take that long; it took longer to strategize how I wanted to do it. It would take a long time to work through the mental steps I went through to create the CustomProjectSupport and CustomProjectSupportTest class. The code for the test is first, followed by the code that was slowly pulled together.

I decided that I was going to implement the code to create the project, add the nature and create my folder structure in a separate class to make it easier to test and insert into the wizard’s performFinish() method. It will be named CustomProjectSupport. The test class will be named CustomProjectSupportTest.

Add org.eclipse.core.resources to plugin.xml (well, really MANIFEST.MF) –> Dependencies or else the code won’t compile.
Here is the test code:

package customplugin.projects;

import java.io.File;
import java.net.URI;
import java.net.URISyntaxException;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.io.SAXReader;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.runtime.AssertionFailedException;
import org.eclipse.core.runtime.CoreException;
import org.junit.Assert;
import org.junit.Test;

import customplugin.natures.ProjectNature;

public class CustomProjectSupportTest {
    @SuppressWarnings("nls")
    @Test
    public void testCreateProjectWithDifferentLocationArg() throws URISyntaxException, DocumentException, CoreException {
        String workspaceFilePath = "/media/disk/home/carlos/Projects/junit-workspace2";
        File workspace = createTempWorkspace(workspaceFilePath);

        String projectName = "delete-me"; //$NON-NLS-1$
        String projectPath = workspaceFilePath + "/" + projectName;
        URI location = new URI("file:/" + projectPath);

        assertProjectDotFileAndStructureAndNatureExist(projectPath, projectName, location);

        deleteTempWorkspace(workspace);
    }

    @Test
    public void testCreateProjectWithEmptyNameArg() {
        String projectName = " "; //$NON-NLS-1$
        assertCreateProjectWithBadNameArg(projectName);
    }

    @Test
    public void testCreateProjectWithNullNameArg() {
        String projectName = null;
        assertCreateProjectWithBadNameArg(projectName);
    }

    @SuppressWarnings("nls")
    @Test
    public void testCreateProjectWithGoodArgs() throws DocumentException, CoreException {
        // This is the default workspace for this plug-in
        String workspaceFilePath = "/media/disk/home/carlos/Projects/junit-workspace";
        String projectName = "delete-me";
        String projectPath = workspaceFilePath + "/" + projectName;

        URI location = null;
        assertProjectDotFileAndStructureAndNatureExist(projectPath, projectName, location);
    }

    @SuppressWarnings("nls")
    private void assertProjectDotFileAndStructureAndNatureExist(String projectPath, String name, URI location) throws DocumentException,
            CoreException {
        IProject project = CustomProjectSupport.createProject(name, location);

        String projectFilePath = projectPath + "/" + ".project";
        String[] emptyNodes = { "/projectDescription/comment", "/projectDescription/projects", "/projectDescription/buildSpec" };
        String[] nonEmptyNodes = { "/projectDescription/name", "/projectDescription/natures/nature" };

        Assert.assertNotNull(project);
        assertFileExists(projectFilePath);
        assertAllElementsEmptyExcept(projectFilePath, emptyNodes, nonEmptyNodes);
        assertNatureIn(project);
        assertFolderStructureIn(projectPath);

        project.delete(true, null);
    }

    @SuppressWarnings("nls")
    private void assertFolderStructureIn(String projectPath) {
        String[] paths = { "parent/child1-1/child2", "parent/child1-2/child2/child3" };
        for (String path : paths) {
            File file = new File(projectPath + "/" + path);
            if (!file.exists()) {
                Assert.fail("Folder structure " + path + " does not exist.");
            }
        }
    }

    private void assertNatureIn(IProject project) throws CoreException {
        IProjectDescription descriptions = project.getDescription();
        String[] natureIds = descriptions.getNatureIds();
        if (natureIds.length != 1) {
            Assert.fail("No natures found in project."); //$NON-NLS-1$
        }

        if (!natureIds[0].equals(ProjectNature.NATURE_ID)) {
            Assert.fail("CustomProject natures not found in project."); //$NON-NLS-1$
        }
    }

    private void assertAllElementsEmptyExcept(String projectFilePath, String[] emptyNodes, String[] nonEmptyNodes) throws DocumentException {
        SAXReader reader = new SAXReader();
        Document document = reader.read(projectFilePath);
        int strLength;
        for (String emptyNode : emptyNodes) {
            strLength = document.selectSingleNode(emptyNode).getText().trim().length();
            if (strLength != 0) {
                Assert.fail("Node " + emptyNode + " was non-empty!"); //$NON-NLS-1$ //$NON-NLS-2$
            }
        }

        for (String nonEmptyNode : nonEmptyNodes) {
            strLength = document.selectSingleNode(nonEmptyNode).getText().trim().length();
            if (strLength == 0) {
                Assert.fail("Node " + nonEmptyNode + " was empty!"); //$NON-NLS-1$//$NON-NLS-2$
            }
        }
    }

    private void assertFileExists(String projectFilePath) {
        File file = new File(projectFilePath);

        if (!file.exists()) {
            Assert.fail("File " + projectFilePath + " does not exist."); //$NON-NLS-1$//$NON-NLS-2$
        }
    }

    private void assertCreateProjectWithBadNameArg(String name) {
        URI location = null;
        try {
            CustomProjectSupport.createProject(name, location);
            Assert.fail("The call to CustomProjectSupport.createProject() did not fail!"); //$NON-NLS-1$
        } catch (AssertionFailedException e) {
            // An exception was thrown as expected; the test passed.
        }
    }

    private void deleteTempWorkspace(File workspace) {
        boolean deleted = workspace.delete();
        if (!deleted) {
            Assert.fail("Unable to delete the new workspace dir at " + workspace); //$NON-NLS-1$
        }
    }

    private File createTempWorkspace(String pathToWorkspace) {
        File workspace = new File(pathToWorkspace);
        if (!workspace.exists()) {
            boolean dirCreated = workspace.mkdir();
            if (!dirCreated) {
                Assert.fail("Unable to create the new workspace dir at " + workspace); //$NON-NLS-1$
            }
        }

        return workspace;
    }

}

The CustomProjectSupport code looks like this:

package customplugin.projects;

import java.net.URI;

import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IProjectDescription;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;

import customplugin.natures.ProjectNature;

public class CustomProjectSupport {
    /**
     * For this marvelous project we need to:
     * - create the default Eclipse project
     * - add the custom project nature
     * - create the folder structure
     *
     * @param projectName
     * @param location
     * @param natureId
     * @return
     */
    public static IProject createProject(String projectName, URI location) {
        Assert.isNotNull(projectName);
        Assert.isTrue(projectName.trim().length() &gt; 0);

        IProject project = createBaseProject(projectName, location);
        try {
            addNature(project);

            String[] paths = { "parent/child1-1/child2", "parent/child1-2/child2/child3" }; //$NON-NLS-1$ //$NON-NLS-2$
            addToProjectStructure(project, paths);
        } catch (CoreException e) {
            e.printStackTrace();
            project = null;
        }

        return project;
    }

    /**
     * Just do the basics: create a basic project.
     *
     * @param location
     * @param projectName
     */
    private static IProject createBaseProject(String projectName, URI location) {
        // it is acceptable to use the ResourcesPlugin class
        IProject newProject = ResourcesPlugin.getWorkspace().getRoot().getProject(projectName);

        if (!newProject.exists()) {
            URI projectLocation = location;
            IProjectDescription desc = newProject.getWorkspace().newProjectDescription(newProject.getName());
            if (location != null &amp;&amp; ResourcesPlugin.getWorkspace().getRoot().getLocationURI().equals(location)) {
                projectLocation = null;
            }

            desc.setLocationURI(projectLocation);
            try {
                newProject.create(desc, null);
                if (!newProject.isOpen()) {
                    newProject.open(null);
                }
            } catch (CoreException e) {
                e.printStackTrace();
            }
        }

        return newProject;
    }

    private static void createFolder(IFolder folder) throws CoreException {
        IContainer parent = folder.getParent();
        if (parent instanceof IFolder) {
            createFolder((IFolder) parent);
        }
        if (!folder.exists()) {
            folder.create(false, true, null);
        }
    }

    /**
     * Create a folder structure with a parent root, overlay, and a few child
     * folders.
     *
     * @param newProject
     * @param paths
     * @throws CoreException
     */
    private static void addToProjectStructure(IProject newProject, String[] paths) throws CoreException {
        for (String path : paths) {
            IFolder etcFolders = newProject.getFolder(path);
            createFolder(etcFolders);
        }
    }

    private static void addNature(IProject project) throws CoreException {
        if (!project.hasNature(ProjectNature.NATURE_ID)) {
            IProjectDescription description = project.getDescription();
            String[] prevNatures = description.getNatureIds();
            String[] newNatures = new String[prevNatures.length + 1];
            System.arraycopy(prevNatures, 0, newNatures, 0, prevNatures.length);
            newNatures[prevNatures.length] = ProjectNature.NATURE_ID;
            description.setNatureIds(newNatures);

            IProgressMonitor monitor = null;
            project.setDescription(description, monitor);
        }
    }

}

For the above tests to run you need to export some of the packages from the customplugin project. In the customplugin plugin.xml Runtime tab add the following packages to the Exported Packages list:

  • customplugin.natures
  • customplugin.projects

Finally, let’s add CustomProjectSupport to the CustomProjectNewWizard:

    @Override
    public boolean performFinish() {
        String name = _pageOne.getProjectName();
        URI location = null;
        if (!_pageOne.useDefaults()) {
            location = _pageOne.getLocationURI();
        } // else location == null

        CustomProjectSupport.createProject(name, location);

        return true;
    }

One last thing: let’s set up the process of creating a new project to end with the opening of the Custom Plug-in Perspective.

Select the “Custom Project (wizard)” entry under org.eclipse.ui.newWizards.

  • finalPerspective: customplugin.perspective
  • Save the file

Add IExecutableExtension to CustomProjectNewWizard:

public class CustomProjectNewWizard extends Wizard implements INewWizard, IExecutableExtension {

Let the editor add the unimplemented (and empty) method setInitializationData().

Before you implement the method add the following field to hold the plug-in configuration information necessary to make the perspective change:

private IConfigurationElement _configurationElement;

The plug-in will call setInitializationData() to supply the plug-in with the information it needs to display the proper perspective when performFinish() is complete.

    @Override
    public void setInitializationData(IConfigurationElement config, String propertyName, Object data) throws CoreException {
        _configurationElement = config;
    }

In performFinish() add the call to updatePerspective():

    @Override
    public boolean performFinish() {
        String name = _pageOne.getProjectName();
        URI location = null;
        if (!_pageOne.useDefaults()) {
            location = _pageOne.getLocationURI();
            System.err.println("location: " + location.toString()); //$NON-NLS-1$
        } // else location == null

        CustomProjectSupport.createProject(name, location);
        // Add this
        BasicNewProjectResourceWizard.updatePerspective(_configurationElement);

        return true;
    }

All done. Go create a project and check that the tests actually did their jobs. For extra points, open the Custom Project Navigator. It should show you the same thing as the Package Navigator or the plain vanilla Navigator view.

On the off chance I missed something or did not explain something properly, please let me know. I wrote this all down as I was doing it so I may have missed a step or 12.

Perhaps I should re-release my book….

Writing an Eclipse Plug-in (Part 2): Creating a custom project in Eclipse – Adding to the New Project Wizard

July 11, 2009 19 comments

Last time I discussed this I mentioned that the steps to create the custom project will be:

  • Open the New Wizard dialog.
  • Open the Custom Project folder
  • Select the Custom Project item
  • Press Next
  • Enter the name of the project and a location in which to put it
  • Press Finish
  • A custom perspective will open displaying a custom navigator.

In this entry I will not be implementing any behavior. Soon. However, the GUI pieces leading to the desired workflow will be implemented.

Let’s get to work.

Assumptions: Eclipse 3.4.

1. Create a Plug-in Project

  • Name: customplugin
  • Eclipse version: 3.4
  • Click Next
  • Execution environment: JavaSE-1.6
  • Would you like to create a rich client application? No.
  • Click Finish

The only class that should have been generated was customplugin.Activator. It can be ignored for now.

2. In the Extension tab:

  • Click Add
  • Type new and select the org.eclipse.ui.newWizards extension (do not choose any available templates)
  • Click Finish.
  • Right click on org.eclipse.ui.newWizards and select New –> Category
  • With the “name (category)” selected enter an id of customplugin.category.wizards
  • Name: Custom Wizards
  • Save the file
  • Right click on org.eclipse.ui.newWizards and select New –> Wizard
  • With the “name (wizard)” selected enter:
    • id: customplugin.wizard.new.custom
    • name: Custom Project
    • class: customplugin.wizards.CustomProjectNewWizard
    • category: customplugin.category.wizards
  • Save the file

3. Create the CustomProjectWizard class

  • Click on the class link for customplugin.wizards.CustomProjectNewWizard to open the New Class Wizard.
  • In the New Class Wizard click Finish.
  • The CustomProjectNewWizard class will open in the workspace.

Believe it or not this is enough for a quick test of the plug-in:

  • Right click on the customplugin label in the Package Explorer and select Run As –> Run Configurations.
  • Right click on Eclipse Applications and select New
    • Name: customplugin
  • Click on the Plug-ins tab to the right
  • Launch with: Plug-ins selected below
  • Uncheck the Workspace folder
  • Check the customplugin project
  • Click Run

When the runtime workbench opens press Ctrl+N. The Custom Wizards folder should be under the General folder and the Custom Project item should be in the Custom Wizards folder.

Clicking Next will do nothing. That is fine; we will take care of that next. Quit the runtime workbench.

4. Add the WizardNewProjectCreationPage to the CustomProjectWizard

  • Add a private field to the CustomProjectWizard:
        private WizardNewProjectCreationPage _pageOne;
  • You will get a compile error. Return to the plugin.xml file and select the Dependencies tab.
  • Click Add and select org.eclipse.ui.ide. Click Finish.
  • Return to CustomProjectWizard. Press Ctrl+Shift+O to add any missing imports.
  • Save the file. The compile error should disappear.
  • Override addPages() (defined in the parent Wizard class):
    @Override
    public void addPages() {
        super.addPages();

        _pageOne = new WizardNewProjectCreationPage("From Scratch Project Wizard");
        _pageOne.setTitle("From Scratch Project");
        _pageOne.setDescription("Create something from scratch.");

        addPage(_pageOne);
    }
  • Have performFinish() return true.
  • Have the constructor set the title field.
  •     public CustomProjectNewWizard() {
            setWindowTitle(WIZARD_NAME);
        }
    
  • Save the file.

Time for a manual test:

  1. Start the runtime workbench.
  2. Press Ctrl+N.
  3. Open the Custom Project folder.
  4. Select the Custom Project item and click Next.
  5. Enter a project name and click Finish.
  6. Whatever perspective you started with will still be there.
  7. Quit the runtime workbench.

Not bad for one extension with two entries and two Java files that were mostly (except for the wizard file) untouched by us.

Next, create a custom navigator using the Common Navigator framework. The Common Navigator View template will create a fully functioning navigator which will be ideal for our purposes.

In the Extension tab:

  • Click Add, click the Extension Wizards tab and select the Common Navigator View template.
  • Click Next
  • Enter the following:
    • View Id: customplugin.navigator
    • View Name: Custom Plug-in Navigator
    • Uncheck Add to Resource Perspective
  • Click Finish.
  • If a dialog opens requesting to save changes click Yes. Two extensions have been added as well as three dependencies.

Run the runtime workbench again. From the main menu select Window –> Show View. When the Show View dialog opens select the Other folder. The Custom Plug-in Navigator is found there. If you select it, the navigator will open in the current perspective. If you want to see something displayed in this navigator you can create a general project and create an empty file. Close the navigator before you exit the runtime workbench.

Time to create a custom perspective.

1. In the Extensions tab:

  • Add the perspectives extension
  • Click Add
  • Type “pers” (no quotes) and select the org.eclipse.ui.perspectives extension (do not choose any available templates)
  • Click Finish.
  • Enter the following:
    • id: customplugin.perspective
    • name: Custom Plug-in Perspective
    • class: customplugin.perspectives.Perspective
  • Save the plugin.xml
  • Click the class link and click Finish in the New Java Class wizard. Ignore the generated code (or better yet, close the Java editor)
  • Add the perspectiveExtensions extension
    • Click Add
    • Type “pers” (no quotes) and select the org.eclipse.ui.perspectiveExtensions extension
    • Click Finish.
  • Select the “* (perspectiveExtension)
  • targetID: customplugin.perspective
  • Save the file
  • Right click on “customplugin.perspective (perspectiveExtension)” and select New –> View
    • id: customplugin.navigator
    • relationship: left
    • relative: org.eclipse.ui.editorss
    • ratio: 0.25

Open the runtime workbench. Select Window –> Open Perspective –> Other –> Custom Plug-in Perspective. The perspective should have the custom navigator open and over to the left.

Cool, isn’t it? Open the Resource perspective before exiting the runtime workbench. When we add behavior Eclipse will automatically open our new Custom perspective.

This was not that hard; that is the point of of the Eclipse plug-in architecture. The problem is that Eclipse is large and non-trivial and more than half the battle is knowing what is already available to get things done.

Next time: adding testable behavior to the plug-in to create a custom project.

Writing an Eclipse Plug-in (Part 1)- What I’m going to do

July 8, 2009 6 comments

(The next few posts are a cheat. I have been slowly amassing these blogs for the last few weeks/months.)

So you want to write an Eclipse plug-in and don’t know where to start? Join the crowd. I wrote a great book on Eclipse (well, at least I liked it) and still find myself at a loss when it comes to implementing a standard feature set of functionality within a plug-in (sample chapters here).

I am going to document (read: heavily edit) what I did to implement a plug-in I am building to relearn all the things I wrote about in my book, but never really got a chance to use in this ever changing economy.

This blog will act as my requirements document as well as my FAQ. This way, when I forget how I did something I can just come here.

Where to Start?

Let’s say I have a software tool that is in desperate need of an IDE to help automate a number of silly tasks. That means a few things:

  • I want to aggregate the tasks
  • I want to aggregate any files involved in configuring the tool
  • I want to aggregate any source code involved in extending the tool

In addition, I want to use Mylyn to help me keep track of my tasks.

The list of things that can be done in Eclipse can be rather daunting. At a high-level this plug-in needs to accomplish a stardard list of things:

  • Create a project
  • Create some custom format XML files
  • Manage files within the project
  • Edit some XML files using a form-based approach rather than just editing the XML directly
  • Deploy files to a target directory

When it comes to adding functionality the best place to start is with use cases. Here are the ones I used:

  • Create a custom project
  • Edit a custom XML file
  • Deploy the files
  • Monitor how the deployed files have affected

The actor for all of the use cases will be a developer.

Create a custom project

The steps to create the custom project will be:

  1. Open the New Wizard dialog.
  2. Open the Custom Project folder
  3. Select the Custom Project item
  4. Press Next
  5. Enter the name of the project and a location in which to put it
  6. Press Finish
  7. A custom perspective will open

The custom perspective that will only display projects of my type. The project will contain custom categories where files will be displayed. The physical location of the files and the displayed location of the files in the navigator will be different. For example, the files may be located in:

root/
  folder1/
    filetype1-1.xml
  folder2/
  folder3/
    filetype2-1.xml
    filetype3-1.xml

The custom navigator will display the files in an almost flat structure:
My Project
  Category 1
    fileType1-1.xml
  Category 2
    fileType2-1.xml
  Category 3
    fileType3-1.xml

If the user wants to see the actual folder structure they can look at the project in the Navigator view.

The custom perspective will display:

  • A custom navigator
  • The Navigator view
  • The Outline view
  • A log file view
  • Some yet-to-be-announced views
  • Editors as appropriate

The custom navigator will display individual images next to the workspace, project, categories and various file types that are displayed.

The custom navigator will allow the standard Eclipse behaviors from a popup:

  • Cut/copy/paste
  • Rename
  • Refresh
  • Workspaces
  • New
  • Project
  • Other

Edit a Custom XML file

There are a few XML files that need to be edited in a consistent and reliable way. The user will interact with a custom editor that will let them modify the XML file through a form or as XML text. The editor will have multiple form pages and one text page for direct XML editing.

Deploy the files

Once the files are in place, copy the files to the proper locations in the target software.

Right-click on the workspace or project will deploy the entire project.
Right-click on a particular category will deploy only the contents of that category
Right-click on a particular file will deploy just that file

In Addition

The above is all well and good, but the plug-in should also adhere to best practices whenever possible and that includes testing, putting strings in separate property files and flagging other strings as ignorable, caching images and disposing them.

What about testing? Testing is certainly interesting in the development of an Eclipse plug-in. The Eclipse Plug-ins book explains how to test a plug-ins, but there are a few things to bear in mind when testing anything:

  • Don’t test the platform
  • Test new functionality
  • Test before fixing a bug

I don’t enjoy having to change the API of a class to support testing, but sometimes there aren’t a lot of ways to test an API without retrofitting Eclipse to support dependency injection.

Therefore I will not be writing tests when creating Wizards or perspectives, but I will be writing tests on the behavior that the various GUI components will execute when they are told to do something. For example, the GUI flow for the use case Create A Custom Project will consist of:

  • Opening the New Wizard dialog
  • Displaying one or more Wizard pages
  • Clicking Finish causes the custom perspective to open
  • Clicking Cancel leaves everything the way it was

The flow, as a first phase of implementation, can work with little or no code. Whatever code needs to be written will just be glue code. Once those pieces come together then behavior can be written:

  • Create a custom project
  • Display the custom project in a custom navigator

Bear in mind that the Eclipse Plug-in Wizard has a number of templates that you can select from to create most of what you need to implement the first phase of a plug-in. The problem is that some of the things I needed were not there so I thought it better to assemble it piece by piece and blog about it.

Sorry for all the talking and not implementing anything. And BTW, I might never finish. Hope that is not a problem.