Saturday, August 29, 2009

Creating tree table in RCP (Eclipse 3.4 - Ganymede)

Introduction:

In this article, I will explain about how to create tree table functionality. Here, I assume that you are familiar with creating trees and tables.

Creating tree table:

First of all, do not use TableTreeViewer and TableTree to create tree table. They are deprecated as of 3.1. Creating tree table is easy if you know how to work with trees and tables. Tree table is nothing but a tree which has columns. This is basically how you will create tree table.

Let's assume that you have a tree which displays your organization hierarchy (tree of employee names) . But now, you want to convert that into a tree table so that you can also display employee age and salary (in separate columns) in the hierarchy.

this.treeViewer = new TreeViewer(parent);

Tree tree = this.treeViewer.getTree();

final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
this.treeViewer.getControl().setLayoutData(gridData);

this.treeViewer.setUseHashlookup(true);

/*** Tree table specific code starts ***/

tree.setHeaderVisible(true);
tree.setLinesVisible(true);

TreeColumn treeColumn = new TreeColumn(tree, SWT.LEFT);
treeColumn.setText("Name");

treeColumn = new TreeColumn(tree, SWT.LEFT);
treeColumn.setText("Age");

treeColumn = new TreeColumn(tree, SWT.LEFT);
treeColumn.setText("Salary");

TableLayout layout = new TableLayout();
int nColumns = 3;
int weight = 100 / nColumns;
for (int i = 0; i < nColumns; i++) {
layout.addColumnData(new ColumnWeightData(weight));
}

tree.setLayout(layout);

/*** Tree table specific code ends ***/

this.treeViewer.setContentProvider(new OrgHierachyContentProvider());
this.treeViewer.setLabelProvider(new OrgHierarchyLabelProvider());
this.treeViewer.setInput(this.employeeOrgHierarchyModel);

If you are familiar with creating tree, you would be familiar with the first few lines (creating tree) and the last few lines (setting content provider and label provider). The middle section (as marked above) deals with the tree table specific code. If you are familiar with creating tables, then you would understand this. Basically, you are saying that header and column lines should be visible in the tree and you are also creating tree columns and setting the layout for the columns.

Tweaking label provider:

Your content provider would be exactly same as you would do for any tree. This is because underlying data is still the same. In this particular case, the content provider provides employee object for every tree node. Only the representation of model data is going to be in tree table form instead of tree. So, once the above code is set up, you need to tweak the label provider so that it provides labels for individual columns in the tree node.

For tree, you would normally implement ILabelProvider and implement getImage and getText (which just had employee name in our case). For tree table, you should implement ITableLabelProvider (just like you would do for tables) and implement getColumnImage and getColumnText. This way for each column we know what employee attribute to return. Below is a snippet of how your label provider would look:

public String getColumnText(final Object element, final int columnIndex) {
if (element instanceof Employee) {
if (columnIndex == 0) {
return (Employee(element)).getName();
} else if (columnIndex == 1) {
return (Employee(element)).getAge();
} else if (columnIndex == 2) {
return (Employee(element)).getSalary();
}
}

return "";
}

Conclusion:

Basically you set up tree table as specified above and your data (content provider) provides tree data i.e bean for every node. Then your label provider takes the bean and provides values for different columns in the tree node. That's all you need to create a tree table. So, if you are familiar with trees and tables, creating tree table is very easy.

Friday, August 28, 2009

Adding dynamic filter to trees in RCP (Eclipse 3.4 - Ganymede)

Introduction:

Dynamic filter provides a search box above the tree and filters elements on the tree as user types it. In this article, I will talk about how to add dynamic filtering capabilities to your tree. I will also explain about how to customize the default filter.

Adding dynamic filter:

Adding dynamic filter to the tree is very easy. Instead of creating treeviewer directly, you need to create FilteredTree (org.eclipse.ui.dialogs.FilteredTree.FilteredTree). The below code creates FilteredTree using PatterFilter.

PatternFilter filter = new PatternFilter();
FilteredTree tree = new FilteredTree(parent, SWT.MULTI | SWT.H_SCROLL | SWT.V_SCROLL, filter);
this.treeViewer = tree.getViewer();

and that's all you need to do to have dynamic filtering in your tree. Isn't that amazing! I know how hard it is to do in Swing.

When your view initially comes up, it would have a text box above your tree. By default it says "type filter text". But you can change that by calling setInitialText(String text) method on the FilteredTree.

Customizing Filter:

If you see above, the constructor for FilteredTree takes PatternFilter. By default, it does word matching and it matches the beginning of every word in your text. In the previous article, we discussed about how to create filters. You would create a similar filter, but which extends PatternFilter (implement just the select method like in any filter) and pass it to FilteredTree.

To customize the pattern in PatternFilter, you could use the void setPattern(String pattern) in PatternFilter. You can set the pattern string using which this filter should select elements in the viewer. This is a public method.

You can also override methods like boolean isElementSelectable(Object element) (Answers whether the given element is a valid selection in the filtered tree), boolean isElementVisible(Viewer viewer, Object element) (Answers whether the given element in the given viewer matches the filter pattern), boolean isParentMatch(Viewer viewer, Object element), boolean isLeafMatch(Viewer viewer, Object element) and boolean wordMatches(String text) to customize the filter.

wordMatches tells if any of the words in the given text satisfy the filter criteria. You could override this method if you want your filter to behave differently. For example, instead of matching every word, you can override this method so that it matches the whole string or just the first word in your string etc.,

Conclusion:

We saw how to create a dynamic filter and attach it to the tree. Also, we saw how to customize the pattern filter.

Wednesday, August 19, 2009

Applying filters to trees and tables in Eclipse RCP (3.4 - Ganymede)

Introduction:

This article talks about applying filters to trees and tables. Basically, this would apply to any viewer (which extends StructuredViewer) in Eclipse RCP framework.

Implementing filter:

All filters extend from ViewerFilter and need to implement select method. select method looks like this:

public boolean select(final Viewer viewer, final Object parentElement, final Object element)

Here viewer is the table or tree viewer from your view.
parentElement is the parent node (for the node to be selected) in the tree and for table it is the whole content of the table
element is the current node (to be selected) in the tree and for table it is the current row in the table.

element and parentElement are actually model objects which form the content in your content provider. This method needs to return true if the node/row needs to be selected based on the filter criteria.

Say you have a tree which displays names and we are writing a filter to only display names in the tree, which starts with "XYZ", then the filter class might look like this:

public class NameFilter extends ViewerFilter {

/*
* @see ViewerFilter#select(Viewer, Object, Object)
*/
public boolean select(final Viewer viewer, final Object parentElement, final Object element) {
if (element instanceof NameNode) { // Assume your tree node is of type NameNode
NameNode node = (NameNode) element;
String nodeName = node.getName();
return nodeName.startsWith("XYZ");
}

return false;
}

For a table, once you get the object (which represents the row) you can get the individual column values from the object, check the column value you want and return true or false depending on if the row needs to be displayed or not.

Attaching filter to the viewer:

Once you define the filter, next step is to attach the filter to the viewer. For example, in your view you could create a menu which lists filters. Each menuitem in the menu could be an Action (org.eclipse.jface.Action).

Your action can be like this. It basically updates the filter everytime when user clicks on the menuitem.

this.xyzNameFilter = new NameFilter(); // Creating filter instance

this.xyzNameAction = new Action("Names starting with XYZ") {
public void run() {
MyView.this.updateFilter(JobsExplorerView.this.xyzNameAction);
}
};
this.xyzNameAction .setChecked(false); // turned off initially

and updateFilter adds/removes the filter to the viewer.

public void updateFilter(final Action action) {
if (action == this.xyzNameAction) {
if (action.isChecked()) {
this.treeViewer.addFilter(this.xyzNameFilter);
} else {
this.treeViewer.removeFilter(this.xyzNameFilter);
}
}
}

treeViewer.addFilter and removeFilter takes care of adding/removing filters. Good thing about filters is you can have multiple filters and they are chained in the order as you add them. The filter filters on the results from the previous filters.

Conclusion:

This article explained about how to implement filters and adding them to your view. Though it talked about trees and tables, similar approach would work for any type of viewer.

Monday, August 10, 2009

Writing Maven (2.x) Plugins

Introduction:

This article talks about writing maven 2.x plugins. You can write maven plugins in Java, Groovy, Ruby and BeanShell. This article specifically talks about writing maven plugins in Java.

Maven Plugins:

Maven plugin is just a maven artifact which has plugin descriptor (xml file) and a set of mojos. Mojo is a java class and each mojo in the artifact corresponds to one goal in the plugin. So, a plugin can have any number of goals.

Creating maven plugin project:

You can create a maven plugin using the archetype plugin. Use the following command:

mvn archetype:create -DgroupId=com.mycompany -DartifactId=myfirst-maven-plugin -DarchetypeGroupId=org.apache.maven.archetypes -DarchetypeArtifactId=maven-archetype-mojo

Run this in any directory and it will create a maven plugin project (myfirst-maven-plugin) under that directory.

Take a look at pom.xml in the base folder. packaging is set to maven-plugin indicating that is a maven-plugin project and there will be two default dependencies. One is the maven plugin api and the other one is JUnit (test scope).

At this point, you can do mvn eclipse:eclipse if you want to start developing the plugin in eclipse.

Understanding plugin descriptor:

Before we look at the mojo's, let's take a look at the plugin descriptor. Do mvn install to install your plugin into your local repository. If you look at the jar artifact in your maven repository, you can find plugin.xml in META-INF\maven folder. "Maven plugin" plugin takes care of generating this during packaging.

<plugin>
<description></description>
<groupId>com.mycompany</groupId>
<artifactId>first-maven-plugin</artifactId>
<version>1.0-SNAPSHOT</version>
<goalPrefix>first</goalPrefix>
<isolatedRealm>false</isolatedRealm>
<inheritedByDefault>true</inheritedByDefault>
<mojos>
<mojo>
<goal>touch</goal>
<description>Goal which touches a timestamp file.</description>
<requiresDirectInvocation>false</requiresDirectInvocation>
<requiresProject>true</requiresProject>
<requiresReports>false</requiresReports>
<aggregator>false</aggregator>
<requiresOnline>false</requiresOnline>
<inheritedByDefault>true</inheritedByDefault>
<phase>process-sources</phase>
<implementation>com.mycompany.MyMojo</implementation>
<language>java</language>
<instantiationStrategy>per-lookup</instantiationStrategy>
<executionStrategy>once-per-session</executionStrategy>
<parameters>
<parameter>
<name>outputDirectory</name>
<type>java.io.File</type>
<required>true</required>
<editable>true</editable>
<description>Location of the file.</description>
</parameter>
</parameters>
<configuration>
<outputDirectory implementation="java.io.File">${project.build.directory}</outputDirectory>
</configuration>
</mojo>
</mojos>
<dependencies/>
</plugin>


The first few are maven plugin co-ordinates just like any other maven plugin. Goal prefix is the prefix for all the goals in the plugin. For example, when you do mvn surefire:test surefire is the goal prefix.

isolatedRealm - Deprecated

inheritedByDefault - defaults to true. Value of true indicates that any binding (i.e execution of goal in certain hase) in the parent pom will be inherited by the child poms.

mojos - Under mojos, you can have any number of mojo elements each corresponding to a goal in your plugin.
goal - name of your goal. In mvn surefire:test test is the goal name.

description - goal description to display in help

requiresDirectInvocation - defaults to false. Value of true means you can only execute the goal directly but
cannot bind it to any phase in the pom.

requiresProject - defaults to true. Value of true means you can execute the goal only within a maven project.

requiresReports - defaults to false. Value of true means the goal relies on the presence of reports section.

aggregator - Most likely will be deprecated in future.

requiresOnline - default is false. Value of true means the goal cannot be executed in offline (-o) mode.

phase - default phase where this goal binds to (if the user does not specify any binding to phase then this would be phase where goal would bind to). If this is not specified, then user is required to provide the binding phase.
implementation - class name of mojo

language - default is java, but can be Groovy, Ruby or BeanShell.

instantantiationStrategy - instantiation strategy to load mojo

executionStrategy - Most likely will be deprecated in future release

parameters - This list all the parameters of the mojo.

parameter name - name of the parameter

parameter type - type of the parameter (java.lang.String, java.io.File etc.,)

parameter required - if true, the parameter must be specified to run the goal

parameter editable - if false, user cannot override parameter value in the pom (the will always come from plugin descriptor)

parameter description - parameter description to display during help

parameter configuration - provides default values for all of the parameters

dependencies - maven plugin dependencies. Maven will download these dependencies before executing this goal.

Details of plugin descriptor can be daunting. But the good news is we don't have to write the plugin descriptor by hand. Maven generates them based on the annotations in the mojos. But, plugin descriptors are good to understand anyway.

Understanding Mojo:

When you open the source, you can see "MyMojo" class present there. All mojos extend from AbstractMojo which implements Mojo interface. The interface has setLog, getLog and execute() methods. Base class implements set and get log methods and the individual mojos have to implement the execute method. Simply the mojo cares only about execute and what it needs to do, when user executes this plugin.

MyMojo java class:

package com.mycompany;

/*
* Copyright 2001-2005 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

/**
* Goal which touches a timestamp file.
*
* @goal touch
*
* @phase process-sources
*/
public class MyMojo extends AbstractMojo {
/**
* Location of the file.
* @parameter expression="${project.build.directory}"
* @required
*/
private File outputDirectory;

public void execute() throws MojoExecutionException {
File f = outputDirectory;

if (!f.exists()) {
f.mkdirs();
}

File touch = new File(f, "touch.txt");

FileWriter w = null;
try {
w = new FileWriter(touch);

w.write("touch.txt");
} catch (IOException e) {
throw new MojoExecutionException("Error creating file " + touch, e);
} finally {
if (w != null) {
try {
w.close();
} catch (IOException e) {
// ignore
}
}
}
}
}

As said above, we don't have to write the plugin descriptor by ourselves, but it comes from annotations from the mojo java class. If you look at the javadoc for class, it has description and then @goal goalName to indicate that this mojo class defines a goal and also @phase (which is optional) to indicate which phase the goal binds to by default.

javadoc on the variable outputDirectory has @required which means the parameter is required to run the goal and it also has @parameter to indicate that this variable is a parameter which user can use to configure the goal. The parameter also has default configuration which comes from default-value attribute. You can also specify alias for your parameter using alias attribute.
Parameter example:
@parameter [alias="someAlias"] [expression="${someExpression}"]
[default-value="value"]

You can also add @readOnly attribute, which makes the parameter read only
and not configurable by the user. To deprecate a parameter, use @deprecated
attribute.

@requiresDependencyResolution - Flags this goal as requiring the
dependencies in the specified scope i.e all the dependencies in this scope
would first be resolved before executing the goal.
So, when the plugin is built, it builds the plugin descriptor using these and the rest of the parameters
use defaults.


Running the goal:

mvn com.mycompany:myfirst-maven-plugin:1.0-SNAPSHOT:touch (which is mvn groupId:artifactId:version:goal)
Notice that this goal runs only within any of your maven
projects. If you run it outside, then it will error out,
since @requiresProject is true by default. You can turn
it off in the mojo, if you want to run the goal in any
directory.

If you want to customize/configure the params you can do
so using the -D parameter.

Plugin Prefix:

As you can see typing the above goal is long and cumbersome.
To avoid typing this everytime, maven assigns plugin prefix.

For our plugin, prefix is "myfirst". This is because maven
automatically generates plugin prefix for your plugin if
your plugin name follows the maven syntax of
$name-maven-plugin or maven-$name-plugin. You can see in
maven-metadata-local.xml in your group id directory in M2
Repo to get an idea of how the prefixes are assigned.

<plugin>
<name>myfirst maven Plugin Maven Mojo</name>
<prefix>myfirst</prefix>
<artifactId>myfirst-maven-plugin</artifactId>
</plugin>

Here myfirst is the prefix for myfirst maven plugin.

But by default, maven searches in the
org.apache.maven.plugins (core plugins) and
org.codehaus.mojo (extra plugins) groups to see if the
plugin is assigned a prefix. It looks at
maven-metadata-central.xml to find this.
If we want our
group id to be also searched for prefixes, then we can
do so by adding our plugin group in settings.xml.

Add this block to your ~\.m2\settings.xml:

<pluginGroups>
<pluginGroup>com.mycompany</pluginGroup>
</pluginGroups>

Now, you should be able to run the same above goal like
this: mvn myfirst:touch

If you do not like the maven generated plugin prefix, you
can also set your own by configuring maven plugin plugin in
your pom. For, example you can add the following to your
pom:

<build>
<plugins>
<plugin>
<artifactId>maven-plugin-plugin</artifactId>
<version>2.3</version>
<configuration>
<goalPrefix>mycustomprefix</goalPrefix>
</configuration>
</plugin>
</plugins>
</build>

Now, you should be able to run the same above goal like
this:
mvn mycustomprefix:touch


Logging and Exceptions:

During execution, the mojo can get the log using getLog and
log messages. This log is very similar to log4j and has
debug, info, warn, error etc., Mojo should log error
messages if it is recoverable and can still continue. But,
if it cannot continue, it should throw exception. There are
two types of exceptions available and a mojo should decide
on which exception to throw at what time:

MojoExecutionException - Throw this for fatal exceptions
and the build cannot continue. This will cause BUILD ERROR
message to be displayed.
MojoFailureException - The goal cannot continue, but still
the build can continue.

Customizing Mojo:

You can customize the mojo using mojo parameters. You can
customize the parameter using the following ways:

1) Using -D option in command line while executing the goal
(-DparamName=paramValue)
2) Using the properties section in settings.xml
<properties>
paramValue
</properties>
3) Adding the goal to the build section of the POM in your
project (by using executions and configuration). You can
even bind your goal to multiple phases and configure them
differently while executing in each phase.

settings.xml takes precendence over -D property and the one
in pom takes precedence over settings.xml.

Parameter Types:

The parameter type can be String, StringBuffer, Character,
Boolean (true or false), Integer, Double, Float, Date
(Example format: "yyyy-MM-dd HH:mm:ssa" (a sample date is
"2009-08-15 3:43:23PM"), URL (http:\\www.mycompany.com),
Files and Directories (C:\temp.txt)

We can also have multi-valued parameters.

1) For array - private String[] files;
<files>
<param>c:\test.txt</param>
<param>c:\test2.txt</param>
</files>

2) Collection (Any implementation of collection interface
like LinkedList, HashSet etc.,)
private HashSet mySetOfFiles;
<mySetOfFiles>
<param>c:\test.txt</param>
<param>c:\test2.txt</param>
</mySetOfFiles>

3) Map (Any implementation of Map interface like HashMap,
LinkedHashMap etc.,)
private Map myMap;
<myMap>
<key1>value1</key1>
<key2>value2</key2>
</myMap>

4) Properties - java.util.properties
private Properties myProps;
<myProps>
<property>
<name>propertyName1</name>
<value>propertyValue1</value>
<property>
<property>
<name>propertyName2</name>
<value>propertyValue2</value>
<property>
</myProps>

5) Any other object - private Tag tag;
(name and desc are fields of Tag)

...
<configuration>
<tag>
<name>tagname</name>
<desc>tagdesc</desc>
</tag>
</configuration>
...

The rules for mapping complex objects are as follows:

  • There must be a private field that corresponds to name of the element being mapped. So in our case the tag element must map to a tag field in the mojo. If the private variable has _ in it (for example: name_) and if you want name to be the parameter then add a setter (setName) so that this can work.
  • The object instantiated must be in the same package as the Mojo itself. So if your mojo is in com.mycompany.mojo.query then the mapping mechanism will look in that package for an object named Tag.
  • If you wish to have the object to be instantiated live in a different package or have a more complicated name then you must specify this using an implementation attribute like this:
    ...
    <configuration implementation="com.mycompany.xyz.Tag">
    <tag>
    <name>tagname</name>
    <desc>tagdesc</desc>
    </tag>
    </configuration>
    ...
Handling pre-requisites: 

You can use @executes goal to achieve this:

This annotation will cause Maven to spawn a parallel build
and execute a goal or a lifecycle in a parallel instance of
Maven that isn't going to affect the current build.

Few examples:

@execute phase="package" - Executes a parallel lifecycle
ending in the specified phase. Results of this execution is
available via maven property ${executedProperty}.
@execute goal="eclipse:eclipse" - Executes this goal in
parallel. Results of this does not affect the current build.

Conclusion:

We saw how to create a maven plugin, went through plugin
descriptor file (plugin.xml), and mojo's. We then saw how to
install the maven plugin, how plugin prefix mechanism works,
how to customize mojo parameters and different types of
parameters including custom objects. Finally, we also briefly
touched upon how to execute pre-conditions for the goal. Hope
this helps in understanding how to write maven plugins and
gives you a quick start on writing your own maven plugin.