Archive

Posts Tagged ‘plugin.xml’

Writing an Eclipse Plug-in (Part 24): Common Navigator: Configuring the submenus (Presentation…again)

August 29, 2010 9 comments

When we last left our erstwhile travelers (that would be all of you) they were surfing the quantum wave on their way to a future refactoring of their past to allow them to simultaneously feel proud of their work and embarrassed that they were following some guy who is making it all up as he goes along causing them to need refactoring in the first place.

Hey. Refactoring happens. All the time. Just ask evolution (only don’t mention the alligators).

What I will post about is adding behavior to our menu items. After having added 4 (count ’em) new menu items it is time to make them actually do something. What should they do? Well, they are going to add elements into their respective XML files (and respectively) while looking like they are adding items into their respective category nodes in the navigator.
Read more…

Writing an Eclipse Plug-in (Part 21): Return of the Popup Menu (Displaying Resources)

[In case anyone cares: I have upgraded to Eclipse 3.6 RC1]

Welcome to the second of what will probably be 4 posts on creating popup menus using the Common Navigator Framework.

In the last post we created a two item popup that appears when there are no resources displayed or selected. In this post we will have the popup menu appear even when we right click on a resource.

How (are we doing it?)

Perform the following on the customnavigator plug-in project.

  1. Delete the navigatorContent enablement adapt entries for both CustomNewActionProvider and CustomRefreshActionProvider.
    • plugin.xml –> Extensions
    • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomNewActionProvider –> (enablement) –> (or) –> org.eclipse.core.resources.IResource (adapt) –> Delete
    • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomRefreshActionProvider –> (enablement) –> (or) –> org.eclipse.core.resources.IResource (adapt) –> Delete
  2. Add two enablement instanceof entries for both CustomNewActionProvider and CustomRefreshActionProvider.
    • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomNewActionProvider –> (enablement) –> (or) –> New –> instanceof
      • value: customnavigator.navigator.ICustomProjectElement
    • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomRefreshActionProvider –> (enablement) –> (or) –> New –> instanceof
      • value: customnavigator.navigator.ICustomProjectElement
  3. Start the runtime workbench, create a Custom Project, go to the Custom Perspective and right click on the project. You should see the popup menu.

Why (did we do it that way?)

First, let’s do that simplest thing I can think of: have a popup menu appear with one menu item. Once that is in place the rest are mechanical steps.

In order to have a new popup menu appear Eclipse needs to recognize the resource so that it will show just the menus you want and no others. How do we do that? By setting up an enablement with our custom project type.

Perform the following on the customnavigator plug-in project.

Go to plugin.xml –> Extensions.

Remove the entry for IResource (adapt):

  • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomNewActionProvider –> (enablement) –> (or) –> org.eclipse.core.resources.IResource (adapt) –> Delete

That entry told Eclipse to open the popup when the selected resource was of type IResource. Well the custom project does not include the IResource. We don’t need it for now. What we do need is for the popup to open when any of our custom types is selected. Since all of the custom nodes implement ICustomProjectElement we use that instead.

  • org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomNewActionProvider –> (enablement) –> (or) –> New –> instanceof
    • value: customnavigator.navigator.ICustomProjectElement

Why use instanceof instead of adapt? An adapt entry means that the selected object, in this case CustomProjectParent, can be adapted (converted) into the listed type, originally IResource, Since a CustomProjectParent does not implement IResource anywhere in its inheritance hierarchy the adapt will never work. Adapt means that we would have to change the custom node types while instanceof puts the onus on Eclipse. You know where my vote goes.

Perform the same steps for CustomRefreshActionProvider:

  1. org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomRefreshActionProvider –> (enablement) –> (or) –> org.eclipse.core.resources.IResource (adapt) –> Delete
  2. org.eclipse.ui.navigator.navigatorContent –> customnavigator.popup.actionprovider.CustomRefreshActionProvider –> (enablement) –> (or) –> New –> instanceof
    • value: customnavigator.navigator.ICustomProjectElement
  3. Start the runtime workbench, create a Custom Project, go to the Custom Perspective and right click on the project. You should see the popup menu.

While I was doing this I found that the above did not work. It turned out that my Launch configuration thought I only needed 73 of the existing plug-ins while I needed a few more. It is possible that your Launch Configuration might not have enough dependencies selected. In Eclipse 3.6 RC1 this needed 80 out of 375 plug-ins. The only other plug-in I have installed is EGit.

What Just Happened?

Not a lot just happened. Changing the adapt entry to instanceof specific to our custom project type was enough to get the popup to behave the way we needed. Not a lot of work.

What we need to do next is create New behavior for:

  • New Schema Table
  • New Schema View
  • New Schema Filter
  • New Stored Procedure

That will take commands, handlers and command images.

Our current setup creates new projects, schema files and deployment files. Oops. We’ll have to change that. Next time. Maybe.

After that we’ll add properties pages to each node type fix the New menus in the toolbar and main menu.

The cat is tired. It may be time to do some genetic programming posts.

Code

<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
   <extension
         point="org.eclipse.ui.views">
      <category
            id="customnavigator.category"
            name="%category.name">
      </category>
      <view
            allowMultiple="false"
            category="customnavigator.category"
            class="org.eclipse.ui.navigator.CommonNavigator"
            icon="icons/navigator.png"
            id="customnavigator.navigator"
            name="%view.name">
      </view>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.viewer">
      <viewer
            viewerId="customnavigator.navigator">
         <popupMenu
               id="customnavigator.navigator#PopupMenu">
            <insertionPoint
                  name="group.new">
            </insertionPoint>
            <insertionPoint
                  name="group.build"
                  separator="true">
            </insertionPoint>
         </popupMenu>
      </viewer>
      <viewerContentBinding
            viewerId="customnavigator.navigator">
         <includes>
            <contentExtension
                  pattern="customnavigator.navigatorContent">
            </contentExtension>
         </includes>
      </viewerContentBinding>
      <viewerActionBinding
            viewerId="customnavigator.navigator">
         <includes>
            <actionExtension
                  pattern="customnavigator.popup.actionprovider.CustomNewAction">
            </actionExtension>
            <actionExtension
                  pattern="customnavigator.popup.actionprovider.CustomRefreshAction">
            </actionExtension>
         </includes>
      </viewerActionBinding>
   </extension>
   <extension
         point="org.eclipse.ui.navigator.navigatorContent">
      <navigatorContent
            activeByDefault="true"
            contentProvider="customnavigator.navigator.ContentProvider"
            id="customnavigator.navigatorContent"
            labelProvider="customnavigator.navigator.LabelProvider"
            name="%navigatorContent.name">
         <triggerPoints>
            <instanceof
                  value="org.eclipse.core.resources.IWorkspaceRoot">
            </instanceof>
         </triggerPoints>
         <commonSorter
               class="customnavigator.sorter.SchemaCategorySorter"
               id="customnavigator.sorter.schemacategorysorter">
            <parentExpression>
               <or>
                  <instanceof
                        value="customnavigator.navigator.CustomProjectSchema">
                  </instanceof>
               </or>
            </parentExpression>
         </commonSorter>
      </navigatorContent>
      <actionProvider
            class="customnavigator.popup.actionprovider.CustomNewActionProvider"
            id="customnavigator.popup.actionprovider.CustomNewAction">
         <enablement>
            <or>
               <instanceof
                     value="customnavigator.navigator.ICustomProjectElement">
               </instanceof>
               <adapt
                     type="java.util.Collection">
                  <count
                        value="0">
                  </count>
               </adapt>
            </or>
         </enablement>
      </actionProvider>
      <actionProvider
            class="customnavigator.popup.actionprovider.CustomRefreshActionProvider"
            id="customnavigator.popup.actionprovider.CustomRefreshAction">
         <enablement>
            <or>
               <instanceof
                     value="customnavigator.navigator.ICustomProjectElement">
               </instanceof>
               <adapt
                     type="java.util.Collection">
                  <count
                        value="0">
                  </count>
               </adapt>
            </or>
         </enablement>
      </actionProvider>
      <commonWizard
            type="new"
            wizardId="customplugin.wizard.new.custom">
         <enablement></enablement>
      </commonWizard>
   </extension>

</plugin>

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 9): Custom Project: Defining a Custom File Type

October 25, 2009 Leave a comment

So things have been going swimmingly for the last 8 parts. Time to return to our roots: the original project.

But first: time to floss. Open customplugin MANIFEST.MF and click on the light bulb to fix the warning. It should now display:

MANIFEST.MF
...
Export-Package: customplugin,
 customplugin.natures,
 customplugin.perspectives,
 customplugin.projects,
 customplugin.wizards

Also, return to the Overview tab and click on the Externalize Strings Wizard. That should take care of externalizing 4 strings from the MANIFEST.MF and plugin.xml files. Click Finish.

There. My teeth feel so much better.

Okay, now on to the serious stuff.

The custom project will use 3 files types (for now):

  1. Schema definition
  2. Deployment descriptor
  3. Java source code

The two descriptor files will be XML files. They are what we need to define.

The current tasks are to:

  • Define a custom project file type for the Schema Definition.
  • Define a custom project file type for the Deployment Definition.

There are two things we need to know before defining the file type:

  • how can we determine the type of a file?
  • can we leverage existing Eclipse functionality?

The butler did it. Metaphorically anyway. There is no need to wait until the end of the story to be told what the answer is: Eclipse can associate the content type of an XML file by looking inside it and reading the root element.

By configuring the content type extension with a base-type of org.eclipse.core.runtime.xml and selecting a parser of type org.eclipse.core.runtime.content.XMLRootElementContentDescriber2 we can create an extensive list of content types that are all XML files, but have different file formats. Very convenient. Very cool. That’s two verys.

Define a Custom Project File Type for the Schema Definition.

First:

  1. Open customplugin –> plugin.xml –> Extensions –> Add –> contenttypes
  2. Select org.eclipse.core.contenttype.contentTypes
  3. Click Finish
  4. Enter:
    • ID: customplugin.contenttype
  5. Select org.eclipse.core.contenttype.contentTypes –> new –> content-type
  6. Enter:
    • id: customplugin.contenttype.schema
    • name: Hidden Clause Schema Definition
    • base-type: org.eclipse.core.runtime.xml
    • file-extensions: xml
    • priority: normal
  7. Select Schema Definition –> new –> describer
  8. Enter:
    • class: org.eclipse.core.runtime.content.XMLRootElementContentDescriber2
  9. Select org.eclipse.core.runtime.content.XMLRootElementContentDescriber2 –> new –> parameter
  10. Enter:
    • name: element
    • value: hc-schema

Start the runtime workbench and open Window –> Preferences –> General –> Content Types –> Text –> XML. Hidden Clause Schema Definition should be listed as an XML content type.

customplugin-part-9-preferences-with-new-contenttype

From the Resource Perspective create a project, custom or otherwise. Create a file and name it special-schema.xml. Enter the following into the file:

<hc-schema>
  <tables>
  </tables>
</hc-schema>

Right click on the file and select Properties –> Resource. The file should be listed as Hidden Clause Schema Definition (the name we gave it above).

customplugin-part-9-properties-file-hc-schema-def

Define a Custom Project File Type for the Deployment Definition.

Time for the second file type:

  1. Select org.eclipse.core.contenttype.contentTypes –> new –> content-type
  2. Enter:
    • id: customplugin.contenttype.deployment
    • name: Hidden Clause Deployment Definition
    • base-type: org.eclipse.core.runtime.xml
    • file-extensions: xml
    • priority: normal
  3. Select Schema Definition –> new –> describer
  4. Enter:
    • class: org.eclipse.core.runtime.content.XMLRootElementContentDescriber2
  5. Select org.eclipse.core.runtime.content.XMLRootElementContentDescriber2 –> new –> parameter
  6. Enter:
    • name: element
    • value: hc-deployment

Start the runtime workbench and open Window –> Preferences –> General –> Content Types –> Text –> XML. Hidden Clause Deployment Definition should be listed as an XML content type.

customplugin-part-9-preferences-with-contenttype-deployment

From the Resource Perspective, using the same project as before create another file and name it special-dep.xml. Enter the following into the file:

<hc-deployment>
  <tables>
  </tables>
</hc-deployment>

Right click on the file and select Properties –> Resource. The file should be listed as Hidden Clause Deployment Definition.

customplugin-part-9-properties-file-hc-deploy-def

Take a bow.

Cleaning Up

Return to the Overview tab of plugin.xml and click on the Externalize Strings Wizard. Oh, look: the two custom file type names are there.

  • Assign the key content-type.name.schema to Hidden Clause Schema Definition
  • Assign the key content-type.name.deployment to Hidden Clause Deployment Definition
  • Click Finish

I think we might have missed a spot behind the ears, but they feel dry so I think we are okay.

What Just Happened?

Well, adding two new content types was pretty straightforward. Some would say that we should have done this directly in XML. Others think we should learn latin in school. Others think we should go back to the oceans.

Personally, I prefer to use editors to control configuration files; it keeps me from doing something complicated like…forgetting a closing brace, or misspelling an element name.

References

The Eclipse help files were somewhat useful, but not as useful as just trying it out. This is one of the few times where stumbling around in the room is actually useful.

Of course the Eclipse Tip: Define Custom Content Types to Identify Your Data Files was rather helpful in pointing out the use of org.eclipse.core.runtime.content.XMLRootElementContentDescriber2 as an existing content type XML parser that also supports namespaces. Very cool. Very useful.

The cat is alive.

Writing an Eclipse Plug-in (Part 8): Common Navigator: Adding a New Sorter Under navigatorContent

October 24, 2009 Leave a comment

[Just an FYI: I updated Part 7‘s code to reflect the suggestions from Simon Zambrovski so the code is slightly different. The zip file was also updated.]

[I have upgraded to Eclipse 3.6 M2. Nothing should need to be changed.]

Today’s tasks:

  • Remove the warning from plugin.xml
  • Add a customSorter to the navigator
  • Refactor the CustomProjectSchema* classes
  • Implement the SchemaCategorySorter
  • Remove the warning from plugin.xml

Remove the Warning from plugin.xml

This is code hygiene.

  1. Open plugin.xml
  2. Click on the light bulb, select Add Missing Packages
  3. Save plugin.xml
    1. The manifest editor will add a new package to Export-Package:

      MANIFEST.MF
      ...
      Export-Package: customnavigator,
       customnavigator.navigator
      

      Pat yourself on the back.

      Add a customSorter to the Navigator

      A funny thing happened when I took a good look at the categories listed under Custom Project: the Schema categories were not in the right order. I wanted them to appear as:

      • Tables
      • Views
      • Filters

      Instead they appear as:

      • Filters
      • Tables
      • Views

      In other words, they are appearing in alphabetical order instead of the arbitrary order I have determined is the correct one for my custom project. How to fix such a stain on humanity? Add a custom sorter.

      Do the following in the customnavigator plugin.xml:

      1. Open org.eclipse.ui.navigator.navigatorContent
      2. Right-click on Custom Navigator Content and select New –> commonSorter. Enter:
        • class: customnavigator.sorter.SchemaCategorySorter
        • id: customnavigator.sorter.schemacategorysorter
      3. Click on the class link and, when the New Java Class wizard opens, click Finish.
      4. Right click customnavigator.sorter.schemacategorysorter (commonSorter) and select New –> parentExpression
      5. Right click parentExpression and select New –> or
      6. Right click or and select New –> instanceof. Since we want to sort the categories under the Schema category we set the class to trigger on to be the CustomProjectSchema. Enter:
        • value: customnavigator.navigator.CustomProjectSchema
      7. Save plugin.xml

      Refactor the CustomProjectSchema* classes

      Before we implement the SchemaCategorySorter let’s do some upfront work. Refactor the constant NAME into the 3 category class CustomProjectSchemaTables, CustomProjectSchemaViews, CustomProjectSchemaFilters.

      public class CustomProjectSchemaTables implements ICustomProjectElement {
      
          public static final String NAME = "Tables"; //$NON-NLS-1$
          ...
          public String getText() {
              return NAME;
          }
          ...
      }
      
      public class CustomProjectSchemaViews implements ICustomProjectElement {
      
          public static final String NAME = "Views"; //$NON-NLS-1$
          ...
          public String getText() {
              return NAME;
          }
          ...
      }
      
      public class CustomProjectSchemaFilters implements ICustomProjectElement {
      
          public static final String NAME = "Filters"; //$NON-NLS-1$
          ...
          public String getText() {
              return NAME;
          }
          ...
      }
      

      I know, I know…there is a lot of great refactoring in these classes. Patience. All will be revealed (in another post).

      Implement the SchemaCategorySorter

      The sorter is straightforward.

      The SchemaCategorySorter works just like a Comparable object: it is expecting either a 1, 0 or -1, or more generically, a positive number, a zero or a negative number depending on how you want to sort the incoming items. We inherit from ViewerSorter as that is the superclass the New Java Class wizard selected as the parent class. Who are we to argue?

      We override compare() to make Tables first and Filters last:

      /**
       * Coder beware: this code is not warranted to do anything.
       *
       * Copyright Oct 24, 2009 Carlos Valcarcel Hidden Clause
       */
      package customnavigator.sorter;
      
      import java.text.Collator;
      
      import org.eclipse.jface.viewers.Viewer;
      import org.eclipse.jface.viewers.ViewerSorter;
      
      import customnavigator.navigator.CustomProjectSchemaFilters;
      import customnavigator.navigator.CustomProjectSchemaTables;
      import customnavigator.navigator.CustomProjectSchemaViews;
      import customnavigator.navigator.ICustomProjectElement;
      
      /**
       * @author carlos
       *
       */
      public class SchemaCategorySorter extends ViewerSorter {
      
          /**
           * 
           */
          public SchemaCategorySorter() {
              // purposely empty
          }
      
          /**
           * @param collator
           */
          public SchemaCategorySorter(Collator collator) {
              super(collator);
          }
      
          @Override
          public int compare(Viewer viewer, Object e1, Object e2) {
              String catName1 = ((ICustomProjectElement)e1).getText();
              String catName2 = ((ICustomProjectElement)e2).getText();
              
              int result = -1;
              if (catName1.equals(CustomProjectSchemaTables.NAME)) {
                  result = -1;
              } else if (catName2.equals(CustomProjectSchemaTables.NAME)) {
                  result = 1;
              } else if (catName1.equals(CustomProjectSchemaViews.NAME)) {
                  result = -1;
              } else if (catName1.equals(CustomProjectSchemaFilters.NAME)) {
                  result = 1;
              } // else result == -1
              
              return result;
          }
      
      }
      

      Remove the warning from plugin.xml

      In the plugin.xml editor go to the MANIFEST.ML tab. There is a warning icon in the left hand margin of the file. Click on the icon; it will open a window with a single suggestion for fixing the warning: Add Missing Packages. Double click Add Missing Packages. The manifest editor will add a new package to Export-Package:

      MANIFEST.MF
      ...
      Export-Package: customnavigator,
       customnavigator.navigator,
       customnavigator.sorter
      

      Go ahead. Start the runtime workbench and take a look.
      custom-navigator-end-of-part-8

      So What Just Happened?

      After correcting the missing package export (which we should have done in the previous post) we:

      • Added a new commonSorter to the navigatorContent extension
      • Created and implemented the SchemaCategorySorter
      • Minimally refactored the CustomProjectSchema* classes so their names would be available in constants
      • Exported the customnavigator.sorter in MANIFEST.MF

      The cat is alive and resting comfortably.

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….

My Eclipse Plug-in Stopped Working After I Copied It Onto My Linux Box!

February 17, 2009 Leave a comment

I ran into an interesting problem the other day: I was copying my Eclipse workspace from my Windows Vista box to a Linux environment and the plug-in stopped working (when I say not working I mean that I double-clicked on plugin.xml and the Plug-in Development Environment treated it like an empty plug-in: no name., no id, no dependencies…nothing). I have been working on and off on a plug-in, keeping copious notes as I want to blog about the various things I have been doing for future blogs, when I found that the plugin.xml file appeared to be disconnected from the plug-in project itself.

Rather than write my usual wordy post here is the solution: when the plug-in was created/copied on Windows the META-INF/MANIFEST.MF files somehow ended up in lower case. When I copied the workspace to the Linux box the Eclipse plug-in development environment did not know what to make of the new lower case names and so did what any self-respecting IDE would do: Eclipse ignored it.

Rename the folder and file to upper case: META-INF/MANIFEST.MF. The project will come back to life. Life goes on.