Monday, April 16, 2012

Source Provider, Property Tester and the Expressions Framework

The expressions framework provides powerful means to declare expressions via XML and query them at runtime. Such expressions can be used without loading the defining Plug-in. You can find them in command/menu enablements. There you find visibleWhen, enabledWhen, activeWhen expressions. But you also can use them for your own needs.

In this example we will create our own Source Provider with a custom property tester (don't worry, I will explain this right ahead).

There is good documentation available for further reading.

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. 

Introduction

The expression framework can create/evaluate boolean expressions - something that in the end evaluates to true or false. To evaluate we need an object to work with (a source) and operators which return a boolean value.

Eclipse already defines commonly used sources and operators.

A simple example of an expression looks like this:
<visibleWhen>
     <and>
        <systemTest
              property="os.name"
              value="Linux">
        </systemTest>
        <systemTest
              property="os.arch"
              value="i386">
        </systemTest>
     </and>
<visibleWhen>

and is a basic operator that - of course - performs a boolean and operation. systemTest queries a java system property and compares it to a given value. So this expression evaluates to true only if we are working on an i386 linux machine.

Our source is not explicitely defined in the expression as the operator queries a static source System.getProperties().

Step 1: Creating a SourceProvider

For most expressions we will need sources that contain modifiable content. Use the predefined sources to get the active editor, the active selection or similar workbench related items. In this tutorial we will create a custom source to denote the current user.

Create a new Plug-in Project called com.codeandme.coreexpressions. In the plugin.xml create a new extension for org.eclipse.ui.services and add a new sourceProvider to it. Create two new variables for the sourceProvider:
set name to com.codeandme.coreexpressions.currentStatus and com.codeandme.coreexpressions.currentUser and leave priorityLevel set to workbench.

Now implement the sourceProvider by creating a class com.codeandme.coreexpressions.ExampleSourceProvider:
package com.codeandme.coreexpressions;

import java.util.HashMap;
import java.util.Map;

import org.eclipse.ui.AbstractSourceProvider;
import org.eclipse.ui.ISources;

public class ExampleSourceProvider extends AbstractSourceProvider {

 public static final String CURRENT_STATUS = "com.codeandme.coreexpressions.currentStatus";
 public static final String CURRENT_USER = "com.codeandme.coreexpressions.currentUser";

 private Object fUser = null;
 private String fStatus = "startup";

 public ExampleSourceProvider() {
 }

 @Override
 public void dispose() {
 }

 @Override
 public Map getCurrentState() {
  HashMap<String, Object> map = new HashMap<String, Object>();
  map.put(CURRENT_USER, fUser);
  map.put(CURRENT_STATUS, fStatus);

  return map;
 }

 @Override
 public String[] getProvidedSourceNames() {
  return new String[] { CURRENT_USER, CURRENT_STATUS };
 }
}
We need to implement two methods:
  • getProvidedSourceNames()
    returns an array of source identifiers (variables). These are the same we already defined as variables in the plugin.xml.
  • getCurrentState()
    returns a Map<String, Object> with entries for each defined variable.

Step 2: Creating a property tester

Eclipse provides a generic expression operator test which allows to register custom tests.

Create a new extension for org.eclipse.core.expressions.propertyTesters with a unique id.

The type reflects to the kind of objects you'd get from the source provider. For now leave it to java.lang.Object.

The identifier of a specific property to check should be a fully qualified name. It consists of a namespace part and a properties name. It is good practice to set the namespace to your Plug-in identifier. Therefore set namespace to com.codeandme.coreexpressions and add two specific properties name,validated to properties. Hence our properties are named:
  • com.codeandme.coreexpressions.name
  • com.codeandme.coreexpressions.validated
Finally implement the class com.codeandme.coreexpressions.ExamplePropertyTester:
package com.codeandme.coreexpressions;

import org.eclipse.core.expressions.PropertyTester;

public class ExamplePropertyTester extends PropertyTester {

 private static final String NAME = "name";
 private static final String VALIDATED = "validated";

 public ExamplePropertyTester() {
 }

 @Override
 public boolean test(Object receiver, String property, Object[] args, Object expectedValue) {
  if (NAME.equals(property))
   return receiver.toString().equals(expectedValue);

  if (VALIDATED.equals(property)) {
   // do some background checks to see if user is validated
   return true;
  }

  return false;
 }
}
The receiver of the test() implementation is some object from a source provider. If you set your property tester to only accept objects of a certain type you need to add an instanceof check in the test() method.

Step 3: Your own expression

To use our source provider and property tester we can either create an expression for some activeWhen, enabledWhen or visibleWhen statement or we can extend org.eclipse.core.expressions.definitions to store the expression for later usage.

Create a new extension for org.eclipse.core.expressions.definitions with a unique id. Add a with section to define which source to use. Set variable to the name of our currentUser source: com.codeandme.coreexpressions.currentUser.

Add a test section to define our custom test. Set property to com.codeandme.coreexpressions.name and value to a string to compare against. In the sample I use "John Doe".

Make sure you activate forcePluginActivation. This defines that the Plug-in defining the property tester should be activated. If it is not activated, the property test cannot be performed.

The final definition:
         <with
               variable="com.codeandme.coreexpressions.currentUser">
            <test
                  forcePluginActivation="true"
                  property="com.codeandme.coreexpressions.name"
                  value="John Doe">
            </test>
         </with>
Step 4: Updating your source

Expressions do work right now, but our source is some kind of static. It won't change its status. Open ExampleSourceProvider and add following code:
 public void setCurrentUser(Object user) {
  fUser = user;
  fStatus = (user == null) ? "logged off" : "logged on";

  fireSourceChanged(ISources.WORKBENCH, CURRENT_USER, fUser);
  fireSourceChanged(ISources.WORKBENCH, CURRENT_STATUS, fStatus);
 }
fireSourceChanged() will tell Eclipse that a source changed and therefore forces expressions using this source to be re-evaluated.

To get an instance of your source provider you can query the ISourceProviderService:
ISourceProviderService service =(ISourceProviderService) PlatformUI.getWorkbench().getService(ISourceProviderService.class);
ISourceProvider provider = service.getSourceProvider(ExampleSourceProvider.CURRENT_USER);

3 comments:

  1. Hi,

    thanks for this great Tutorial.
    From where do you call setCurrentUser()? Where do you have a reference to your ExampleSourceProvider?

    Best regards
    Frank

    ReplyDelete
    Replies
    1. Hi Frank,

      check out the example code in the SVN repository, especially this file:
      http://codeandme.googlecode.com/svn/trunk/blog/core_expressions/com.example.coreexpressions.samples/src/com/example/coreexpressions/samples/commands/Login.java

      The reference is gatherede from ISourceProviderService.getSourceProvider(...) as shown in the very last code snippet in the blog post

      Delete