Archive

Archive for July 26, 2009

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

July 26, 2009 51 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() > 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 && 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….