Eclipse Search


Oct 31, 2008

A glimpse at the Faceted Project Framework

If you haven't noticed, the Faceted Project Framework from WTP is now proposed as a separate project. I like the framework very much, probably because my very first Eclipse Plug-in is simply a WTP Facet :-) When I wrote that plug-in, there was no much documentation available, but later Konstantin wrote a nice article about it. It might get into platform for the e4 timeframe, so lets see what's in the box.
Before 'what' and 'how' the question of 'why'. Stealing Konstantin's thoughts on this question from the newsgroup:
Natures are extremely simple. The original use case for natures is to basically serve as a flag modeling something akin to a project type. The good UI that you speak of for adding and removing natures would need a lot more information and callable behaviors than is provided by the natures API. The limitations make sense since the natures API was written with simple use cases in mind and natures were not intended to be fully-described and self-supporting.

Let's look at one API as an example. The IProjectNature interface has configure() and deconfigure() methods that nature authors get to implement. These methods get called when a nature is added to or removed from a project. Notice how those methods take no parameters. That means it is not possible to pass in a configuration object that would let the user have input as these actions take place. So what, you say. We've solved that problem before. We will just create IProjectNature2 with variants of those two methods capable of taking parameters. Problem solved. Not quite. The issue is that there are many natures already implemented in various Eclipse projects and commercial products. For years nature authors have been working around this API limitation. The most common work-around is to move most of the interesting code outside these two methods. In the case of the configure() method all of this code is embedded in various project creation wizards and "Enable function X" menu actions. The configure() implementations themselves typically are not sufficient to properly configure the project and often rely on stuff happening prior to the nature getting added. For the most part, the deconfigure() methods are implemented as no-ops. Since there is no UI for the user to remove a nature, why bother with implementing this method?

In short, the biggest obstacle to making it possible for users to add and remove natures are the existing nature implementations. Practically every nature out there would be broken behaviorally if UI for adding and removing natures was introduced.

Ok. So we need a more gradual phased-in approach that doesn't give everyone in the Eclipse Community a massive coronary. Enter facets. We started from scratch and designed the facets API together with UI for adding and removing them. The framework evolved over the years to cover a lot of different use cases that adopters brought to the table. Since making a project faceted is controlled by a flag (a nature, actually), the project wizard owner get to opt-in when they are ready. Part of the opt-in process is gathering all of the code that strewn about in various wizards and actions and writing a facet based on that code. It often make sense to retain any existing natures for backwards compatibility, but facets would now control adding and removing them. The pattern of wrapping a facet around a nature is particularly important when you consider the case of an Eclipse Project re-using another Project's functionality. We need a solution for enabling a downstream Project to transition to facets while the upstream Project is not ready yet and continues using natures. The original example of this is WTP's re-use of JDT's java nature for various WTP projects like Dynamic Web Project. It was necessary for WTP to move forward with creating faceted projects without depending on JDT adopting the same approach and making requisite changes to it's code base. The facets architecture allowed us in WTP to implement a java facet on our own that wraps JDT's java nature. JDT's Java Project (as well as all the other projects that re-use java nature) continue to work as before while WTP users start benefiting from the richer experience provided by facets. If JDT ever decided to adopt facets, the implementation of the java facet can move to JDT. For now, it will live in the JDT Enablement component of this Project. In short, the approach taken is to enable gradual and controlled adoption without asking everyone in the community to jump at the same time. :)

Ok. So how do we create a Facet?
Facets are declared thru the extension point: org.eclipse.wst.common.project.facet.core.facets. You can categorize a facet; a facet is identified by its id + version; and it can depend on other facets. Each version of the facet can be associated with its actions like install or uninstall or version change. Say if you want to install Java facet, typically the install action would add the Java nature, Java builder, set up the classpath. The install action for a Plugin Facet would probably involve adding the & plugin.xml files, adding the required libraries etc. So pretty much all the activities can be handled by the actions. If you want to get the plug-in id, name, etc from the user, you can extend the wizardPages extension and contribute to the wizard. All these are nicely explained in the article, so you have to refer there for details. Remember, the facets doesn't depend on any other component of WTP, so all you need is org.eclipse.wst.common.project.facet.core & org.eclipse.wst.common.project.facet.ui in your dependencies !
For this tip, I've created two facets and one preset:
Java Facet:
   1: <project-facet id="java.facet">
   2:    <default-version version="1.0"/>
   3:    <label>Java Facet</label>
   4: </project-facet>
   6: <project-facet-version facet="java.facet" version="1.0">
   7:    <action type="install">
   8:       <delegate class="com.eclipse_tips.facets.InstallJavaDelegate"/>
   9:    </action>
  10: </project-facet-version>
Plugin Facet:

   1: <project-facet id="plugin.facet">
   2:    <default-version version="1.0"/>
   3:    <label>Plug-in Facet</label>
   4: </project-facet>
   5: <project-facet-version facet="plugin.facet" version="1.0">
   6:    <action type="install">
   7:       <delegate class="com.eclipse_tips.facets.InstallPluginDelegate"/>
   8:    </action>
   9: </project-facet-version>

   1: <static-preset id="com.eclipse-tips.facets.pluginProject">
   2:    <description>
   3:       This will create a project with both java and plugin facet
   4:    </description>
   5:    <facet id="java.facet" version="1.0" />
   6:    <facet id="plugin.facet" version="1.0" />
   7:    <label>
   8:       Default PDE Project
   9:    </label>
  10: </static-preset>
To create a Faceted Project, you can use the New Faceted Project Wizard:


 You can choose from the available list of facets and their versions. If you choose an invalid combination, (say both Plug-in Facet and a Feature Facet), you should get an error (assuming the facets declare the conflict in the extension point. The preset will be available in the Configurations, so that you can do a one click selection of the facets you would need.

In case you wish to change the facets, the same wizard UI is available in the Project->Properties->Project Facets. So you can change your Plug-in project into a Feature project or add C++ capabilities to your Java project easily. All the necessary files will be added and classpath will be set for you.

How to programmatically create a Faceted Project?

If you want to manipulate the facets on a project in the code, the ProjectFacetsManager is the key. You can create IFacetedProject thru ProjectFacetsManager.create(*) methods and obtain a working copy. You can do all the operations like add/remove a facet, change the version of the facet etc, and then finally you can do a commit on the working copy to persist the changes. Yup, you can revert the changes as well, if you didn't want to persist them. Here is the sample code, which takes a normal project; converts to a Faceted Project; adds the Java Facet; and commits the changes:

   1: IFacetedProject facetedProject = ProjectFacetsManager.create(project, true, null);
   2: IFacetedProjectWorkingCopy workingCopy = facetedProject.createWorkingCopy();
   4: IProjectFacet javaFacet = ProjectFacetsManager.getProjectFacet("java.facet");
   5: IProjectFacetVersion defaultJavaFacet = javaFacet.getDefaultVersion();
   6: workingCopy.addProjectFacet(defaultJavaFacet);
   7: workingCopy.commitChanges(null);
With the ProjectFacetsManager, you can pretty much do everything, except for defining new facets. If you really need that, probably you should raise a bug. Might get addressed for e4 :-)

No comments:

Post a Comment