Wednesday, December 2, 2020

Add Checkstyle support to Eclipse, Maven, and Jenkins

After PMD and SpotBugs we will have a look at Checkstyle integration into the IDE and our maven builds. Parts of this tutorial are already covered by Lars' tutorial on Using the Checkstyle Eclipse plug-in.

Step 1: Add Eclipse IDE Support

First install the Checkstyle Plugin via the Eclipse Marketplace. Before we enable the checker, we need to define a ruleset to run against. As in the previous tutorials, we will setup project specific rules backed by one ruleset that can also be used by maven later on.

Create a new file for your rules in <yourProject>.releng/checkstyle/checkstyle_rules.xml. If you are familiar with writing rules just add them. In case you are new, you might want to start with one of the default rulesets of checkstyle.

Once we have some rules, we need to add them to our projects. Therefore right click on a project and select Checkstyle/Activate Checkstyle. This will add the project nature and a builder. To make use of our common ruleset, create a file <project>/.checkstyle with following content.

<?xml version="1.0" encoding="UTF-8"?>

<fileset-config file-format-version="1.2.0" simple-config="false" sync-formatter="false">
  <local-check-config name="Skills Checkstyle" location="/yourProject.releng/checkstyle/checkstyle_rules.xml" type="project" description="">
    <additional-data name="protect-config-file" value="false"/>
  </local-check-config>
  <fileset name="All files" enabled="true" check-config-name="Skills Checkstyle" local="true">
    <file-match-pattern match-pattern=".java$" include-pattern="true"/>
  </fileset>
</fileset-config>

Make sure to adapt the name and location attributes of local-check-config according to your project structure.

Checkstyle will now run automatically on builds or can be triggered manually via the context menu: Checkstyle/Check Code with Checkstyle.

Step 2: Modifying Rules

While we had to do our setup manually, we can now use the UI integration to adapt our rules. Select the Properties context entry from a project and navigate to Checkstyle, page Local Check Configurations. There select your ruleset and click Configure... The following dialog allows to add/remove rules and to change rule properties. All your changes are backed by our checkstyle_rules.xml file we created earlier.

Step 3: Maven Integration

We need to add the Maven Checkstyle Plugin to our build. Therefore add following section to your master pom:

	<properties>
		<maven.checkstyle.version>3.1.1</maven.checkstyle.version>
	</properties>

	<build>
		<plugins>
			<!-- enable checkstyle code analysis -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-checkstyle-plugin</artifactId>
				<version>${maven.checkstyle.version}</version>
				<configuration>
					<configLocation>../../releng/yourProject.releng/checkstyle/checkstyle_rules.xml</configLocation>
					<linkXRef>false</linkXRef>
				</configuration>

				<executions>
					<execution>
						<id>checkstyle-integration</id>
						<phase>verify</phase>
						<goals>
							<goal>check</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

In the configuration we address the ruleset we also use for the IDE plugin. Make sure that the relative path fits to your project setup. In the provided setup execution is bound to the verify phase.

Step 4: File Exclusions

Excluding files has to be handled differently for IDE and Maven. The Eclipse plugin allows to define inclusions and exclusions via file-match-pattern entries in the .checkstyle configuration file. To exclude a certain package use:

  <fileset name="All files" enabled="true" check-config-name="Skills Checkstyle" local="true">
    ...
    <file-match-pattern match-pattern="org.yourproject.generated.package.*$" include-pattern="false"/>
  </fileset>

In maven we need to add exclusions via the plugin configuration section. Typically such exclusions would go to the pom of a specific project and not the master pom:

	<build>
		<plugins>
			<!-- remove generated resources from checkstyle code analysis -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-checkstyle-plugin</artifactId>
				<version>${maven.checkstyle.version}</version>

				<configuration>
					<excludes>**/org/yourproject/generated/package/**/*</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

Step 5: Jenkins Integration

If you followed my previous tutorials on code checkers, then this is business as usual: use the warnings-ng plugin on Jenkins to track our findings:

	recordIssues tools: [checkStyle()]

Try out the live chart on the skills project.

Tuesday, November 24, 2020

Add SpotBugs support to Eclipse, Maven, and Jenkins

SpotBugs (successor of FindBugs) is a tool for static code analysis, similar like PMD. Both tools help to detect bad code constructs which might need improvement. As they partly detect different issues, they may be well combined and used simultaneously.

Step 1: Add Eclipse IDE Support

The SpotBugs Eclipse Plugin can be installed directly via the Eclipse Marketplace.

After installation projects can be configured to use it from the projects Properties context menu. Navigate to the SpotBugs category and enable all checkboxes on the main site. Further set Minimum rank to report to 20 and Minimum confidence to report to Low.

Once done SpotBugs immediately scans the project for problems. Found issues are displayed as custom markers in editors. Further they are visible in the Bug Explorer view as well as in the Problems view.

SpotBugs also comes with a label decoration on elements in the Package Explorer. If you do not like these then disable all Bug count decorator entries in Preferences/General/Appearance/Label Decorations.

Step 2: Maven Integration

Integration is done via the SpotBugs Maven Plugin. To enable, add following section to your master pom:

	<properties>
		<maven.spotbugs.version>4.1.4</maven.spotbugs.version>
	</properties>

	<build>
		<plugins>
			<!-- enable spotbugs code analysis -->
			<plugin>
				<groupId>com.github.spotbugs</groupId>
				<artifactId>spotbugs-maven-plugin</artifactId>
				<version>${maven.spotbugs.version}</version>

				<configuration>
					<effort>Max</effort>
					<threshold>Low</threshold>
					<fork>false</fork>
				</configuration>

				<executions>
					<execution>
						<id>spotbugs-integration</id>
						<phase>verify</phase>
						<goals>
							<goal>spotbugs</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

The execution entry takes care that the spotbugs goal is automatically executed during the verify phase. If you remove the execution section you would have to call the spotbugs goal separately:

mvn spotbugs:spotbugs

Step 3: File Exclusions

You might have code that you do not want to get checked (eg generated files). Exclusions need to be defined in an xml file. A simple filter on package level looks like this:

<?xml version="1.0" encoding="UTF-8"?>
<FindBugsFilter>
    <!-- skip EMF generated packages -->
    <Match>
        <Package name="~org\.eclipse\.skills\.model.*" />
    </Match>
</FindBugsFilter>

See the documentation for a full description of filter definitions.

Once defined this file can be used from the SpotBugs Eclipse plugin as well as from the maven setup.

To simplify the maven configuration we can add following profile to our master pom:

	<profiles>
		<profile>
			<!-- apply filter when filter file exists -->
			<id>auto-spotbugs-exclude</id>
			<activation>
				<file>
					<exists>.settings/spotbugs-exclude.xml</exists>
				</file>
			</activation>

			<build>
				<plugins>
					<!-- enable spotbugs exclude filter -->
					<plugin>
						<groupId>com.github.spotbugs</groupId>
						<artifactId>spotbugs-maven-plugin</artifactId>
						<version>${maven.spotbugs.version}</version>

						<configuration>
							<excludeFilterFile>.settings/spotbugs-exclude.xml</excludeFilterFile>
						</configuration>
					</plugin>
				</plugins>
			</build>
		</profile>
	</profiles>

It gets automatically enabled when a file .settings/spotbugs-exclude.xml exists in the current project.

Step 4: Jenkins Integration

Like with PMD, we again use the warnings-ng plugin on Jenkins to track our findings:

	recordIssues tools: [spotBugs(useRankAsPriority: true)]

Try out the live chart on the skills project.

Final Thoughts

PMD is smoother on integration as it stores its rulesets in a common file which can be shared by maven and the Eclipse plugin. SpotBugs currently requires to manage rulesets separately. Still both can be implemented in a way that users automatically get the same warnings in maven and the IDE.

Friday, November 20, 2020

Add Code Coverage Reports to Eclipse, Maven, and Jenkins

Code coverage may provide some insights in your tests. They show which classes, lines of codes, and conditional branches are called by your tests. A high percentage of coverage does not automatically mean that your tests are great - as you might not have a single assertion in your test code - but at least they can give you an impression of dark areas in your code base.

This article is heavily based on the article of Lorenzo Bettini on JaCoCo Code Coverage and Report of multiple Eclipse plug-in projects, so the credits for this setup are his!

Step 1: Eclipse IDE Setup

Coverage in Java projects is typically tracked with the JaCoCo library. The according plugin for Eclipse is called EclEmma and is available via the Eclipse Marketplace.

After installation you have a new run target 

that adds coverage information to your execution. Only thing to do is to rerun your unit tests and check out the Coverage view.


Multiple coverage sessions can be combined into one. That allows to accumulate the results of multiple unit tests into a single coverage report.

Step 2: Tycho integration

For the next steps I expect that you basically followed my tycho tutorials and have a similar setup.

First we need to enable JaCoCo in our builds:

	<build>
		<plugins>
			<!-- enable JaCoCo code coverage -->
			<plugin>
				<groupId>org.jacoco</groupId>
				<artifactId>jacoco-maven-plugin</artifactId>
				<version>0.8.6</version>

				<configuration>
					<output>file</output>
				</configuration>

				<executions>
					<execution>
						<id>jacoco-initialize</id>
						<phase>pre-integration-test</phase>
						<goals>
							<goal>prepare-agent</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

Tycho surefire executes unit tests in the maven integration-test phase, therefore we start the agent right before. This plugin needs to be active for any plugin of type eclipse-plugin-test (see tycho tutorial), but it is safe to put it in the master pom of your *.releng project.

Now each test run creates coverage reports. For analysis purposes we need to merge them into a single one. Therefore create a new General/Project in your workspace, named *.releng.coverage. In the pom.xml file we need to add a step to aggregate all reports into one:

	<build>
		<plugins>
			<plugin>
				<groupId>org.jacoco</groupId>
				<artifactId>jacoco-maven-plugin</artifactId>
				<version>${jacoco.version}</version>
				<executions>
					<execution>
						<phase>verify</phase>
						<goals>
							<goal>report-aggregate</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
	</build>

Afterwards we need to define dependencies for the projects containing our source code:

	<dependencies>
		<!-- Code dependencies to show coverage on -->
		<dependency>
			<groupId>com.example</groupId>
			<artifactId>com.example.plugin1</artifactId>
			<version>0.1.0-SNAPSHOT</version>
			<scope>compile</scope>
		</dependency>

		<dependency>
			<groupId>com.example</groupId>
			<artifactId>com.example.plugin2</artifactId>
			<version>0.1.0-SNAPSHOT</version>
			<scope>compile</scope>
		</dependency>
		...
	</dependencies>

Further we need dependencies to our test fragments (mind the different scope) :

	<dependencies>
		...
		<!-- Test dependencies -->
		<dependency>
			<groupId>com.example</groupId>
			<artifactId>com.example.project1.test</artifactId>
			<version>0.1.0-SNAPSHOT</version>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>com.example</groupId>
			<artifactId>com.example.project2.test</artifactId>
			<version>0.1.0-SNAPSHOT</version>
			<scope>test</scope>
		</dependency>
		...
	</dependencies>

If unsure, have a look at a complete pom file.

Finally add the new project as a module to your master pom:

	<modules>
		...
		<module>../your.project.releng.coverage</module>
		...
	</modules>

The maven build now generates *.releng.coverage/target/site/jacoco-aggregate/jacoco.xml which can be picked up by various tools. Further you get a nice HTML report in the same folder for free.

Step 3: Jenkins reports

While you may directly publish the HTML report on your jenkins builds, I prefer to use the Code Coverage plugin. With a single instruction in your pipeline

	publishCoverage adapters: [jacocoAdapter(path: 'releng/com.example.releng.coverage/target/site/jacoco-aggregate/jacoco.xml')], sourceFileResolver: sourceFiles('STORE_LAST_BUILD')

it generates nice, interactive reports like these:

You may also have a look at this live report to play around with.

Wednesday, November 18, 2020

Add PMD support to Eclipse, Maven, and Jenkins

PMD is a static code analyzer that checks your source for problematic code constructs, design patterns, and code style.

The code smells reported on grown projects might be huge at first, but PMD allows to customize its rules and to adapt them to your needs.

Step 1: Add PMD support to Eclipse

I am using eclipse-pmd which can be installed from the Eclipse Marketplace.

Step 2: Define a ruleset

PMD needs a ruleset to run against. It is stored as an xml file and can be global, workspace specific or project specific. The choice is up to you. For eclipse projects I typically have a "releng" project to host all my configuration files.

A default ruleset looks like this:

<?xml version="1.0"?>
<ruleset name="Custom Rules"
	xmlns="http://pmd.sourceforge.net/ruleset/2.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 https://pmd.sourceforge.io/ruleset_2_0_0.xsd">

	<description>custom ruleset</description>

	<rule ref="category/java/bestpractices.xml" />
	<rule ref="category/java/codestyle.xml" />
	<rule ref="category/java/design.xml" />
	<rule ref="category/java/documentation.xml" />
	<rule ref="category/java/errorprone.xml" />
	<rule ref="category/java/multithreading.xml" />
	<rule ref="category/java/performance.xml" />
	<rule ref="category/java/security.xml" />
</ruleset>

Store your ruleset somewhere in your workspace or on your file system.

Step 3: Enable PMD on project level

Right click on a project in your Eclipse workspace and select Properties. In PMD section check Enable PMD for this project and Add... the ruleset file stored before. The Name is not important and can be freely chosen.

Your rules are live now and PMD should immediately start to add warnings to your code and the Problems view.

Step 4: Refine your rules

The default ruleset might report some issues you do want to treat differently in your project. Therefore you may change rules by setting parameters or disable unwanted rules at all. To alter a rule, you first have to find it in the list of available rules. For disabling you just need to add an exclude node to your rule settings file, eg:

	<rule ref="category/java/bestpractices.xml">
		<!-- logger takes care of guarding -->
		<exclude name="GuardLogStatement" />
	</rule>

Configuring a rule can be done like this:

	<rule ref="category/java/codestyle.xml/ClassNamingConventions">
		<properties>
			<property name="utilityClassPattern"
				value="[A-Z][a-zA-Z0-9]+" />
		</properties>
	</rule>

A full working ruleset as used by one of my projects can be viewed online.

Whenever you change your ruleset you need to recompile your project to get these rules applied. You may do so by selecting Project/Clean... from the main menu.

Step 5: Maven integration

Integration is done by the maven-pmd-plugin. Just add following section to your pom:

	<build>
		<plugins>
			<!-- enable PMD code analysis -->
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-pmd-plugin</artifactId>
				<version>3.13.0</version>
				<configuration>
					<linkXRef>false</linkXRef>
					<rulesets>path/to/your/ruleset.xml</rulesets>
				</configuration>
			</plugin>
		</plugins>
	</build>

Make sure to adapt the path to your ruleset accordingly.

Afterwards run your build using

mvn pmd:pmd pmd:cpd

If you use the maven-site-plugin, you may additionally generate html reports of PMD findings.

Step 6: Jenkins integration

Static reports are nice, but charts over time/commits are even better. In case you use Jenkins you may have a look at the warnings-ng plugin. When you generate yout pmd.xml files via maven, this plugin can pick them up and draw nice reports. In a pipeline build this only needs one line:

recordIssues(tools: [cpd(), pmdParser()])

to get charts like these:


Try out the live chart on the skills project.

Finally the plugin even allows to compare the amount of issues against a baseline. This allows to add  quality gates, eg to fail the build in case your issue count increases. I strongly encourage to enforce such rules. Otherwise warnings are nice but do get ignored by everybody.


Monday, November 18, 2019

Jakarta Microprofile REST Client in Eclipse

Today we are going to implement a simple REST client for an Eclipse RCP application. Now with Jakarta @ Eclipse and all these nice Microprofile implementations this should be a piece of cake, right? Now lets see...

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

Step 1: Dependencies

The Eclipse Microprofile REST Client repository is a good place to get started. It points to several implementations (at the bottom of the readme). Unfortunately these implementations do not host any kind of p2 sites which we could use directly. So our next stop is Eclipse Orbit, but same situation there. This means we need to collect our dependencies manually.

For my example I used RESTEasy, simply as it was the only one I could get working within reasonable time. To fetch dependencies, download the latest version of RESTEasy. As the RESTEasy download package does not contain the REST client API, we need to fetch that from another source. I found it in the Apache CXF project, so download the latest version too. If you know a better source, please let me know in the comments.

Now create a new Plug-in from Existing JAR Archives. Click on Add External... and add all jars from resteasy-jaxrs-x.y.z.Final/lib/*.jar. Further add apache-cxf-x.y.z/lib/jakarta.ws.rs-api-x.y.z.jar.
This plug-in now contains all dependencies we need for our client. Unfortunately also a lot of other stuff we probably do not need, but we leave the cleanup for later.

Step 2: Define the REST service

For our example we will build a client for the Petstore Service, which can be used for testing purposes. Further it provides a swagger interface to test the REST calls online. I recommend to check out the API and play with some commands online and with curl.

Lets write a simple client for the store with its 4 commands. The simplest seems to be the inventory command, so we will start there. Create a new Java interface:
package com.codeandme.restclient.resteasy;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

public interface IStoreService {

 @GET
 @Path("/v2/store/inventory")
 @Produces(MediaType.APPLICATION_JSON)
 Response getInventory();
}
Everything necessary for RESTEasy is provided via annotations:

  • @Path defines the path for the command of the REST service
  • @GET defines that we have to use a GET command (there exist annotations for POST, DELETE, PUT)
  • @Produces finally defines the type of data we do get in response from the server.
Step 3: Create an instance of the service

Create a new class StoreServiceFactory:
package com.codeandme.restclient.resteasy;

import java.net.URI;
import java.net.URISyntaxException;

import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyWebTarget;
import org.jboss.resteasy.client.jaxrs.internal.ResteasyClientBuilderImpl;

public class StoreServiceFactory {

 public static IStoreService createStoreService() throws URISyntaxException {
  ResteasyClient client = new ResteasyClientBuilderImpl().build();
  ResteasyWebTarget target = client.target(new URI("https://petstore.swagger.io/"));
  return target.proxy(IStoreService.class);
 }
}

This is the programmatic way to create a client instance. There also exists another method called CDI, which I did not try out in Eclipse.

The service is ready and usable, so give it a try. The result object returned does contain some valuable information:

  • getStatus() provides the HTTP response status. 200 is expected for a successful getInventory()
  • getEntity() provides an InputStream which contains the JSON encoded response data from the server
Step 4: Response decoding

Our response is encoded as JSON collection of properties. In Java terms this basically reflects to a Map<String, String>. Instead of decoding the data manually, we let the framework do it for us:

Change the IStoreService to:

 Map<String, String> getInventory();
Anything else is done by the framework. Now how easy was that?

Step 5: POST request

To place an order we need order parameters. Best we encapsulate them in a dedicated Order class. From the definition of the order REST call we can see that we need following class properties: id, petId, quantity, shipDate, status, complete. Add these parameters as fields to the Order class and create getters/setters for them.

Now we can extend our IStoreService with the fileOrder() call:


@Path("/v2/store")
public interface IStoreService {

 @GET
 @Path("inventory")
 @Produces(MediaType.APPLICATION_JSON)
 Map<String, String> getInventory();

 @POST
 @Path("order")
 @Consumes(MediaType.APPLICATION_JSON)
 void fileOrder(Order order);
}

The Order automatically gets encoded as JSON object. No need for us to do the coding manually!

As parts of the path are the same for both calls, I moved the common component to the class level.

Step 6: Path parameters

To fetch an order we need to put the orderId in the request path. Coding of such parameters is put in curly braces. The parameter on the java call then gets annotated so the framework knows which parameter value to put into the path:

 @GET
 @Path("order/{orderId}")
 @Produces(MediaType.APPLICATION_JSON)
 Order getOrder(@PathParam("orderId") int orderId);

Again the framework takes care of the decoding of the JSON data.

Step 7: DELETE an Order

Deleting needs the orderId as before:

 @DELETE
 @Path("order/{orderId}")
 void deleteOrder(@PathParam("orderId") int orderId);

The REST API does not provide a useful JSON response to the delete call. One option is to leave the response type to void. In case the command fails, an exception will be thrown (eg when the orderId is not found and the server returns 404).

Another option is to set the return type to javax.ws.rs.core.Response. Now we do get everything the server sends back and no execption is thrown anymore. Sometimes we might only be interested in the status code. This can be fetched when setting the return type to Response.Status. Again, no exception will be thrown on a 404.

Optional: Only have required RESTEasy dependencies

Looking at all these jars I could not figure out a good way to get rid of the ones unused by the REST client. So I provided unit tests for all my calls and then removed dependencies step by step until I found the minimal set of required jars.



Monday, July 8, 2019

Building UIs with EASE

You probably used EASE before to automate daily tasks in your IDE or to augment toolbars and menus with custom functionality. But so far scripts could not be used to build UIs. This changed with the recent contribution of the UI Builder module.

What it is all about
The UI Builder module allows to create views and dialogs by pure script code in your IDE. It is great for custom views that developers do not want to put into their products, for rapid prototyping and even for mocking.

The aim of EASE is to hide layout complexity as much as possible and provide a simple, yet flexible way to implement typical UI tasks.

Example 1: Input Form
We will start by creating a simple input form for address data.

loadModule("/System/UI Builder");
createView("Create Contact");

setColumnCount(2);
createLabel("First Name:");
var txtFirstName = createText();
createLabel("Last Name:");
var txtLastName = createText();
This snippet will create a dynamic view as shown below:
The renderer used will apply a GridLayout. By setting the columnCount to 2 we may simply add our elements without providing any additional layout information - a simple way to create basic layouts.

If needed EASE provides more control by providing layout information when creating components:

createView("Create Contact");
createLabel("First Name:", "1/1 >x");
var txtFirstName = createText("2-4/1 o!");
createLabel("Last Name:", "1/2 >x");
var txtLastName = createText("2-4/2 o!");
Creates the same view as above, but now with detailed layout information.
As an example "1/2 >x" means: first column, second row, horizontal align right, vertical align middle. A full documentation on the syntax is provided in the module documentation (Hover over the UI Builder module in the Modules Explorer view).

Now lets create a combo viewer to select a country for the address:
cmbCountry = createComboViewer(["Austria", "Germany", "India", "USA"])
Simple, isn't it?

So far we did not need to react on any of our UI elements. Next step is to create a button which needs some kind of callback action:
createButton("Save 1", 'print("you hit the save button")')
createButton("Save 2", saveAddress)

function saveAddress() {
 print("This is the save method");
}
The first version of a button adds the callback code as string argument. When the button gets pressed, the callback code will be executed. You may call any script code that the engine is capable of interpreting.

The second version looks a bit cleaner, as it defines a function saveAddress() which is called on a button click. Note that we provide a function reference to createButton().

View the full example of this script on our script repository. In addition to some more layouting it also contains a working implementation of the save action to store addresses as JSON data files.

Interacting with SWT controls

The saveAddress() method needs to read data from the input fields of our form. This could be done using
var firstName = txtFirstName.getText();
Unfortunately SWT Controls can only be queried in the UI thread, while the script engine is executed in its own thread. To move code execution to the UI thread, the UI module provides a function executeUI(). By default this functionality is disabled as a bad script executed in the UI thread might stall your Eclipse IDE. To enable it you need to set a checkbox in Preferences/Scripting. The full call then looks like this:
loadModule("/System/UI")
var firstName = executeUI('txtFirstName.getText();');

Example 2: A viewer for our phone numbers

Now that we are able to create some sample data, we also need a viewer for our phone numbers. Say we are able to load all our addresses into an array, the only thing we need is a table viewer to visualize our entries. Following 2 lines will do the job:
var addresses = readAddresses();
var tableViewer = createTableViewer(addresses)
Where readAddresses() collects our *.address files and stores them into an array.

So the viewer works, however we need to define how our columns shall be rendered.
createViewerColumn(tableViewer, "Name", createLabelProvider("getProviderElement().firstName + ' ' + getProviderElement().lastName"))
createViewerColumn(tableViewer, "Phone", createLabelProvider("getProviderElement().phone"))
Whenever a callback needs a viewer element, getProviderElement() holds the actual element.
We are done! 3 lines of code for a TableViewer does not sound too bad, right? Again a full example is available on our script repository. It automatically loads *.address files from your workspace and displays them in the view.

Example 3: A workspace viewer

We had a TableViewer before, now lets try a TreeViewer. As a tree needs structure, we need to provide a callback to calculate child elements from a given parent:
var viewer = createTreeViewer(getWorkspace().getProjects(), getChildren);

function getChildren() {
 if (getProviderElement() instanceof org.eclipse.core.resources.IContainer)
  return getProviderElement().members();
 
 return null;
}
So simple! The full example looks like this:
Example 4: Math function viewer

The last example demonstrates how to add a custom Control to a view.
For plotting we use the Charting module that is shipped with EASE. The source code should be pretty much self explanatory.

Some Tips & Tricks

  • Layouting is dynamic.
    Unlike the Java GridLayout you do not need to fill all cells of your layout. The EASE renderer takes care to automatically fill empty cells with placeholders
  • Elements can be replaced.
    If you use coordinates when creating controls, you may easily replace a given control by another one. This simplifies the process of layouting (eg if you experience with alignments) and even allows a view to dynamically change its components depending on some external data/events
  • Full control.
    While some methods from SWT do not have a corresponding script function, still all SWT calls may be used as the create* methods expose the underlying SWT instances.
  • Layout help.
    To simplify layouting use the showGrid() function. It displays cell borders that help you to see row/column borders.


Monday, March 25, 2019

JFace TableViewer sorting via Drag and Drop

Recently I wanted to sort elements in a TableViewer via drag and drop and was astonished that I could not find  existing helper classes or tutorial for this fairly trivial use case. So here is one for you in case you got the same use case.

Source code for this tutorial is available on github as a single zip archive, as a Team Project Set or you can browse the files online.

If you are just interested in the helper class, have a look at DnDSortingSupport.

Prerequisites:

To have something to work on I will start with a TableViewer containing some data stored in a java.util.List. It is a default TableViewer and therefore I expect you have something similar ready for your experiments.

Step 1: Add drag support

Drag and Drop support for SWT is implemented via DragSource and DropTarget instances. To define that we can drag data, we need to bind a DragSource to a Control.
  DragSource dragSource = new DragSource(tableViewer.getControl(), DND.DROP_MOVE);
  dragSource.setTransfer(LocalSelectionTransfer.getTransfer());
  dragSource.addDragListener(new DragSourceAdapter() {

   @Override
   public void dragStart(DragSourceEvent event) {
    event.doit = !tableViewer.getStructuredSelection().isEmpty();
   }

   @Override
   public void dragSetData(DragSourceEvent event) {
    if (LocalSelectionTransfer.getTransfer().isSupportedType(event.dataType)) {
     LocalSelectionTransfer.getTransfer().setSelection(tableViewer.getStructuredSelection());
     LocalSelectionTransfer.getTransfer().setSelectionSetTime(event.time & 0xFFFF);
    }
   }

   @Override
   public void dragFinished(DragSourceEvent event) {
    LocalSelectionTransfer.getTransfer().setSelection(null);
    LocalSelectionTransfer.getTransfer().setSelectionSetTime(0);
   }
  });

In line 1 we create the DragSource and define allowed DnD operations. As we want to sort elements, we only allow DND.MOVE operations. Then we define the way data gets transferred from the DragSource to the DropTarget. As we stay within  the same Eclipse application we may use a LocalSelectionTransfer.

The first thing that happens on a drag is dragStart(). Technically the selection cannot be empty as we have to select something before we start the operation, so this implementation is merely to understand how we could deny the operation right from the start.

After the drop operation got accepted in the DropTarget (see below) we get asked to dragSetData() and define what data we are moving. setSelectionSetTime() is not needed by our DropTarget, so again this is for completeness only.

Finally we need to clean up after the operation is done.

Step 2: Add drop support

Implementation is similar like before, just now we need a DropTarget. Instead of writing our own DropTargetListener we may use a ViewerDropAdapter which covers most of the required work already.
  DropTarget dropTarget = new DropTarget(tableViewer.getControl(), DND.DROP_MOVE);
  dropTarget.setTransfer(LocalSelectionTransfer.getTransfer());
  dropTarget.addDropListener(new ViewerDropAdapter(tableViewer) {

   @Override
   public void dragEnter(DropTargetEvent event) {
    // make sure drag was triggered from current tableViewer
    if (event.widget instanceof DropTarget) {
     boolean isSameViewer = tableViewer.getControl().equals(((DropTarget) event.widget).getControl());
     if (isSameViewer) {
      event.detail = DND.DROP_MOVE;
      setSelectionFeedbackEnabled(false);
      super.dragEnter(event);
     } else
      event.detail = DND.DROP_NONE;
    } else
     event.detail = DND.DROP_NONE;
   }

   @Override
   public boolean validateDrop(Object target, int operation, TransferData transferType) {
    return true;
   }

   @Override
   public boolean performDrop(Object target) {
    int location = determineLocation(getCurrentEvent());
    if (location == LOCATION_BEFORE) {
     if (modelManipulator.insertBefore(getSelectedElement(), getCurrentTarget())) {
      tableViewer.refresh();
      return true;
     }

    } else if (location == LOCATION_AFTER) {
     if (modelManipulator.insertAfter(getSelectedElement(), getCurrentTarget())) {
      tableViewer.refresh();
      return true;
     }
    }

    return false;
   }

   private Object getSelectedElement() {
    return ((IStructuredSelection) LocalSelectionTransfer.getTransfer().getSelection()).getFirstElement();
   }
  });

dragEnter() is the first thing that happens on the drop part of DnD. The default implementation is already fine. Our implementation additionally checks that the drag source is our current TableViewer. Further we disable the selectionFeedback. The feedback visually shows the user whether we drop before an element, on the element, or after it. The ViewerDropAdapter already supports these kind of feedbacks. Until bug 545733 gets fixed the helper class contains a small patch to provide before/after feedback only. It does not make sense to drop on another element when we do sorting, right?

validateDrop() will be queried multiple times. We might check that we do not drop the table element on itself, but we spared this check for the current example.

performDrop() finally implements the drop operation. To keep the helper class generic I used an interface that allows to insert elements before or after another element. An implementation of it needs to be passed to the helper class.

 public interface IModelManipulator {
  boolean insertBefore(Object source, Object target);

  boolean insertAfter(Object source, Object target);
 }
The helper class comes with an implementation for java.util.List, which you may reuse.