Archive
Writing an Eclipse Plug-in (Part 23): Common Navigator: Rewriting History
[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…
Writing an Eclipse Plug-in (Part 15): Custom Project: Customizing the Perspective Menus (Main menu)
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:
- Ctrl+N
- Main menu: File –> New
- Shift+Alt+N – opens the popup menu; select New
- Right click on a Project and select New
- Right click on a Folder and select New
- Toolbar: New button
- 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)
- New
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).
- Open up plugin.xml for customplugin
- Go to Extensions–> Add –> perspectiveExtension and click Finish (yes, you could skip this step and use the existing perspectiveExtension entry)
- Change
- targetID: *
to
- targetID: customplugin.perspective
- Right click on customplugin.perspective (perspectiveExtension) –> new –> newWizardShortcut
- 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)
- Perform steps 4 and 5 for the Schema File (wizard) and Deployment File (wizard)
- 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.
Writing an Eclipse Plug-in (Part 13): Common Navigator: Adding Tests
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
- Create a plug-in test project for the navigator
- Enter the following:
- Project name: customnavigator.test
- Eclipse version: 3.5
- Click Next
- Enter the following:
- Version: 1.0.1.3 [Actually anything you want]
- Name: Custom Navigator Test
- Click Finish
- Enter the following:
- Clean up MANIFEST.MF
- Click the MANIFEST.MF tab
- Move the cursor to line 1 and Press Ctrl+1
- Select Add Missing Packages
- Move the cursor to line 3 and Press Ctrl+1
- Select Externalize the Bundle-Name header
- Save the file
- Dependencies tab: Add org.junit4
- Dependencies tab: Add org.eclipse.core.resources
- 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
- Open the customnavigator.test Properties dialog. In the Project References element put a check mark next to the customnavigator project. Click Finish.
- Implement customnavigator.navigator.ContentProviderTest in the customnavigator.test project
- Create a new JUnit class named customnavigator.navigator.ContentProviderTest
- Test getParent()
- Test getChildren()
- 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
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:
- 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]
- Click the class link for CustomProjectNewSchemaFile
- Everything looks good
- Click Finish to close the New Java Class Wizard
[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:
- 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
- wizard.name.0 to wizard.name.schema
- wizard.name.1 to wizard.name.deployment
- the wizard window title to WIZARD_NAME and add $NON-NLS$ using the Quick Fix functionality.
- the WizardSchemaNewFileCreationPage title to PAGE_NAME.
- 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
- Right click in the editor for CustomProjectNewSchemaFile and select Refactor –> Extract Superclass
- Superclass Name: CustomProjectNewFile
- In Types to Extract a Superclass From click Add and add CustomProjectNewDeploymentFile
- In Specify Actions for Members check:
- _pageOne
- _workbench
- _selection
- performFinish()
- init()
- Click Next
- 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.
- Click Finish.
[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:
In the schema file related files refactor:
In WizardSchemaNewFileCreationPage:
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:
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.

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
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:
- Create a new plug-in project, I have named it customplugin.test.
- 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.
- 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.
- 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.
- In the customplugin.test plugin.xml file:
- Runtime –> Classpath: Add the two jar files in the lib folder.
- 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….


