How to make a manager

From Bioclipse
Jump to: navigation, search
Development tutorial
Responsible author:jonalv
Bioclipse version:N/A
"N/A" is not a number.
Last updated:2009-10-10
Tags:


Caution: You should probably use How to make a manager 2 instead!
This page describes how managers where made once upon a time. It is left here as a reference. Please do not use this way unless you have very good reasons for not using the new way described at How to make a manager 2. Not everything that we have come to expect from a manager works with the approach described on this page.

--Jonalv 16:46, 28 August 2009 (CEST)


How managers was once made

A Bioclipse Manager is an object used for performing things related to a specific topic in Bioclipse. As an example we'll take the ui manager. It contains methods for doing stuff like opening files in editors and doing other stuff with files. The same code for doing this is being used both from the graphical user interface (GUI) and from the JavaScript console and scripts.

However there are a few differences on how you want to call manager methods from the GUI and from JavaScript. As an example, let's look at the method that removes files: ui.remove. When calling this method from JavaScript we would like to write: ui.remove("/MyProject/temp.cml"). But when using the manager from the GUI, we get an object of the type IFile so then we want to call the method remove(IFile file) on the manager.

There are also a few other tedious tasks that we want the managers to leverage. For example, if we look at the delete method of an IFile it can take an IProgressMonitor which can be used for giving feedback to the user on progress when doing an operation that might take a while. However in order to do this in a good way, we need to be running a job. Another problem is that when doing GUI stuff such as opening an editor, this must be done by the GUI thread, but if an exception is thrown in that thread, we still want feedback about what went wrong on the JavaScript console, if that happens to be the way we were calling the manager method.

What follows is a collection of instructions on how to do things like easily running a manager method as a job and converting between String and IFile and a bunch of other stuff.

Start by writing the manager interface

In order to be usable with Spring for AOP later on, our Bioclipse manager is going to be written with an interface. Bioclipse uses annotations and method signature conventions for doing automagic things with managers. Let's start out by looking at some example code:

 @PublishedClass(value = "Controls access to Bioclipse UI.")
 @TestClasses(
   "net.bioclipse.ui.business.tests.UIManagerTest," +
   "net.bioclipse.ui.business.tests.UIManagerPluginTest"
 )
 public interface IUIManager extends IBioclipseManager {
 
 @Recorded
 @PublishedMethod(params="String filePath",
                  methodSummary="Opens a file in an editor.")
 @TestMethods("testOpen_String")
 @GuiAction
 public void open(String filePath);
 
 @Recorded
 @GuiAction
 public void open(IFile file);

This is a part of the interface defining the ui manager. I have marked all the annotations in different color (yes I know it's a bit too colorful, but it's for a good cause so please bear with me...). Just by looking at all the different colors we can see that there are many annotations involved here. Let's go through them one by one.

The @PublishedClass and @PublishedMethod annotations

The first annotation found in the example is @PublishedClass. It is used by the help system in the JavaScript console in Bioclipse. The value of this annotations is the first text that is showed when when you write man ui in the JavaScript console.

What about the text that is printed when you write man ui.open? Yes, you guessed it! That text is contained in the @PublishedMethod annotation. This annotation takes two paramaters — params and methodSummary.

The two paramaters are used for documenting the manager methods and they should be used in a very special ways for our manager documentation to be consistent. The params shall be just a copy of the paramaters of the method as they are written in Java. Or, more formally: "a comma separated list of the type (without package name) and name of each parameter, separated by a space, e.g. 'IMolecule molecule' or 'ICDKMolecule mol, boolean isFancy'".

All the explanations about those paramaters shall be found in the methodSummary paramater of the annotation. Here follows an example just to make things clear:

 
 @Recorded
 @PublishedMethod(params = "IMolecule mol, boolean overwrite",                                  
                  methodSummary = "saves mol to a file, if previously read from file. " +       
         		           "Overwrite determines if existing file shall be overwritten.")
 @TestMethods("testSaveMolecule_IMolecule_boolean")
 public void saveMolecule(IMolecule mol, boolean overwrite)
             throws BioclipseException, CDKException, CoreException;
 

If the methods takes zero parameters just skip the annotation-parameter named "params". It is not mandatory (just becasue of this situation actually).

The @TestClasses and @TestMethods annotations

These two annotations are used by the test framework for keeping track of which test exercises which methods, and for making sure each method is covered by a test. The @TestClasses annotation is used to specify in which test classes the actual method that tests this manager's methods can be found, and the annotation @TestMethods specifies the names of the test methods that test the annotated method. The @TestClasses annotates a manager class, and @TestMethods annotates a manager method.

The @GuiAction annotation

The @GuiAction annotation is used by a Spring AOP advice (more about them later), basically it indicates that this method should be run by the SWT GUI thread. The reason for this annotation was that exception handling for code running in the SWT GUI thread was excessively cumbersome. So I came up with the solution to lift out the exception handling from the manager method using AOP. As a bonus, the part where you implement the Runnable interface and give it to the asyncExec method got lifted out as well.

Any method annotated with the @GuiAction annotation will be executed in the GUI thread, and exceptions thrown during such an execution will be printed to the JavaScript console if and when the method was called from the JavaScript console.

The @Recorded annotation

Also the Recorded annotation is used by a Spring AOP advice (yes, we will look at them later). If a method has this annotation the fact that it was called and with what parameters it was called will be recorded in such a way that a JavaScript script repeating whatever the user did can be generated. At least in theory. However this feature is very experimental for the moment and it is not fully working yet.

The translation from a String representing a file path to an IFile

This translation is done automagicly with the help of Spring AOP based on some method signature conventions. If there exists two manager methods with the same name and the same number of parameters but at the location where one of them takes an IFIle the other one takes a String then each time the one taking a String is called the String will be converted into an IFile and the method taking an IFile at that position will be called instead.

On the topic of running manager methods as jobs

Yes, as I hinted in the introduction to this page manager methods can be run as Eclipse jobs. This however is not done by annotations but by method signature conventions. A manager method that fulfills the following criteria will be run as a job: If there exists a method with the same name as the called method and with the same number of parameters, plus as the last parameter an IProgressMonitor then the method taking the IProgressMonitor will be called in a job with the jobs progress monitor instead of the method without IProgressMonitor that was originally called. Note that the "translation" from String to IFile also takes place.

Then implement the manager methods

So, we are happy with our interface and are ready to start implementing the manager. This is the easy part. There is just one thing. One not so very nice thing. Since the interface defines all those different methods that we don't really want to implement in our manager class -- that one taking a String instead of an IFile and that one not taking a progress monitor and so on -- we will end up with a bunch of methods that we could leave empty. I instead prefer to actually implement them in this way:

   public void remove( String filePath ) {
       throw new IllegalStateException("This method should not be called");
   }

This means that if something went wrong with the Spring AOP we immediately get an exception telling us that the method that never should be called in fact has been called. If we just left the method empty and it was called strange things could happen...

And finally we hook in all the Spring AOP stuff

Okey, buckle up and fasten your seat belts because now we are leaving the land of Java code and entering XML country. It has become time to write the XML configuration file that Spring will read and use as a recipe when instantiating our manager. It's also become time to reveal the well kept secret that there in fact exist two different manager objects for each manager. One for JavaScript and one for the graphical user interface. They are both based on the same class and they both implement the same interface (although to be precise that might not be entirely true either) but they have different Spring advices wired in to them. These configurations are made in XML files situated in META-INF/spring. You can either put all in one file or split it up among many XML files if you would prefer that. But let's look at an example:

 <?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:osgi="http://www.springframework.org/schema/osgi"
   xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
                       http://www.springframework.org/schema/osgi http://www.springframework.org/schema/osgi/spring-osgi.xsd">
   
   <osgi:service id="jmolManagerOSGI"                                      
                 ref="jmolManager"                                         
                 interface="net.bioclipse.jmol.business.IJmolManager" />   
                                                                           
   <osgi:service id="jsjmolManagerOSGI"                                    
                 ref="jsjmolManager"                                       
                 interface="net.bioclipse.jmol.business.IJSJmolManager" /> 
   
   <osgi:reference id="recordingAdvice"                                               
                   interface="net.bioclipse.recording.IRecordingAdvice" />            
                                                                                      
   <osgi:reference id="createJobAdvice"                                               
                   interface="net.bioclipse.ui.jobs.ICreateJobAdvice" />              
                                                                                      
   <osgi:reference id="wrapInProxyAdvice"                                             
                   interface="net.bioclipse.recording.IWrapInProxyAdvice" />          
                                                                                      
   <osgi:reference id="JSJobCreatorAdvice"                                            
                   interface="net.bioclipse.scripting.business.IJSJobCreatorAdvice"/> 
   
   <bean id="recordingAdvisor"                                                
         class="org.springframework.aop.support.RegexpMethodPointcutAdvisor"> 
     <property name="advice" ref="recordingAdvice" />                         
     <property name="pattern" value=".*" />                                   
   </bean>                                                                    
   
   <bean id="jmolManagerTarget"                           
         class="net.bioclipse.jmol.business.JmolManager"> 
   </bean>                                                
   
   <bean id="jmolManager"                                            
         class="org.springframework.aop.framework.ProxyFactoryBean"> 
     <property name="target"                                         
               ref="jmolManagerTarget" />                            
     <property name="proxyInterfaces"                                
               value="net.bioclipse.jmol.business.IJmolManager" />   
     <property name="interceptorNames">                              
       <list>                                                        
         <value>recordingAdvisor</value>                             
         <value>wrapInProxyAdvice</value>                            
         <value>createJobAdvice</value>                              
       </list>                                                       
     </property>                                                     
   </bean>                                                           
   
   <bean id="jsjmolManager"                                          
         class="org.springframework.aop.framework.ProxyFactoryBean"> 
     <property name="target"                                         
               ref="jmolManagerTarget" />                            
     <property name="proxyInterfaces"                                
               value="net.bioclipse.jmol.business.IJSJmolManager" /> 
     <property name="interceptorNames">                              
       <list>                                                        
         <value>JSJobCreatorAdvice</value>                           
       </list>                                                       
     </property>                                                     
   </bean>                                                           
 </beans>

That was actually the entire file for the plugin containing the ui manager. Let's go through the different parts of the file shall we?

Our actual manager bean, the jmolManagerTarget bean

Let's start with something easy -- the jmolManagerTarget bean. A Spring bean is basically just an instantiated class. This bean is the instantiation of our manager class. It is called target because it is going to be the target object of the manager proxies that we are going to create later.

Importing all advices

But before we start writing proxies let's get a hold on all the advices that we re going to use. These advices are instantiated by Spring in other Bioclipse plugins and published to the OSGI container by the Spring OSGI extender bundle. So each osgi:reference tag in this example imports the service registered for the given interface and places it in the local Spring container so that it can be used when putting together other beans.

The recording advisor

This is a recording specific thing which gives a chance to change which methods should be "decorated" with the recorder advice. For the moment it is called on all methods and then it checks for the @Recorded annotation and acts if that annotation is present. For the future it would be nice to have an Advisoer that looks for that annotation instead of simply doing all methods.

Building the manager object to be used from the gui

This is the part where we specify how our manager is going to be weaved together. Basically we specify the proxy that is going to be controlling the execution of a certain manager method. We define which interface we are gonna be proxying and what bean is going to be proxied. Then comes a list of interceptors that we want to add to our service object. For a typical GUI flavoured manager these are:

Advisor Description
recordingAdvisor Does the recording of manager methods
wrapInProxyAdvice Wraps all return IBIoObjects in a Spring proxy so calls to them can be recorded
createJobAdvice Creates jobs and performs String -> IFile translations in a way specific for a manager called from the GUI

Building the manager object to be used from JavaScript

From JavaScript we need another flavor of our manager. For example, as things are now we are not recording JavaScript calls to a manager since obviously the JavaScript code for doing that already exists. Also jobs are done in a different way since when running a JavaScript script we don't know how many ticks the progress monitor will get before being done. The advices here are:

Advisor Description
JSJobCreatorAdvice Creates jobs and performs String -> IFile translations in a way specific for a manager called by JavaScript

Publishing our manager objects

Finally we publish our new manager beans to the OSGI container so that other plugins can use them. Just as the beans we imported where associated with an interface these beans are associated with an interface when exported.

Making our manager available from JavaScript and for our gui code

The default way of getting a manager for GUI use has become to have a method on the Activator of the plugin containing said manager and here is what is needed for that. Once again let's have a look at some example code:

 public class Activator extends AbstractUIPlugin {
 
   // The plug-in ID
   public static final String PLUGIN_ID = "net.bioclipse.ui.business";
  
   // The shared instance
   private static Activator plugin;
 
   private ServiceTracker finderTracker;
   private ServiceTracker jsFinderTracker;
 
   /**
   * The constructor
   */
   public Activator() {
   }
 
   public void start(BundleContext context) throws Exception {           
     super.start(context);                                               
     plugin = this;                                                      
                                                                         
     finderTracker = new ServiceTracker( context,                        
                                         IUIManager.class.getName(),     
                                         null );                         
     finderTracker.open();                                               
                                                                         
     jsFinderTracker = new ServiceTracker( context,                      
                                           IJSUIManager.class.getName(), 
                                           null );                       
     jsFinderTracker.open();                                             
   }                                                                     
 
   public void stop(BundleContext context) throws Exception {
     plugin = null;
     super.stop(context);
   }
 
   /**
    * Returns the shared instance
    *
    * @return the shared instance
    */
    public static Activator getDefault() {
      return plugin;
    }
 
   public IUIManager getUIManager() {                                                 
     IUIManager uiManager;                                                            
                                                                                      
     try {                                                                            
       uiManager                                                                      
         = (IUIManager) finderTracker.waitForService(1000*30);                        
     }                                                                                
     catch (InterruptedException e) {                                                 
       throw new IllegalStateException("Could not get js console manager", e);        
     }                                                                                
     if (uiManager == null) {                                                         
       throw new IllegalStateException("Could not get js console manager");           
     }                                                                                
     return uiManager;                                                                
   }                                                                                  
 
   public IUIManager getJSUIManager() {                                            
     IJSUIManager jsuiManager;                                                     
                                                                                   
     try {                                                                         
       jsuiManager                                                                 
         = (IJSUIManager) jsFinderTracker.waitForService(1000*30);                 
     }                                                                             
     catch (InterruptedException e) {                                              
       throw                                                                       
         new IllegalStateException("Could not get js console manager", e);         
     }                                                                             
     if (jsuiManager == null) {                                                    
       throw new IllegalStateException("Could not get js console manager");        
     }                                                                             
     return jsuiManager;                                                           
   }                                                                               
 }

The start method

In order to get our manager objects we need to fetch them from the OSGI container. This is done with service trackers which are initialized in the start method.

The getUIManager() and the getJSUIManager() method

In the get methods we ask the service trackers to get the manager objects from the OSGI container. Note that we also give them a time out for how long they should wait before giving up since we can't be sure that the managers actually are in the OSGI container. The exception thrown in this methods are often seen in stack traces when something with the Spring configuration or for example the order plugins are started is wrong. What it means is just that the manager object asked for was not to be found in the OSGI container. Exactly why that was so is not always so easy to figure out though...

 public class UIManagerFactory implements IExecutableExtension, 
                                          IExecutableExtensionFactory {
 
   private Object jsConsoleManager;
   
   public void setInitializationData( IConfigurationElement config,       
                                      String propertyName,                
                                      Object data) throws CoreException { 
                                                                          
     jsConsoleManager = Activator.getDefault().getJSUIManager();          
   }                                                                      
   
   public Object create() throws CoreException { 
     return jsConsoleManager;                    
   }                                                      
 }

The ManagerFactory is used for getting the manager into JavaScript

Managers that are given to the extension point named net.bioclipse.core.scriptingContribution are injected into JavaScript. In order to give a manager to the extension point we use a factory method. In the initialization method it fetches the manager object and the create method it simply returns that object. So we give an instance of the manager factory to the extension point and when it needs the manager it will get it through the create method.

Manager best practices

  • If your manager method ends with creating a file, do not return void but rather a String with the workspace-relative path to the file. This greatly simplifies for people writing scripts.
  • If you return a List<? extends IBioObject>, e.g. List<IMolecule>, return a BioList implementation and not an ArrayList. BioList makes the list available for recording and also allows it to be opened in ui.open(myBioList).
  • Write methods to simplify for users. If you think users may want to operate on one molecule and on a list of molecules, write two methods! It is not good practice to force users to use a list if they only want to input a single molecule.
  • Use List<> for input parameters, e.g. use List<IMolecule> and not IMolecule[]

A few last general tips

A good tip is to make your managers stateless, i.e. without non-final instance variables. A stateless class is much easier to reason about when several threads are working on it, as might happen to any manager class. In the same vein, all public methods should be synchronized in a manager class.

If you are working with Resources, you should consider using Eclipse's jobs functionality.


See also: Building a recordable plugin