Subscribe to AgileIQ via email

Your email:

Subscribe to our Newsletter

siq newsletter 180

AgileIQ Blog

Current Articles | RSS Feed RSS Feed

Developing Eclipse Plug-ins: Program to Publish

  
  
  

by Tim Myer

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



Programming with Groovy: Trampoline and Memoize

  
  
  

by Tim Myer

Groovy 1.8 introduces two new closure functions: memoization and trampolining. Memoization is the automatic caching of programming with Groovy trampoline memoizeclosure results. Trampolining permits a form of declarative tail-call optimization. This article introduces the two concepts and demonstrates how to combine them in order to create cached, tail-recursive closures.

The example code from this article is available on github.

Simple Memoization

Creating a closure that caches the result of some calculation is as easy as appending .memoize() or one of the alternate memoize...(...) methods that allow for more fine-grained control over cache sizes to a closure declaration. One benefit of memoization includes the caching of results of long-running calculations that have no side effects. Memory leaks are a potential pitfall, which is why a maximum cache size should generally be preferred.

The specification below contains a closure with a side effect. This side effect happens just once, despite the closure being invoked twice.

SimpleMemoizationSpec.groovy

package timezra.groovy.trampoline_memoize

class SimpleMemoizationSpec extends spock.lang.Specification {

  int count

  def identity = {
    count++
    it
  }.memoize()

  def "each call should be cached"() {
    when:
    def first = identity 0
    def second = identity 0

    then:
    count == 1
    first == second
    second == 0
  }
}

Recursive Memoization

Suppose we want to memoize the results of a recursive closure call. For example, we can unroll the call tree of this naive implementation of the Fibonacci function.

  def fib = { n ->
    if(n == 0) 0
    else if(n == 1) 1
    else fib(n-1) + fib(n-2)
  }

The call trace for the fourth Fibonacci number looks like this.

                       ___________fib 4___________
                      /                           \
               fib 3 + fib 2                 fib 2 + fib 1
             /               \                 /
      fib 2 + fib 1     fib 2 + fib 1     fib 1 + fib 0
       /
fib 1 + fib 0

NB: The closure here is entered 9 times, but we can see that it only needs to be entered 5 times because 4 calls are repeated. If we cache the results of the Fibonacci calls, the complexity of even a naive implementation such as this will increase linearly, rather than exponentially.

Unfortunately, because in Groovy 1.8 a closure cannot invoke another closure directly, memoizing this closure is not entirely straightforward. The example below does not work.

RecursiveMemoizationSpec.groovy
package timezra.groovy.trampoline_memoize

class RecursiveMemoizationSpec extends spock.lang.Specification {

  int count

  def fib = { n ->
    count++
    if(n == 0) 0
    else if(n == 1) 1
    else fib(n-1) + fib(n-2)
  }.memoize()

  def "calls should be cached"() {
    when:
    def actual = fib 10

    then:
    actual == 55
    count == 11
  }
}

The stack trace when a closure calls itself.

groovy.lang.MissingMethodException: No signature of method: org.codehaus.groovy.runtime.memoize.Memoize$MemoizeFunction.doCall() is applicable for argument types: (java.lang.Integer) values: [9]
Possible solutions: call(), call([Ljava.lang.Object;), call(java.lang.Object), call([Ljava.lang.Object;), findAll(), equals(java.lang.Object)
  at timezra.groovy.trampoline_memoize.RecursiveMemoizationSpec.$spock_initializeFields_closure1(RecursiveMemoizationSpec.groovy:11)
  at groovy.lang.Closure.call(Closure.java:410)
  at groovy.lang.Closure.call(Closure.java:423)
  at timezra.groovy.trampoline_memoize.RecursiveMemoizationSpec.calls should be cached(RecursiveMemoizationSpec.groovy:16)

Since methods can invoke memoized closures, the solution is to invoke the call method on the closure.

RecursiveMemoizationSpec.groovy
class RecursiveMemoizationSpec extends spock.lang.Specification {
....
  def fib = { n ->
    count++
    if(n == 0) 0
    else if(n == 1) 1
    else fib.call(n-1) + fib.call(n-2)
  }.memoize()
....
}


NB: The un-memoized version enters the closure 177 times, but the memoized version enters just 11.

Trampoline

Declarative tail-call optimization is as simple as adding .trampoline() to a closure declaration and ensuring that recursive calls to the closure invoke the trampoline method on the closure instead of the closure itself. Some problems are more clearly solved with recursive solutions, but without automatic tail-call optimization in the JVM, recursion can quickly lead to an explosion in the size of the call stack. Trampolining is one work-around for this design tradeoff (or defect).

A tail-recursive fibonacci closure.
  def fib = { n, a = 0, b = 1 ->
    if(n == 0) a
    else fib n - 1, b, a + b
  }

By tracing the call stack, we can see its linear growth without memoization.
fib 4
  |
fib 3, 1, 1
  |
fib 2, 1, 2
  |
fib 1, 2, 3
  |
fib 0, 3, 5


In order to avoid a java.lang.StackOverflowError for sufficiently large inputs, the tail-recursive closure must be explicitly trampolined.

TrampolineSpec.groovy
package timezra.groovy.trampoline_memoize

class TrampolineSpec extends spock.lang.Specification {

  int count

  def fib = { n, a = 0, b = 1 ->
    count++
    if(n == 0) a
    else fib.trampoline n - 1, b, a + b
  }.trampoline()

  def "tail calls chould be optimized"() {
    when:
    def actual = fib 1000

    then:
    actual == 1556111435
    count == 1001
  }
}


In this example, the trampolined closure is called 1001 times. If a method in Java were to call itself 1001 times, the stack would be overwhelmed. By trampolining, Groovy turns this recursion into iteration.

Memoizing a Trampolined Closure

Suppose we want to cache the results of a computationally expensive tail-recursive closure with no side effects. A trampolined closure that calls itself can easily be memoized in a separate closure declaration.

OneTimeTrampolineMemoizationSpec.groovy
package timezra.groovy.trampoline_memoize

class OneTimeTrampolineMemoizationSpec extends spock.lang.Specification {

  int count

  def fib_aux = { n, a = 0, b = 1 ->
    count++
    if(n == 0) a
    else fib_aux.trampoline n - 1, b, a + b
  }.trampoline()

  def fib = fib_aux.memoize()

  def "top-level trampolined calls should be cached"() {
    when:
    def first = fib 1000
    def second = fib 1000

    then:
    count == 1001
    first == second
    second == 1556111435
  }
}


NB: This solution caches the top-level trampolined closure, not the results of the intermediate calls.

Trampolining a Memoized Closure

Suppose we want to cache the intermediate results of a trampolined function call. For example, in our trace above, suppose we want to cache the results of fib 4 and fib 3, 1, 1 and fib 2, 1, 2 and fib 1, 2, 3 and fib 0, 3, 5. Again, this situation is not straightforward because no closure can call a memoized closure directly, so in this case, the trampolined closure cannot call a memoized version of itself directly. Again, we can use the call function on the memoized closure.

FullTrampolineMemoizationSpec.groovy
package timezra.groovy.trampoline_memoize

class FullTrampolineMemoizationSpec extends spock.lang.Specification {

  int count

  def fib_aux = { n, a, b ->
    count++
    if(n == 0) a
    else fib.trampoline n - 1, b, a + b
  }.memoize()

  def fib = { n, a = 0, b = 1 ->
    fib_aux.call n, a, b
  }.trampoline()

  def "all trampolined calls should be cached"() {
    when:
    def first = fib 1000
    def second = fib 500, 315178285, -1898383934

    then:
    count == 1001
    first == second
    second == 1556111435
  }
}

Conclusion

Trampolining and memoization are two powerful new features in Groovy 1.8, but the combination of the two is not always straightforward. This tutorial has presented strategies for combining the two and for working around some of the limitations of their combination.

 

Application Development: JPA Callbacks With Hibernate

  
  
  

by Tim Myer

Application Development with HibernateGoal:

The purpose of this blog entry is to provide one solution for using JPA Annotations with Hibernate but without using the Hibernate EntityManager. Thanks to Ben Macfarlane for posing this problem and for doing the groundwork for me to come up with a somewhat simple solution.

tl;dr

If you want to look right away at working code that compares the configuration of JPA callbacks with the EntityManager and the Hibernate SessionFactory, a sample project based on this tutorial is available.

Create a Maven Project:

We will start with a boilerplate Maven project that includes dependencies on Spring, Hibernate and JUnit.

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>timezra.hibernate</groupId>
  <artifactId>callbacks</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <description>Demo for using JPA callbacks with hibernate</description>
  <properties>
    <maven.compiler.source>1.6</maven.compiler.source>
    <maven.compiler.target>1.6</maven.compiler.target>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>3.0.6.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>cglib</groupId>
      <artifactId>cglib-nodep</artifactId>
      <version>2.2.2</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>3.6.7.Final</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-annotations</artifactId>
      <version>3.5.6-Final</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-validator</artifactId>
      <version>3.1.0.GA</version>
    </dependency>
    <dependency>
      <groupId>org.javassist</groupId>
      <artifactId>javassist</artifactId>
      <version>3.15.0-GA</version>
    </dependency>
    <dependency>
      <groupId>com.h2database</groupId>
      <artifactId>h2</artifactId>
      <version>1.3.160</version>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.4</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.10</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>3.0.6.RELEASE</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

Create a Domain and Domain Access:

Our domain can be as simple as a single POJO that uses javax.persistence annotations such as @PrePersist, @PreUpdate and @PostLoad.

src/main/java/timezra/hibernate/callbacks/domain/Author.java

package timezra.hibernate.callbacks.domain;

import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.PostLoad;
import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.UniqueConstraint;
import org.hibernate.validator.NotNull;

@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = Author.NAME_ATTRIBUTE))
public class Author {

  
public static final String NAME_ATTRIBUTE = "name";

  @Id
  @GeneratedValue(strategy = GenerationType.SEQUENCE)
  
private Long id;
  @NotNull
  
private String name;
  
private Date dateOfBirth;
  @Transient
  
private Integer age;
  @NotNull
  
private Date dateCreated;
  @NotNull
  
private Date lastUpdated;

  @PrePersist
  
void prePersist() {
    dateCreated = lastUpdated = 
new Date();
  }

  @PreUpdate
  
void preUpdate() {
    lastUpdated = 
new Date();
  }

  @PostLoad
  
void postLoad() {
    
if (dateOfBirth != null) {
      
final Calendar now = Calendar.getInstance(Locale.getDefault());
      
final int thisYear = now.get(Calendar.YEAR);
      
final int thisDay = now.get(Calendar.DAY_OF_YEAR);
      now.setTime(dateOfBirth);
      
final int birthYear = now.get(Calendar.YEAR);
      
final int birthDay = now.get(Calendar.DAY_OF_YEAR);
      age = thisYear - birthYear - (birthDay > thisDay ? 1 : 0);
    }
  }

  
public Long getId() {
    
return id;
  }

  
public Date getDateCreated() {
    
return dateCreated;
  }

  
public Date getLastUpdated() {
    
return lastUpdated;
  }

  
public Integer getAge() {
    
return age;
  }

  
public Date getDateOfBirth() {
    
return dateOfBirth;
  }

  
public void setDateOfBirth(final Date dateOfBirth) {
    
this.dateOfBirth = dateOfBirth;
  }

  
public String getName() {
    
return name;
  }

  
public void setName(final String name) {
    
this.name = name;
  }
}

A DAO and Service can provide access into our domain.

src/main/java/timezra/hibernate/callbacks/dao/AuthorDAO.java

package timezra.hibernate.callbacks.dao;

import javax.annotation.Resource;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Restrictions;
import org.springframework.stereotype.Repository;
import timezra.hibernate.callbacks.domain.Author;

@Repository
public class AuthorDAO {

  @Resource
  
private SessionFactory sessionFactory;

  
public void create(final Author a) {
    getSession().save(a);
  }

  
public Author findByName(final String name) {
    
return (Author) getSession().createCriteria(Author.class//
        .add(Restrictions.eq(Author.NAME_ATTRIBUTE, name)) //
        .uniqueResult();
  }

  
public void update(final Author a) {
    getSession().update(a);
  }

  
public void delete(final Author a) {
    getSession().delete(a);
  }

  
private Session getSession() {
    
return sessionFactory.getCurrentSession();
  }
}

src/main/java/timezra/hibernate/callbacks/service/AuthorService.java

package timezra.hibernate.callbacks.service;

import javax.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import timezra.hibernate.callbacks.dao.AuthorDAO;
import timezra.hibernate.callbacks.domain.Author;

@Service
public class AuthorService {

  @Resource
  
private AuthorDAO authorDAO;

  @Transactional
  
public void create(final Author a) {
    authorDAO.create(a);
  }

  @Transactional
  
public void update(final Author a) {
    authorDAO.update(a);
  }

  @Transactional
  
public void delete(final Author a) {
    authorDAO.delete(a);
  }

  @Transactional(propagation = Propagation.SUPPORTS)
  
public Author findByName(final String name) {
    
return authorDAO.findByName(name);
  }
}

Wire It Together With Spring:

We will use Spring to glue all our components together and to manage their lifecycles.

src/main/resources/applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
  xmlns:tx="http://www.springframework.org/schema/tx"
  xsi:schemaLocation="
    http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
    http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd"
>

  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="org.h2.Driver" />
    <property name="url" value="jdbc:h2:test" />
    <property name="username" value="sa" />
    <property name="password" value="" />
  </bean>

  <bean id="sessionFactory"
    class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
    <property name="dataSource" ref="dataSource" />
    <property name="packagesToScan">
      <list>
        <value>timezra.hibernate.callbacks.domain</value>
      </list>
    </property>
    <property name="hibernateProperties">
      <props>
        <prop key="hibernate.dialect">org.hibernate.dialect.H2Dialect</prop>
        <prop key="hibernate.hbm2ddl.auto">update</prop>
      </props>
    </property>
  </bean>

  <bean id="transactionManager"
    class="org.springframework.orm.hibernate3.HibernateTransactionManager">
    <property name="sessionFactory" ref="sessionFactory" />
  </bean>

  <context:component-scan base-package="timezra.hibernate.callbacks" />
  <context:annotation-config />
  <tx:annotation-driven />
</beans>

Test The JPA Callbacks:

Finally we can test that the annotated methods are called at the expected times during Hibernate lifecycle events.

src/test/java/timezra/hibernate/callbacks/domain/AuthorTest.java

package timezra.hibernate.callbacks.domain;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import javax.annotation.Resource;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import timezra.hibernate.callbacks.service.AuthorService;

@RunWith(SpringJUnit4ClassRunner.
class)
@ContextConfiguration(
"/applicationContext.xml")
public class AuthorTest {

  @Resource
  
private AuthorService authorService;

  
private Author testAuthor;

  @Before
  
public void setup() {
    testAuthor = 
new Author();
    testAuthor.setName(
"timezra");
    authorService.create(testAuthor);
  }

  @After
  
public void tearDown() {
    authorService.delete(testAuthor);
  }

  @Test
  
public void theCreationDateIsSetAutomatically() {
    assertNotNull(testAuthor.getDateCreated());
  }

  @Test
  
public void theUpdatedDateIsSetAutomaticallyOnCreation() {
    assertEquals(testAuthor.getDateCreated(), testAuthor.getLastUpdated());
  }

  @Test
  
public void theUpdatedDateIsSetAutomaticallyOnUpdate() {
    testAuthor.setDateOfBirth(
new Date());

    authorService.update(testAuthor);

    assertTrue(testAuthor.getDateCreated().before(testAuthor.getLastUpdated()));
  }

  @Test
  
public void theAgeIsSetAutomaticallyWhenTheAuthorIsLoaded() {
    
final Integer expectedAge = 42;
    
final Calendar birthDate = Calendar.getInstance(Locale.getDefault());
    birthDate.add(Calendar.YEAR, -expectedAge);
    testAuthor.setDateOfBirth(birthDate.getTime());
    authorService.update(testAuthor);

    assertNull(testAuthor.getAge());
    assertEquals(expectedAge, authorService.findByName(testAuthor.getName()).getAge());
  }
}

We should expect all these tests to fail with a Hibernate validation Exception.

JUnit Test Failures for JPA Annotations

Update Project Dependencies:

Fortunately, the hibernate-entitymanager component provides a set of JPA lifecycle listeners that can be used independent of the Hibernate EntityManager, so we will update our project dependencies.

pom.xml

<project ....>
  ....
  <dependencies>
    ....
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>3.6.7.Final</version>
    </dependency>
    ....
  </dependencies>
</project>

NB: for this sample code, the version must be 3.6.x+ in order to access the ReflectionManager from the Hibernate configuration.

Register Hibernate Event Listeners:

We must manually tie the JPA lifecycle listeners to Hibernate events, much as the org.hibernate.ejb.EventListenerConfigurator#configure would if the Hibernate EntityManager were used.

src/main/java/timezra/hibernate/callbacks/domain/EntityCallbackHandlerInitializer.java

package timezra.hibernate.callbacks.dao;

import java.util.Iterator;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import org.hibernate.annotations.common.reflection.ReflectionManager;
import org.hibernate.cfg.Configuration;
import org.hibernate.ejb.event.EntityCallbackHandler;
import org.hibernate.mapping.PersistentClass;
import org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean;
import org.springframework.stereotype.Component;

@Component
public class EntityCallbackHandlerInitializer {

  @Resource
  
private AnnotationSessionFactoryBean annotationSessionFactory;

  @Resource
  
private EntityCallbackHandler entityCallbackHandler;

  @PostConstruct
  
public void init() throws ClassNotFoundException {
    
final Configuration configuration = annotationSessionFactory.getConfiguration();
    
final ReflectionManager reflectionManager = configuration.getReflectionManager();
    
final Iterator<PersistentClass> classMappings = configuration.getClassMappings();
    
while (classMappings.hasNext()) {
      entityCallbackHandler.add(
          reflectionManager.classForName(classMappings.next().getClassName(), 
this.getClass()),
          reflectionManager);
    }
  }
}

Finally, we will update the Hibernate configuration in the Spring context to use these event listeners. This configuration is based off a similar solution on the Spring Forum.

src/main/resources/applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans ....>
  ....
  <bean id="sessionFactory" ....>
    ....
    <property name="eventListeners">
      <map>
        <entry key="save" value-ref="saveEventListener" />
        <entry key="flush-entity" value-ref="flushEntityEventListener" />
        <entry key="post-load" value-ref="postLoadEventListener" />
      </map>
    </property>
  </bean>

  <bean id="saveEventListener" parent="callbackHandlerEventListener"
    class="org.hibernate.ejb.event.EJB3SaveEventListener" />
    
  <bean id="flushEntityEventListener" parent="callbackHandlerEventListener"
    class="org.hibernate.ejb.event.EJB3FlushEntityEventListener" />
    
  <bean id="postLoadEventListener" parent="callbackHandlerEventListener"
    class="org.hibernate.ejb.event.EJB3PostLoadEventListener" />

  <bean id="entityCallbackHandler" class="org.hibernate.ejb.event.EntityCallbackHandler" />

  <bean id="callbackHandlerEventListener" abstract="true"
    class="org.hibernate.ejb.event.CallbackHandlerConsumer">
    <property name="callbackHandler" ref="entityCallbackHandler" />
  </bean>
  ....
</beans>

NB: This configuration is for a subset of javax.persistence annotations and should be considered a starting point. It is up to the reader to configure Hibernate to use other other listeners in the org.hibernate.ejb.event package if the enablement of more JPA annotations is desired. For the purpose of this tutorial, the three listeners above are sufficient.

The test cases now succeed.

Conclusion:

The purpose of this tutorial has been to demonstrate that, with minimal code and configuration additional to what is necessary for a standard Maven/Spring/Hibernate project, Hibernate can detect JPA annotations and tie them to its persistence lifecycle events. Along the way, we have also developed a simple methodology for testing that JPA annotated methods are called when expected, and this methodology can be used to extend the sample code to cover the remaining javax.persistence annotations not specified in this tutorial.

Sample code based on this tutorial is also available.

Fostering Collaboration in a Multicultural Scrum Team

  
  
  

by Valerie Morris

I was talking with a colleague who is a ScrumMaster for an international team. All of the team members are co-locate in the same physical locale and are working together as a Scrum team. So, what do I mean by international team? Often, we use the term international to insinuate geography — I am using the term to represent cultural and diversity about this particular Scrum team, called Mad Dog. There are 9 members on the team representing Russian, Japanese, Chinese, Indian, Scottish, and American cultures. This diversity creates a rich opportunity for team members to form relationships outside of their primary language and customs.

agile acceleratorIn our conversation, the ScrumMaster told me about a situation some of the team members were experiencing with team conversations. Here is the scenario he presented to me:

“Recently I had a delivery team member (now a former delivery team member) complain that other team members were speaking a foreign language in the team room and that it was an impediment to him. I didn’t give it too much thought because I thought it was a onetime occurrence. Now I’m seeing it very often. What is your perspective on how I can address this with the team?”

In my response to him and anyone else struggling with this same thing, I recommend bringing this topic up in a conversation about the team’s working agreement. We are comfortable with our learned primary language and customs. Being on the outside of one or both if these may cause people to feel left out or insecure. This is an area to build trust and acceptance of a multinational team. As a ScrumMaster, you must facilitate a discussion with the team about “what a team is”. Remind people about the team values of openness, courage, and respect. The diversity of a team can be embraced through all of these values. The team will come to an agreement on this topic if you help them find their voices.


Every Building Needs a Solid Foundation

  
  
  

by Kendrick Burson

What Tools are Necessary to Any Successful Agile Endeavor, and Why
  • What is the path to better software faster?
  • What is the way of harmony between developers, managers, and sales?
  • What is the means to achieving faster time to market?
  • How can we achieve all of this with Quality as our top priority?

Of the many buzzwords thrown around the industry, all trying to satisfy this list of needs, the one you hear the most is Agile. Oh, there are other important buzzwords out there, like Scrum and XP, but many of these fall under the umbrella of Agile Software Development Practices.

So, you have decided to take the plunge into the world of Agile Software Development. There are many challenges ahead before your scrum team is able to churn out software at a wicked pace. One of the many challenges to any organization adopting agile is building a strong foundation of tools to support your agile development efforts.

Would you consider entering a dark cave for the first time without a few tools? A flashlight comes to mind, maybe some rope… in any endeavor there are certain tools that improve the outcome.

agile solid foundationThe foundation of any agile development effort is the Release Management System. Yes, I know, one of the latest buzzwords that everybody is talking about is ALM, the Application/Agile Lifecycle Management system. ALM is a collection of tools and processes that move software from idea through development and finally to the customer. Release Management is the foundation that ALM is built on top of and thus is a subset of ALM. Continuous Integration, another popular buzzword, is a subset of Release Management.

Any Release Management System stands on 3 main legs;

  • SCM, or Source Code Management system
  • Build Automation
  • Continuous Integration

These three layers are stacked, one on top of the other. Yes, you can have build automation scripts and tools without an SCM, but without it your build automation tools become weak and lifeless. They are merely a utility of convenience. A CI server cannot operate without a build automation system. These three layers combine in harmony as the Release Management Stack.

Now, don’t get me wrong, Release Management is more than just the stack of tools you collect and configure to build, test, and deploy your product. Release Management is also a set of practices on how to use and leverage these tools to achieve the highest quality product in the shortest amount of time with complete traceability and control of what is released to the customer and when.

For now, I want to concentrate on just the stack, the tools, the fun stuff. Later, we’ll talk about the practices around these tools and how to use them effectively.

In the this series of blog entries I want to discuss each of the layers of the stack, what they are, what tools are available, both commercial and open source, and some questions to ask yourself when selecting which tools to include in your stack.

When my family or friends ask me to help them buy a computer, for most people the choice is obvious. For some, they want a Mac, with it’s cool UI and slick industrial engineering. For others, a computer means an Intel-based CPU running Microsoft Windows, and for the truly geeky there is only one true operating system and that is *nix (Unix/Linux).

The reasons we choose one platform over another is hyped in opinion and mystery as much as any religious war… to be true and to choose well ya just gotta have faith – “follow the Gourd!”, “no, the Shoe, follow the Shoe!”… how do you pick?

The first thing I ask is “how are you going to use it”, “what do you want to do with it?”. These same questions are valid when selecting the tools you want to include in your RM Stack.

So, in my next entry we will talk about Source Control Management systems, also referred to as Software Configuration Management. After that we will talk about Build Automation systems, frameworks and scripting languages. And finally we will tie it together with an entry on Continuous Integration systems, which are really just cron on steroids (if you don’t count the auto triggers).

A Large part of the value of CI is in the automated testing, source code analysis and metrics… that is such a large topic, we might need a separate series on that but we will try to shoe-horn it in.

So, please follow me on this journey down the rabbit hole, and you don’t even have to pick the red pill or the blue.

All Posts