Showing posts with label eclipse. Show all posts
Showing posts with label eclipse. Show all posts

Sunday, April 27, 2014

Contributing to the Papyrus Editor Palette

Augmenting the Papyrus Palette

Goal

This entry is the first in a series of articles that details the customizations to the Eclipse Papyrus platform for the UML Testing Tool. These customizations, though specific to the needs of the UML Testing Tool community, can be generalized and used for other products with similar requirements.

The UML Testing Tool is a graphical workbench for the design, visualization, specification, analysis, construction, and documentation of the artifacts involved in testing. The language for modeling these artifacts is based on a UML Testing Profile. This profile defines a set of UML stereotypes that extend a subset of UML elements. There are Similar tools based on other UML profile specifications, such as the NIEM UML Profile.

Papyrus is an environment within Eclipse that enables users to configure graphical editors for UML-based DSLs.

The goal of this first entry is to describe how to register a UML profile with Papyrus and to augment the Papyrus editor palette with stereotyped elements defined in this profile.

tl;dr

The source code for the set of Eclipse plug-ins that contains the UML Testing Profile, its registrations with the Eclipse UML and Papyrus extensions and the test cases described in this post is located here. Feel free to use, explore, contribute to or fork this code for your own needs. A snapshot of the code specific to these examples at the time of writing is here.

Registering the UML Profile

Let's assume that we already have all the boilerplate configuration for an Eclipse plug-in, test fragment, feature and p2-repository in place. For this plug-in, we can use the tycho_new_plugin_project archetype, initially described here.

We will start with an acceptance test to verify that we have correctly registered the UML Testing Profile as a UML dynamic package.

PackageRegistrationTest.java
PackageRegistrationTest.java
package timezra.uml.testing.resources;

import static org.eclipse.uml2.uml.UMLPlugin.getEPackageNsURIToProfileLocationMap;
import static org.eclipse.uml2.uml.resources.util.UMLResourcesUtil.init;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertThat;

import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.impl.ResourceSetImpl;
import org.eclipse.uml2.uml.Profile;
import org.junit.Test;

public class PackageRegistrationTest {
    @Test
    
public void should_register_the_uml_testing_package() {
        
final EObject utp = init(new ResourceSetImpl()).getEObject(
                getEPackageNsURIToProfileLocationMap().get(
"http://www.omg.org/spec/UTP/20120801/utp_1.2"), true);

        assertThat(utp, instanceOf(Profile.
class));
    }
}

In order to satisfy this test, we must include the utp_1.2.xmi and utptypes_1.2.xmi with our plug-in. We will modify the utp_1.2.xmi by adding the xmlns declaration xmlns:cmof="http://www.omg.org/spec/MOF/20110701/cmof.xmi" to the root header. We also need to define the profile through the Eclipse UML Editor.

With the UTP and its supporting type descriptions packaged into a plug-in, we can now register an extension for the UML dynamic package for the profile we just defined through the Eclipse UML Editor.

plugin.xml
plugin.xml
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>
    <extension point="org.eclipse.emf.ecore.uri_mapping">
        <mapping source="pathmap://UTP/"
            target="platform:/plugin/timezra.uml.testing.resources/src/main/resources/uml/">
        </mapping>
    </extension>
    <extension point="org.eclipse.emf.ecore.uri_mapping">
        <mapping source="http://www.omg.org/spec/UTP/20120801/utp_1.2.xmi"
            target="pathmap://UTP/utp_1.2.xmi">
        </mapping>
        <mapping source="http://www.omg.org/spec/UTP/20120801/utptypes_1.2.xmi"
            target="pathmap://UTP/utptypes_1.2.xmi">
        </mapping>
    </extension>
    <extension point="org.eclipse.uml2.uml.dynamic_package">
        <profile location="pathmap://UTP/utp_1.2.xmi#_iCl4wOVMEeG84fBOY39c0g"
            uri="http://www.omg.org/spec/UTP/20120801/utp_1.2">
        </profile>
    </extension>
</plugin>

Now, if we install our feature into Eclipse and create a new UML model, the UTP will be available for profile application through the UML Editor. UTP Registered With UML

Profile Registration With Papyrus

In order to register this profile with Papyrus, we can use similar steps: create a test to verify when the behavior we are trying to expose works and expose the profile location through an extension.

ProfileRegistrationTest.java
ProfileRegistrationTest.java
package timezra.uml.testing.papyrus.palettes;

import static org.eclipse.papyrus.uml.extensionpoints.profile.RegisteredProfile.getRegisteredProfile;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;

import org.junit.Test;

public class ProfileRegistrationTest {
    @Test
    
public void should_register_the_uml_testing_profile() {
        assertThat(getRegisteredProfile(
"UTP"), notNullValue());
    }
}

plugin.xml
plugin.xml
....
   <extension
         point="org.eclipse.papyrus.uml.extensionpoints.UMLProfile">
      <profile
            description="UML Testing Profile"
            iconpath="icons/full/obj16/utp.png"
            name="UTP"
            path="http://www.omg.org/spec/UTP/20120801/utp_1.2.xmi#_iCl4wOVMEeG84fBOY39c0g"
            provider="omg.org">
      </profile>
   </extension>
   <extension
         point="org.eclipse.papyrus.uml.extensionpoints.UMLLibrary">
      <library
            description="UML Testing Profile Types"
            iconpath="icons/full/obj16/utptypes.png"
            name="UTP Types"
            path="http://www.omg.org/spec/UTP/20120801/utptypes_1.2.xmi"
            provider="omg.org">
      </library>
   </extension>
....

The UTP is now available in the Papyrus repository, and it can be applied from the Profile tab of the Properties View. UTP Registered With Papyrus

Palette Customization

Once we are able to apply the UTP to a Model, we can specify tests with UML elements extended by this profile. Suppose we begin by creating a TestContext from the Papyrus palette.

ClassDiagramPaletteTest.java
ClassDiagramPaletteTest.java
package timezra.uml.testing.papyrus.palettes;

import static org.eclipse.papyrus.uml.diagram.common.util.URIUtil.getFile;
import static org.eclipse.papyrus.uml.tools.model.UmlUtils.getUmlResource;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.notNullValue;
import static org.junit.Assert.assertThat;

import org.eclipse.gef.EditDomain;
import org.eclipse.gef.EditPartViewer;
import org.eclipse.gef.Tool;
import org.eclipse.gef.palette.PaletteContainer;
import org.eclipse.gef.palette.PaletteEntry;
import org.eclipse.gef.palette.ToolEntry;
import org.eclipse.gef.ui.palette.PaletteViewer;
import org.eclipse.gmf.runtime.diagram.ui.editparts.IGraphicalEditPart;
import org.eclipse.gmf.runtime.diagram.ui.parts.IDiagramWorkbenchPart;
import org.eclipse.papyrus.editor.PapyrusMultiDiagramEditor;
import org.eclipse.papyrus.infra.core.editor.IMultiDiagramEditor;
import org.eclipse.papyrus.uml.diagram.clazz.CreateClassDiagramCommand;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.widgets.Event;
import org.eclipse.uml2.uml.Class;
import org.eclipse.uml2.uml.Element;
import org.eclipse.uml2.uml.Package;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.RuleChain;
import org.junit.rules.TestRule;

import timezra.uml.testing.papyrus.palettes.rules.AppliesTheUTP;
import timezra.uml.testing.papyrus.palettes.rules.CreatesAPapyrusModel;
import timezra.uml.testing.papyrus.palettes.rules.CreatesAProject;
import timezra.uml.testing.papyrus.palettes.rules.CreatesAServicesRegistry;
import timezra.uml.testing.papyrus.palettes.rules.OpensEditor;
import timezra.uml.testing.papyrus.palettes.rules.SavesAModel;

public class ClassDiagramPaletteTest {

    
private CreatesAServicesRegistry theServicesRegistry;
    
private CreatesAProject theProject;
    
private CreatesAPapyrusModel thePapyrusModel;
    
private OpensEditor<IMultiDiagramEditor> theEditor;

    @Rule
    
public final TestRule ruleChain = RuleChain
            .outerRule(theServicesRegistry = 
new CreatesAServicesRegistry())
            .around(theProject = 
new CreatesAProject(getClass().getSimpleName()))
            .around(thePapyrusModel = 
new CreatesAPapyrusModel(theServicesRegistry, theProject, "model.di",
                    
new CreateClassDiagramCommand()))
            .around(
new AppliesTheUTP(thePapyrusModel))
            .around(
new SavesAModel(thePapyrusModel))
            .around(theEditor = 
new OpensEditor<IMultiDiagramEditor>(
                    () -> getFile(getUmlResource(thePapyrusModel.get()).getURI()), PapyrusMultiDiagramEditor.EDITOR_ID));

    @Test
    
public void can_create_a_test_context() {
        
final IGraphicalEditPart theActivePart = ((IDiagramWorkbenchPart) theEditor.get().getActiveEditor())
                .getDiagramEditPart();
        
final EditPartViewer theEditPartViewer = theActivePart.getViewer();
        doubleClick(findThePaletteTool(theEditPartViewer, 
"UTP/Test Context (Class)"), theEditPartViewer);

        
final Element theTestContext = ((Package) theActivePart.resolveSemanticElement())
                .getPackagedElement(
"TestContext1");

        assertThat(theTestContext, instanceOf(Class.
class));
        assertThat(theTestContext.getAppliedStereotype(
"utp::TestContext"), notNullValue());
    }

    
private Tool findThePaletteTool(final EditPartViewer theEditPartViewer, final String toolPath) {
        
final EditDomain theDomain = theEditPartViewer.getEditDomain();
        
final PaletteViewer thePaletteViewer = theDomain.getPaletteViewer();
        
final ToolEntry toolEntry = findByLabel(thePaletteViewer.getPaletteRoot(), toolPath);
        thePaletteViewer.setActiveTool(toolEntry);

        
final Tool theTool = toolEntry.createTool();
        theTool.setViewer(theEditPartViewer);
        theTool.setEditDomain(theDomain);

        
return theTool;
    }

    @SuppressWarnings(
"unchecked")
    
private <T extends PaletteEntry> T findByLabel(final PaletteContainer thePaletteContainer, final String theLabel) {
        
final String[] path = theLabel.split("/");
        PaletteEntry nextEntry = thePaletteContainer;
        NEXT_SEGMENT: 
for (final String segment : path) {
            
if (nextEntry instanceof PaletteContainer) {
                
for (final Object o : ((PaletteContainer) nextEntry).getChildren()) {
                    
final PaletteEntry paletteEntry = (PaletteEntry) o;
                    
if (segment.equals(paletteEntry.getLabel())) {
                        nextEntry = paletteEntry;
                        
continue NEXT_SEGMENT;
                    }
                }
                
return null;
            } 
else {
                
return null;
            }
        }
        
return (T) nextEntry;
    }

    
private void doubleClick(final Tool theTool, final EditPartViewer theEditPartViewer) {
        
final Event theEvent = new Event();
        theEvent.widget = theEditPartViewer.getControl();
        
final MouseEvent mouseEvent = new MouseEvent(theEvent);
        mouseEvent.button = 1;
        theTool.mouseDoubleClick(mouseEvent, theEditPartViewer);
    }
}

Now we will describe a palette entry for the Class Diagram Editor that is in a UTP drawer and that will apply the utp::TestContext stereotype to a UML class.

UTP.clazz.palette.xml
UTP.clazz.palette.xml
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<paletteDefinition>
    <content>
        <drawer iconpath="/icons/drawer.gif" id="UTP_Drawer" name="UTP">
            <aspectTool description="Create a Test Context (Class)"
                iconpath="platform:/plugin/timezra.uml.testing.papyrus.palettes/icons/full/obj16/test_context_class.png"
                id="clazz.tool.class_UTP_TEST_CONTEXT" name="Test Context (Class)" refToolId="clazz.tool.class">
                <postAction id="org.eclipse.papyrus.applystereotypeactionprovider">
                    <stereotypesToApply>
                        <stereotype stereotypeName="utp::TestContext" />
                    </stereotypesToApply>
                </postAction>
            </aspectTool>
        </drawer>
    </content>
</paletteDefinition>

Finally, we will register this palette configuration with a Papyrus extension.

plugin.xml
Copy of plugin.xml
....
<extension point="org.eclipse.papyrus.uml.diagram.common.paletteDefinition">
    <paletteDefinition ID="timezra.uml.testing.papyrus.palettes.UTP_clazz"
        class="org.eclipse.papyrus.uml.diagram.common.service.PluginPaletteProvider"
        icon="icons/full/obj16/utp_palette.png" name="UTP Class Diagram Elements"
        path="palettes/UTP.clazz.palette.xml">
        <Priority name="Medium">
        </Priority>
        <editor id="org.eclipse.papyrus.uml.diagram.clazz">
        </editor>
    </paletteDefinition>
</extension>
....

Once we register the palette definition with Papyrus, the test will pass and the TestContext contribution will be visible for UML Models that have the UML Testing Profile applied. TestContext palette contribution

Conclusion

In this article, we expose the following behaviors from the Eclipse workbench: the ability to apply the UML Testing Profile within the UML and Papyrus Editors and the ability to create UML elements extended by UML stereotypes defined in the UTP directly from a Papyrus editor palette. We describe these behaviors through tests and define them with plug-in extensions and external configuration. The code for these examples is here.

Wednesday, October 19, 2011

Tycho Archetype

Goal


The purpose of this blog post is to demonstrate how to generalize a Maven project that suits a specific need into a re-usable archetype, how to publish this archetype to a Maven repository and how to use it as the starting point for similar projects.

tl;dr


The artifact that is the end result of this tutorial can be used for creating a boilerplate Tycho plug-in project hierarchy with minimal configuration. This generated project will contain a simple Eclipse UI plug-in, an integration-test fragment, a feature that contains the binary plug-in, a feature that contains the source of the plug-in and its test fragment, and an update site for publishing both features.

Generate Archetype From Project


Since we are moving from the specific to the general, we begin by cloning the save-actions-extensions project from github.

  git clone git://github.com/timezra/save-actions-extensions.git


Since we need only a few of the components from the save-actions-extensions project, we can prune out the extra. We must also rename the features, plugins and update-site directories to match the artifactIds in their respective poms along with the module declarations in the root pom. This directory structure follows the Eclipse convention, but archetype generation will be easier if we make sure that the artifactIds and directory names match. We will also rename the individual plugins and features along with their artifactIds so that there is a consistent name prefix for all projects. Again, this convention will help when generating the archetype.

The pruned project
  save-actions-extensions
  .
  |____pom.xml
  |____README
  | timezra.eclipse.save_actions_extensions.features
  | |____pom.xml
  | | timezra.eclipse.save_actions_extensions.feature
  | | |____build.properties
  | | |____feature.properties
  | | |____feature.xml
  | | |____license.html
  | | |____pom.xml
  | | timezra.eclipse.save_actions_extensions.source.feature
  | | |____build.properties
  | | |____feature.properties
  | | |____feature.xml
  | | |____license.html
  | | |____pom.xml
  | timezra.eclipse.save_actions_extensions.plugins
  | |____pom.xml
  | | timezra.eclipse.save_actions_extensions.plugin
  | | |____build.properties
  | | |____plugin.properties
  | | |____pom.xml
  | | | META-INF
  | | | |____MANIFEST.MF
  | | | src
  | | | | main
  | | | | | java
  | | | | | | timezra
  | | | | | | | eclipse
  | | | | | | | | save_actions_extensions
  | | | | | | | | | plugin
  | | | | | | | | | |____Activator.java
  | | timezra.eclipse.save_actions_extensions.plugin.tests
  | | |____build.properties
  | | |____plugin.properties
  | | |____pom.xml
  | | | META-INF
  | | | |____MANIFEST.MF
  | | | src
  | | | | test
  | | | | | java
  | | | | | | timezra
  | | | | | | | eclipse
  | | | | | | | | save_actions_extensions
  | | | | | | | | | plugin
  | | | | | | | | | |____ActivatorTest.java
  | timezra.eclipse.save_actions_extensions.update-site
  | |____index.html
  | |____pom.xml
  | |____site.xml
  | | web
  | | |____site.css
  | | |____site.xsl



We are now ready to generate, install and test the archetype from maven.

NB: even when specifying filtered extensions, not all files (here README, MANIFEST.MF and site.xsl) are turned into velocity templates, and the hard-coded artifactId references in some files are not all replaced with parameters.
  mvn archetype:create-from-project -Darchetype.filteredExtensions=java,html,xsl,properties,xml,MF
  cd target/generated-sources/archetype/
  find ./src -type f \( -name "*.MF" -o -name "*.xsl" \) -print0 |xargs -0 perl -pi -e 's/\$/\${symbol_dollar}/g'
  find ./src -type f \( -name "*.MF" -o -name "*.xsl" \) -exec perl -pi -e "print qq(#set( \\\$symbol_pound = '#' )\n#set( \\\$symbol_dollar = '\\$' )\n#set( \\\$symbol_escape = '\\\' )\n) if $. == 1" {} \;
  find ./src -type f -print0 |xargs -0 perl -pi -e 's/timezra\.eclipse\.save_actions_extensions/\$\{rootArtifactId}/g'
  find ./src -type f -print0 |xargs -0 perl -pi -e 's/\$\{groupId}\.save_actions_extensions/\$\{rootArtifactId}/g'
  find ./src -name archetype-metadata.xml -exec perl -pi -e 'undef $/; $_=<>; s/\s*\n\s*//g; s~README~README~g;s~META-INF~META-INF~g;s~web\*\*/\*\.xsl~web**/*.xslweb~g' {} \;
  mvn clean install
  cd /path/to/tmp/archetype
  mvn archetype:generate -DarchetypeCatalog=local
    # Choose the new archetype (here local -> timezra.eclipse:timezra.eclipse.save_actions_extensions-archetype)
    # Define a groupId (e.g., my.company)
    # Define an artifactId (e.g., my.company.do_something)
    # Define a version (e.g., 1.0.0-SNAPSHOT)
    # Define a package (e.g., my.company.do_something.plugin)
  cd my.company.do_something
  mvn verify



After running this sequence of commands, we should see the eclipse workbench open and successfully run the canary integration test for our test project.

The generated project structure
  my.company.do_something
  .
  |____pom.xml
  |____README
  | my.company.do_something.features
  | |____pom.xml
  | | my.company.do_something.feature
  | | |____build.properties
  | | |____feature.properties
  | | |____feature.xml
  | | |____license.html
  | | |____pom.xml
  | | my.company.do_something.source.feature
  | | |____build.properties
  | | |____feature.properties
  | | |____feature.xml
  | | |____license.html
  | | |____pom.xml
  | my.company.do_something.plugins
  | |____pom.xml
  | | my.company.do_something.plugin
  | | |____build.properties
  | | |____plugin.properties
  | | |____pom.xml
  | | | META-INF
  | | | |____MANIFEST.MF
  | | | src
  | | | | main
  | | | | | java
  | | | | | | my
  | | | | | | | company
  | | | | | | | | do_something
  | | | | | | | | | plugin
  | | | | | | | | | |____Activator.java
  | | my.company.do_something.plugin.tests
  | | |____build.properties
  | | |____plugin.properties
  | | |____pom.xml
  | | | META-INF
  | | | |____MANIFEST.MF
  | | | src
  | | | | test
  | | | | | java
  | | | | | | my
  | | | | | | | company
  | | | | | | | | do_something
  | | | | | | | | | plugin
  | | | | | | | | | |____ActivatorTest.java
  | my.company.do_something.update-site
  | |____index.html
  | |____pom.xml
  | |____site.xml
  | | web
  | | |____site.css
  | | |____site.xsl



Dynamic Directories


On closer comparison of the directory tree from the original project and the generated project, you might wonder how Maven knows to name folders based on the artifactIds.
If we look at the generated archetype, we will see a parameterized directory structure.

Parameterized directories are in red
  archetype
  .
  |____pom.xml
  | src
  | | main
  | | | resources
  | | | | archetype-resources
  | | | | |____pom.xml
  | | | | |____README
  | | | | | __rootArtifactId__.features
  | | | | | |____pom.xml
  | | | | | | __rootArtifactId__.feature
  | | | | | | |____build.properties
  | | | | | | |____feature.properties
  | | | | | | |____feature.xml
  | | | | | | |____license.html
  | | | | | | |____pom.xml
  | | | | | | __rootArtifactId__.source.feature
  | | | | | | |____build.properties
  | | | | | | |____feature.properties
  | | | | | | |____feature.xml
  | | | | | | |____license.html
  | | | | | | |____pom.xml
  | | | | | __rootArtifactId__.plugins
  | | | | | |____pom.xml
  | | | | | | __rootArtifactId__.plugin
  | | | | | | |____build.properties
  | | | | | | |____plugin.properties
  | | | | | | |____pom.xml
  | | | | | | | META-INF
  | | | | | | | |____MANIFEST.MF
  | | | | | | | src
  | | | | | | | | main
  | | | | | | | | | java
  | | | | | | | | | |____Activator.java
  | | | | | | __rootArtifactId__.plugin.tests
  | | | | | | |____build.properties
  | | | | | | |____plugin.properties
  | | | | | | |____pom.xml
  | | | | | | | META-INF
  | | | | | | | |____MANIFEST.MF
  | | | | | | | src
  | | | | | | | | test
  | | | | | | | | | java
  | | | | | | | | | |____ActivatorTest.java
  | | | | | __rootArtifactId__.update-site
  | | | | | |____index.html
  | | | | | |____pom.xml
  | | | | | |____site.xml
  | | | | | | web
  | | | | | | |____site.css
  | | | | | | |____site.xsl
  | | | | META-INF
  | | | | | maven
  | | | | | |____archetype-metadata.xml
  | | test
  | | | resources
  | | | | projects
  | | | | | basic
  | | | | | |____archetype.properties
  | | | | | |____goal.txt



You might also notice that the Activator.java and ActivatorTest.java are contained in the root of their src/main/java and src/test/java folders, respectively, in the archetype resources; however, in the generated project, the files are in the proper subdirectories with corresponding package declarations. In the generated archetype-metadata.xml, the contents of these source directories are filtered and packaged.

src/main/resources/META-INF/maven/archetype-metadata.xml
<archetype-descriptor ....>
  ....
  <modules>
    <module id="${rootArtifactId}.plugins" ....>
      <modules>
        <module id="${rootArtifactId}.plugin" ....>
          <fileSets>
            <fileSet filtered="true" packaged="true" encoding="UTF-8">
              <directory>src/main/java</directory>
              <includes>
                <include>**/*.java</include>
              </includes>
            </fileSet>
            ....
          </fileSets>
        </module>
        <module id="${rootArtifactId}.plugin.tests" ....>
          <fileSets>
            <fileSet filtered="true" packaged="true" encoding="UTF-8">
              <directory>src/test/java</directory>
              <includes>
                <include>**/*.java</include>
              </includes>
            </fileSet>
            ....
          </fileSets>
        </module>
      </modules>
    </module>
    ....
  </modules>
</archetype-descriptor>



Testing


After customizing our resources and adding any additional required properties, we can integration-test the archetype using a golden template project contained in the src/test/projects/basic/reference directory. For this example, if the generated my.company.do_something project above exactly meets our needs, then we can simply copy the entire contents of this directory to the src/test/projects/basic/reference folder and update the archetype.properties to match the properties we used for project generation.

src/test/resources/projects/basic/archetype.properties
groupId=my.company
artifactId=my.company.do_something
version=1.0.0-SNAPSHOT
package=my.company.do_something.plugin



During the integration-test phase, Maven will generate a sample project from the archetype with these properties and will compare the results with the golden template.

  mvn verify


NB: use of this type of golden template can be very brittle, and any changes made to files in the src/main/resources/archetype-resources directory must be reflected by equivalent changes in the src/test/resources/projects/basic/reference directory.

Publish to a Maven Repository


Now that our Maven archetype is tested and ready for publication, we can publish it, either to an internal repository, to the sonatype repository, which will synch our artifact with Maven central, or to a private repository, which in our case can simply be github pages.
We want to partition our repository into snapshots and releases, and we will need archetype catalogs in a subfolder of each.
We can deploy our archetype to a folder that contains the gh-pages branch of a github project called maven, which will contain all our published maven artifacts.


  mvn -DaltDeploymentRepository=snapshot-repo::default::file:/path/to/git_maven_repository/snapshots clean deploy



Since we have installed the archetype, we already have a ~/.m2/archetype-catalog.xml file.
<?xml version="1.0" encoding="UTF-8"?>
<archetype-catalog
  xsi:schemaLocation="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0 http://maven.apache.org/xsd/archetype-catalog-1.0.0.xsd"
  xmlns="http://maven.apache.org/plugins/maven-archetype-plugin/archetype-catalog/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <archetypes>
    <archetype>
      <groupId>timezra.eclipse</groupId>
      <artifactId>timezra.eclipse.save_actions_extensions-archetype</artifactId>
      <version>1.0.0-SNAPSHOT</version>
      <description>Parent project for the timezra.eclipse.save_actions_extensions project set</description>
    </archetype>
  </archetypes>
</archetype-catalog>



The Maven repository
  github_maven_repository
  .
  | releases
  | snapshots
  | | archetypes
  | | |____archetype-catalog.xml
  | | timezra
  | | | eclipse
  | | | | timezra.eclipse.save_actions_extensions-archetype
  | | | | |____maven-metadata.xml
  | | | | |____maven-metadata.xml.md5
  | | | | |____maven-metadata.xml.sha1
  | | | | | 1.0.0-SNAPSHOT
  | | | | | |____maven-metadata.xml
  | | | | | |____maven-metadata.xml.md5
  | | | | | |____maven-metadata.xml.sha1
  | | | | | |____timezra.eclipse.save_actions_extensions-archetype-1.0.0-20111020.005931-1.jar
  | | | | | |____timezra.eclipse.save_actions_extensions-archetype-1.0.0-20111020.005931-1.jar.md5
  | | | | | |____timezra.eclipse.save_actions_extensions-archetype-1.0.0-20111020.005931-1.jar.sha1
  | | | | | |____timezra.eclipse.save_actions_extensions-archetype-1.0.0-20111020.005931-1.pom
  | | | | | |____timezra.eclipse.save_actions_extensions-archetype-1.0.0-20111020.005931-1.pom.md5
  | | | | | |____timezra.eclipse.save_actions_extensions-archetype-1.0.0-20111020.005931-1.pom.sha1



Use the Archetype From the Repository



After our artifacts have been deployed to a Maven repository, we can generate a project from the deployed archetype in a way similar to how we generated the project from the archetype installed locally above.

Generating from an artifact in a gh-pages repository

  mvn archetype:generate -DarchetypeCatalog=http://your_github_username.github.com/maven/snapshots/archetypes



Conclusion


This tutorial is a continuation of a previous post, in which we created a set of Tycho-enabled Eclipse projects. The archetype we created from this set of projects abstracts the boilerplate and conventions into a re-usable component that will make setting up similar projects even faster in the future.
The end result of this effort is the tycho_new_plugin_project.

Wednesday, October 12, 2011

Eclipse Plug-ins: Program to Publish

Goal


The purpose of this blog entry is to demonstrate one workflow for taking an idea for an Eclipse plug-in from concept to product downloadable from the Eclipse marketplace.

tl;dr


If you want to look right away at working code for a simple Eclipse plug-in, feature, update site and their Tycho configurations, a sample project based on this tutorial is available. The plug-in can also be installed in Eclipse through the Marketplace (search for "save actions") or directly from an update site. In addition, the conventions for project setup contained in this tutorial have been extracted into a Maven archetype. Since this blog entry does not provide an in-depth introduction to the nuances of configuring and writing Eclipse plug-ins, having the project source as a reference, either locally or in a web browser, will be helpful for following the examples.

The Idea


Suppose we are starting a new project on an existing codebase and, as part of our Working Agreement, the team has decided to automate certain coding standards with Eclipse Java editor save actions. It would be convenient to bring all the existing code up to our standards before development even begins. Currently, formatting and import organization can be performed in bulk from the source submenu in the workbench. Eclipse users can also apply clean-up conventions from this submenu. Unfortunately, even though the clean-up and save participants have similar configurations, there is a disconnect between the two.
Exposing save actions for the bulk processing of Java/Groovy files through this source submenu is a simple enough feature to add to Eclipse and should provide the opportunity to experience one full cycle of Eclipse plug-in development, from the creation of a simple plug-in and its integration tests, to the addition of a feature to contain the plug-in, to the packaging of an update site, to the distribution of the product through the Eclipse Marketplace.

The Plug-in


We will begin by creating a new plug-in project named timezra.eclipse.apply_save_actions in our Eclipse workspace. Since we will eventually generate a Tycho configuration in order to automate the compilation, testing and packaging of our product, we will modify a few of the default settings for the plug-in. Our plug-in and fragment projects will be contained in a plugins subdirectory, here /path/to/workspace/plugins/timezra.eclipse.apply_save_actions. Our source folder will be src/main/java and output folder will be target/classes to follow the Maven convention.

The New Plug-in Project wizard with Maven-inspired configurations

We can either use the New Plug-in Project Wizard Hello, World Command Template or we can contribute a command to a menu with manual configuration. There are already quite a few resources for contributing commands through an extension point, so you can get more insight into specific configuration settings there.
For this example, the configuration is boilerplate. The internals of the ApplySaveActions command handler are somewhat interesting. There may be a more direct way to invoke the save participant, but this method works on both un-opened files and source buffers modified in memory for Java and Groovy, so in the interest of DTSTTCPW, we can use this simple implementation until it is no longer sufficient.

plugins/timezra.eclipse.apply_save_actions/src/main/java/timezra/eclipse/apply_save_actions/handlers/ApplySaveActions.java
package timezra.eclipse.apply_save_actions.handlers;

import static java.util.Arrays.asList;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IWorkspace;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IAdapterManager;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.IJavaElement;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.IPackageFragment;
import org.eclipse.jdt.core.IPackageFragmentRoot;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.WorkspaceModifyOperation;
import org.eclipse.ui.handlers.HandlerUtil;
import org.eclipse.ui.part.FileEditorInput;
import org.eclipse.ui.texteditor.IDocumentProvider;

public class ApplySaveActions extends AbstractHandler {

  
private final IAdapterManager adapterManager;
  
private final IWorkspace workspace;
  
private final IWorkbench workbench;

  
public ApplySaveActions() {
    
this(Platform.getAdapterManager(), ResourcesPlugin.getWorkspace(), PlatformUI.getWorkbench());
  }

  ApplySaveActions(
final IAdapterManager adapterManager, final IWorkspace workspace, final IWorkbench workbench) {
    
this.adapterManager = adapterManager;
    
this.workspace = workspace;
    
this.workbench = workbench;
  }

  @Override
  
public Object execute(final ExecutionEvent event) throws ExecutionException {
    
final ISelection currentSelection = HandlerUtil.getCurrentSelectionChecked(event);
    
if (currentSelection instanceof IStructuredSelection) {
      
final IStructuredSelection selections = (IStructuredSelection) currentSelection;
      
try {
        applyTo(selections);
      } 
catch (final JavaModelException e) {
        
throw new ExecutionException(Messages.APPLY_SAVE_ACTIONS_UNEXPECTED_ERROR, e);
      } 
catch (final InvocationTargetException e) {
        
throw new ExecutionException(Messages.APPLY_SAVE_ACTIONS_UNEXPECTED_ERROR, e.getTargetException());
      }
    }
    
return null;
  }

  
private void applyTo(final IStructuredSelection selections) throws JavaModelException, InvocationTargetException {
    
for (final Object o : selections.toList()) {
      
final IJavaProject javaProject = getAdapter(o, IJavaProject.class);
      
if (javaProject != null) {
        applyTo(javaProject.getPackageFragments());
        
continue;
      }
      
final IPackageFragmentRoot packageFragmentRoot = getAdapter(o, IPackageFragmentRoot.class);
      
if (packageFragmentRoot != null) {
        applyTo(packageFragmentRoot);
        
continue;
      }
      
final IPackageFragment packageFragment = getAdapter(o, IPackageFragment.class);
      
if (packageFragment != null) {
        applyTo(packageFragment);
        
continue;
      }
      
final ICompilationUnit compilationUnit = getAdapter(o, ICompilationUnit.class);
      
if (compilationUnit != null) {
        applyTo(compilationUnit);
        
continue;
      }
    }
  }

  
private void applyTo(final IPackageFragmentRoot packageFragmentRoot) throws JavaModelException,
      InvocationTargetException {
    
final IJavaElement[] children = packageFragmentRoot.getChildren();
    
final IPackageFragment[] fragments = new IPackageFragment[children.length];
    System.arraycopy(children, 0, fragments, 0, children.length);
    applyTo(fragments);
  }

  
private void applyTo(final IPackageFragment... packageFragments) throws JavaModelException,
      InvocationTargetException {
    
final Collection<ICompilationUnit> compilationUnits = new ArrayList<ICompilationUnit>();
    
for (final IPackageFragment f : packageFragments) {
      compilationUnits.addAll(asList(f.getCompilationUnits()));
    }
    applyTo(compilationUnits.toArray(
new ICompilationUnit[compilationUnits.size()]));
  }

  
private void applyTo(final ICompilationUnit... compilationUnits) throws InvocationTargetException {
    
final IRunnableWithProgress delegate = new ApplySaveActionsOperation(compilationUnits);
    
try {
      workbench.getProgressService().run(
falsetrue, delegate);
    } 
catch (final InterruptedException e) {
      
// cancellation is fine
    }
  }

  @SuppressWarnings(
"restriction")
  
private IDocumentProvider createDocumentProvider() {
    
return new org.eclipse.jdt.internal.ui.javaeditor.CompilationUnitDocumentProvider();
  }

  @SuppressWarnings(
"unchecked")
  
private <T> T getAdapter(final Object o, final Class<T> c) {
    
return (T) adapterManager.getAdapter(o, c);
  }

  
private final class ApplySaveActionsOperation extends WorkspaceModifyOperation {
    
private final ICompilationUnit[] compilationUnits;

    ApplySaveActionsOperation(
final ICompilationUnit... compilationUnits) {
      
this.compilationUnits = compilationUnits;
    }

    @Override
    
public void execute(final IProgressMonitor pm) throws CoreException {
      pm.beginTask(Messages.APPLY_SAVE_ACTIONS_BEGIN_TASK, compilationUnits.length);
      
try {
        
for (final ICompilationUnit unit : compilationUnits) {
          applyTo(workspace.getRoot().getFile(unit.getPath()), pm);
        }
      } 
finally {
        pm.done();
      }
    }

    
void applyTo(final IFile f, final IProgressMonitor pm) throws CoreException {
      report(f.getName(), pm);
      
final IDocumentProvider provider = createDocumentProvider();
      
final FileEditorInput editorInput = new FileEditorInput(f);
      
try {
        provider.connect(editorInput);
        provider.aboutToChange(editorInput);
        provider.saveDocument(pm, editorInput, provider.getDocument(editorInput), 
true);
      } 
finally {
        provider.changed(editorInput);
        provider.disconnect(editorInput);
      }
    }

    
void report(final String task, final IProgressMonitor pm) {
      
if (pm.isCanceled()) {
        
throw new OperationCanceledException();
      }
      pm.setTaskName(task);
      pm.worked(1);
    }
  }
}



The Test Fragment


We will similarly create a new integration test fragment alongside this plug-in, overriding the default configuration to store the fragment into the plugins subdirectory and to use Maven conventions for the source and output directories.

The New Fragment Project wizard with Maven-inspired configurations

There are various approaches for testing Eclipse plug-ins, but the fragment approach has been embraced by the Tycho community, so we will use it here.
Again, the project configuration can be boilerplate for now. Of particular interest is the handler test case.

plugins/timezra.eclipse.apply_save_actions.tests/src/test/java/timezra/eclipse/apply_save_actions/handlers/ApplySaveActions.java
package timezra.eclipse.apply_save_actions.handlers;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.expressions.EvaluationContext;
import org.eclipse.core.resources.IContainer;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IFolder;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.IWorkspaceRoot;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Path;
import org.eclipse.core.runtime.SubProgressMonitor;
import org.eclipse.core.runtime.preferences.InstanceScope;
import org.eclipse.jdt.core.IClasspathEntry;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jdt.launching.JavaRuntime;
import org.eclipse.jdt.ui.JavaUI;
import org.eclipse.jdt.ui.cleanup.CleanUpOptions;
import org.eclipse.jface.text.TextSelection;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.ui.ISources;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.MethodRule;
import timezra.eclipse.apply_save_actions.Constants;
import timezra.eclipse.apply_save_actions.tests.ModifiesSaveActionsPreferences;
import timezra.eclipse.apply_save_actions.tests.ModifiesSaveActionsPreferencesRule;

public class ApplySaveActionsPluginTest {

  
private static final String SOURCE_FOLDER = "src/test/java";

  
private static final String EOL = System.getProperty("line.separator");

  
private static final IProgressMonitor NULL_PROGRESS_MONITOR = new NullProgressMonitor();

  
private static final String TEST_CLASS = "TestClass";
  
private static final String TEST_PACKAGE = "timezra.eclipse.apply_save_actions";

  
private static final String TEST_CLASS_BEFORE_SAVE_ACTIONS = "package " + TEST_PACKAGE
      + 
";import java.util.*;class " + TEST_CLASS + "{private List<" + TEST_CLASS + "> l;" + TEST_CLASS
      + 
"(List<" + TEST_CLASS + "> l){this.l=l;}}";

  
private static final String TEST_CLASS_AFTER_SAVE_ACTIONS = "package " + TEST_PACKAGE + ";" + EOL + //
      EOL + //
      "import java.util.List;" + EOL + //
      EOL + //
      "class " + TEST_CLASS + " {" + EOL + //
      "  private final List<" + TEST_CLASS + "> l;" + EOL + //
      EOL + //
      "  " + TEST_CLASS + "(List<" + TEST_CLASS + "> l) {" + EOL + //
      "    this.l = l;" + EOL + //
      "  }" + EOL + //
      "}";

  @Rule
  
public final MethodRule rule = new ModifiesSaveActionsPreferencesRule();

  
private IProject aJavaProject;
  
private IFolder aJavaPackage;
  
private IFile aJavaFile;

  
private IFolder aJavaSourceFolder;

  @Before
  
public void setUp() throws CoreException {
    aJavaProject = createAJavaProject(
"a_java_project");
    aJavaSourceFolder = createASourceFolder(SOURCE_FOLDER);
    aJavaPackage = createAPackage(aJavaSourceFolder, TEST_PACKAGE.replaceAll(
"\\.""/"));
    aJavaFile = createAJavaFile(aJavaPackage, TEST_CLASS + 
".java");
  }

  @After
  
public void tearDown() throws CoreException {
    aJavaProject.delete(
true, NULL_PROGRESS_MONITOR);
  }

  @Test
  
public void theCurrentSelectionMustBeStructured() throws ExecutionException {
    
final ApplySaveActions command = new ApplySaveActions();
    
final EvaluationContext context = new EvaluationContext(nullnew Object());
    context.addVariable(ISources.ACTIVE_CURRENT_SELECTION_NAME, 
new TextSelection(0, 100));
    
final ExecutionEvent event = new ExecutionEvent(null, Collections.emptyMap(), null, context);
    assertNull(command.execute(event));
  }

  @Test
  @ModifiesSaveActionsPreferences
  
public void aJavaFileCanBeReformatted() throws ExecutionException, CoreException, IOException {
    enableJavaSaveActions();

    applySaveActions(JavaCore.create(aJavaFile));

    verifyThatSaveActionsHaveBeenApplied(aJavaFile);
  }

  @Test
  @ModifiesSaveActionsPreferences
  
public void aJavaPackageCanBeReformatted() throws ExecutionException, CoreException, IOException {
    enableJavaSaveActions();

    applySaveActions(JavaCore.create(aJavaPackage));

    verifyThatSaveActionsHaveBeenApplied(aJavaFile);
  }

  @Test
  @ModifiesSaveActionsPreferences
  
public void aJavaSourceFolderCanBeReformatted() throws ExecutionException, CoreException, IOException {
    enableJavaSaveActions();

    applySaveActions(JavaCore.create(aJavaSourceFolder));

    verifyThatSaveActionsHaveBeenApplied(aJavaFile);
  }

  @Test
  @ModifiesSaveActionsPreferences
  
public void aJavaProjectCanBeReformatted() throws ExecutionException, CoreException {
    enableJavaSaveActions();

    applySaveActions(JavaCore.create(aJavaProject));

    verifyThatSaveActionsHaveBeenApplied(aJavaFile);
  }

  
private void applySaveActions(final Object selection) throws ExecutionException {
    
final ApplySaveActions command = new ApplySaveActions();
    
final EvaluationContext context = new EvaluationContext(nullnew Object());
    context.addVariable(ISources.ACTIVE_CURRENT_SELECTION_NAME, 
new StructuredSelection(selection));
    
final ExecutionEvent event = new ExecutionEvent(null, Collections.emptyMap(), null, context);
    command.execute(event);
  }

  
// contains a beaut that turns a stream into a String without using IoUtils:
  // http://stackoverflow.com/questions/309424/in-java-how-do-a-read-convert-an-inputstream-in-to-a-string
  private void verifyThatSaveActionsHaveBeenApplied(final IFile aJavaFile) throws CoreException {
    
final String actualContents;
    
final Scanner scanner = new Scanner(aJavaFile.getContents());
    
try {
      actualContents = scanner.useDelimiter(
"\\A").next();
    } 
finally {
      scanner.close();
    }
    assertEquals(TEST_CLASS_AFTER_SAVE_ACTIONS, actualContents);
  }

  @SuppressWarnings(
"restriction")
  
private void enableJavaSaveActions() {
    InstanceScope.INSTANCE.getNode(JavaUI.ID_PLUGIN).putBoolean(Constants.PERFORM_SAVE_ACTIONS_PREFERENCE, 
true);

    
final Map<String, String> cleanupPreferences = new HashMap<String, String>(
        org.eclipse.jdt.internal.ui.JavaPlugin
            .getDefault()
            .getCleanUpRegistry()
            .getDefaultOptions(
                org.eclipse.jdt.internal.corext.fix.CleanUpConstants.DEFAULT_SAVE_ACTION_OPTIONS)
            .getMap());

    cleanupPreferences.put(org.eclipse.jdt.internal.corext.fix.CleanUpConstants.FORMAT_SOURCE_CODE,
        CleanUpOptions.TRUE);
    cleanupPreferences.put(org.eclipse.jdt.internal.corext.fix.CleanUpConstants.ORGANIZE_IMPORTS,
        CleanUpOptions.TRUE);
    cleanupPreferences.put(org.eclipse.jdt.internal.corext.fix.CleanUpConstants.CLEANUP_ON_SAVE_ADDITIONAL_OPTIONS,
        CleanUpOptions.TRUE);
    org.eclipse.jdt.internal.corext.fix.CleanUpPreferenceUtil.saveSaveParticipantOptions(InstanceScope.INSTANCE,
        cleanupPreferences);
  }

  
private IFolder createASourceFolder(final String name) throws CoreException {
    
final IFolder aJavaSourceFolder = aJavaProject.getFolder(Path.fromPortableString(name));
    create(aJavaSourceFolder);
    
return aJavaSourceFolder;
  }

  
private IFolder createAPackage(final IFolder aJavaSourceFolder, final String name) throws CoreException {
    
final IFolder aJavaPackage = aJavaSourceFolder.getFolder(Path.fromPortableString(name));
    create(aJavaPackage);
    
return aJavaPackage;
  }

  
private void create(final IFolder folder) throws CoreException {
    
final IContainer parent = folder.getParent();
    
if (parent.getType() == IResource.FOLDER && !parent.exists()) {
      create((IFolder) parent);
    }
    folder.create(
truetrue, NULL_PROGRESS_MONITOR);
  }

  
private IFile createAJavaFile(final IFolder aJavaPackage, final String name) throws CoreException {
    
final IFile aJavaFile = aJavaPackage.getFile(Path.fromPortableString(name));
    aJavaFile.create(
new ByteArrayInputStream(TEST_CLASS_BEFORE_SAVE_ACTIONS.getBytes()), true,
        NULL_PROGRESS_MONITOR);
    
return aJavaFile;
  }

  
// based on http://www.stateofflow.com/journal/66/creating-java-projects-programmatically
  @SuppressWarnings("restriction")
  
private IProject createAJavaProject(final String name) throws CoreException {
    
final IWorkspaceRoot root = ResourcesPlugin.getWorkspace().getRoot();
    
final IProject project = root.getProject(name);
    project.create(NULL_PROGRESS_MONITOR);
    project.open(NULL_PROGRESS_MONITOR);
    org.eclipse.jdt.internal.ui.wizards.buildpaths.BuildPathsBlock.addJavaNature(project, 
new SubProgressMonitor(
        NULL_PROGRESS_MONITOR, 1));
    
final IJavaProject javaProject = JavaCore.create(project);

    
final List<IClasspathEntry> entries = new ArrayList<IClasspathEntry>();
    
for (final IClasspathEntry entry : javaProject.getRawClasspath()) {
      
if (entry.getEntryKind() == IClasspathEntry.CPE_SOURCE) {
        ((org.eclipse.jdt.internal.core.ClasspathEntry) entry).path = Path.fromPortableString(SOURCE_FOLDER);
      }
      entries.add(entry);
    }
    entries.add(JavaRuntime.getDefaultJREContainerEntry());
    javaProject.setRawClasspath(entries.toArray(
new IClasspathEntry[entries.size()]), NULL_PROGRESS_MONITOR);

    
return project;
  }
}



The Feature


Now that we have a tested plug-in, we will create an Eclipse feature to contain it for distribution. We can do practically all our configuration through the New Feature Project Wizard, except we want to put the feature project into a features subdirectory in the same way that we put our plug-in and fragment into the plugins subdirectory.

The New Feature Project wizard

The Update Site


Now that we have a tested plug-in and a feature to contain it, we will create an Eclipse update site for publishing the feature. In the New Update Site Project Wizard, we will again override the default location so that our update site project is in an update-site subdirectory, just as we separated our plug-in, fragment and feature into plugins and features subdirectories.

The New Update Site Project wizard
NB: Whereas we may have multiple features or plug-ins for our project, we will have a single update site; thus the update-site project is not contained in a subfolder of update-site, but in this folder directly.

Tycho


Compiling, running integration tests and packaging an application entirely within an IDE does not scale to even a single programmer over time, let alone a team of programmers working on multiple plug-in projects. So far, the amount of ceremony for creating our menu contribution has been high, but the IDE has reduced a significant amount of the boilerplate. Before Tycho, the amount of ceremony and hackery required to get plugins, features and update sites packaged and unit and integration test suites running on a CI server had been prohibitively high. Tycho removes a significant amount of that pain.
Generating meaningful poms for our projects is as trivial as going into each of the features, plugins and update-site directories and running a Tycho goal.


  mvn org.eclipse.tycho:tycho-pomgenerator-plugin:generate-poms -DgroupId=timezra.eclipse


We can combine some of the boilerplate in each of the subproject poms in an über-parent pom at the root of our workspace. We will also add the Indigo p2 repository and a target platform configuration resolver since we are developing our Eclipse components Manifest-first.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
  xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>timezra.eclipse</groupId>
  <artifactId>apply-save-actions-parent</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>pom</packaging>
  <properties>
    <tycho-version>0.13.0</tycho-version>
  </properties>
  <modules>
    <module>plugins</module>
    <module>features</module>
    <module>update-site</module>
  </modules>
  <build>
    <plugins>
      <plugin>
        <groupId>org.eclipse.tycho</groupId>
        <artifactId>tycho-maven-plugin</artifactId>
        <version>${tycho-version}</version>
        <extensions>true</extensions>
      </plugin>
      <plugin>
        <groupId>org.eclipse.tycho</groupId>
        <artifactId>target-platform-configuration</artifactId>
        <version>${tycho-version}</version>
        <configuration>
          <resolver>p2</resolver>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <repositories>
    <repository>
      <id>indigo</id>
      <layout>p2</layout>
      <url>http://download.eclipse.org/releases/indigo/</url>
    </repository>
  </repositories>
</project>



We will also configure Tycho to use the UI test runner for our integration test suite, as well as add any platform-specific runtime plug-in dependencies or configuration to the test fragment pom.

plugins/timezra.eclipse.apply_save_actions.tests/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"
  xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>timezra.eclipse</groupId>
    <artifactId>apply-save-actions-plugins-parent</artifactId>
    <version>1.0.0-SNAPSHOT</version>
  </parent>
  <groupId>timezra.eclipse</groupId>
  <artifactId>timezra.eclipse.apply_save_actions.tests</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <packaging>eclipse-test-plugin</packaging>
  <!-- Tell tycho to run PDE tests http://git.eclipse.org/c/tycho/org.eclipse.tycho.git/tree/tycho-demo/itp01/tycho.demo.itp01.tests/pom.xml -->
  <build>
    <outputDirectory>target/test-classes</outputDirectory>
    <plugins>
      <plugin>
        <groupId>org.eclipse.tycho</groupId>
        <artifactId>tycho-surefire-plugin</artifactId>
        <version>${tycho-version}</version>
        <configuration>
          <useUIHarness>true</useUIHarness>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <profiles>
    <profile>
      <id>osx</id>
      <activation>
        <property>
          <name>java.vendor.url</name>
          <value>http://www.apple.com/</value>
        </property>
      </activation>
      <build>
        <pluginManagement>
          <plugins>
            <plugin>
              <groupId>org.eclipse.tycho</groupId>
              <artifactId>tycho-surefire-plugin</artifactId>
              <version>${tycho-version}</version>
              <configuration>
                <argLine>-XstartOnFirstThread</argLine>
                <dependencies>
                  <dependency>
                    <type>p2-installable-unit</type>
                    <artifactId>org.eclipse.jdt.launching.macosx</artifactId>
                  </dependency>
                </dependencies>
              </configuration>
            </plugin>
          </plugins>
        </pluginManagement>
      </build>
    </profile>
  </profiles>
</project>


NB: we would use a different configuration if our fragment contained unit-tests instead of integration tests.

JAR signing


If you do not have access to a certificate from a trusted authority, you can generate a self-signed certificate with 1-year validity by a command such as


  keytool -genkey -alias _keystore_alias_ -keystore /path/to/keystore -validity 365


In order to sign the jars deployed to our update-site before release, we will add a new profile with a plug-in management configuration to the über-parent pom.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project ....>
  ....
  <profiles>
    <profile>
      <id>sign</id>
      <!-- To sign plug-ins and features, run: mvn -Psign -Djarsigner.keystore=<path> -Djarsigner.storepass=******* -Djarsigner.alias=<keyalias> clean package integration-test -->
      <build>
        <pluginManagement>
          <plugins>
            <plugin>
              <groupId>org.apache.maven.plugins</groupId>
              <artifactId>maven-jarsigner-plugin</artifactId>
              <version>1.2</version>
              <executions>
                <execution>
                  <goals>
                    <id>sign</id>
                    <goal>sign</goal>
                  </goals>
                </execution>
                <execution>
                  <goals>
                    <id>verify</id>
                    <goal>verify</goal>
                  </goals>
                </execution>
              </executions>
            </plugin>
          </plugins>
        </pluginManagement>
      </build>
    </profile>
  </profiles>
  ....
</project>



Similarly, we will configure the plug-ins and features parent poms for jar signing.

plugins/pom.xml and features/pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project ....>
  ....
  <profiles>
    <profile>
      <id>sign</id>
      <build>
        <plugins>
          <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-jarsigner-plugin</artifactId>
          </plugin>
        </plugins>
      </build>
    </profile>
  </profiles>
</project>



Publishing


We are now ready to build and test our signed plug-ins and features and to package them in an update site for deployment.


  mvn -Psign -Djarsigner.keystore=/path/to/keystore -Djarsigner.storepass=_keystore_password_ -Djarsigner.alias=_keystore_alias_ clean package integration-test


There will now be a fully deployable update site in update-site/target/site.
For this particular project, I distribute the contents of this directory to the gh-pages branch of the project on github.
Since the update site for the project is publicly-available and since I have an Eclipse Bugzilla login, I can simply add a new solution listing to the Eclipse Marketplace to make the menu contribution even more discoverable by and accessible to Eclipse users.

Conclusion


This tutorial has provided the outline for the workflow of taking an idea for a single-featured Eclipse contribution from inception to delivery in a very short amount of time. While Eclipse tooling has for years provided the means to perform the tasks of plug-in, fragment, feature and update site publishing entirely within the IDE, it is the Tycho project that lowers the barrier to entry for scaling out plug-in development by making the build and test process far simpler to automate and to configure than other PDE-based build systems. Along the way, we have explored JAR signing and uploading of content to github and to the Eclipse Marketplace, and we have hopefully developed a menu contribution that others will find useful in their own projects.

The source submenu contribution for applying save actions in batch mode