27.3. Custom action factories

Some times the factories shipped with BASE are not enough, and you may want to provide your own factory implementation. In this case you will have to create a class that implements the ActionFactory interface. Here is a very simple example that does the same as the previous "Hello world" example.


package net.sf.basedb.examples.extensions;

import net.sf.basedb.clients.web.extensions.JspContext;
import net.sf.basedb.clients.web.extensions.menu.MenuItemAction;
import net.sf.basedb.clients.web.extensions.menu.MenuItemBean;
import net.sf.basedb.util.extensions.ActionFactory;
import net.sf.basedb.util.extensions.InvokationContext;

/**
   First example of an action factory where eveything is hardcoded.
   @author nicklas
*/
public class HelloWorldFactory
   implements ActionFactory<MenuItemAction>
{

   private MenuItemAction[] helloWorld;

   // A public, no-argument constructor is required
   public HelloWorldFactory()
   {
      helloWorld = new MenuItemAction[1];
   }
   
   // Return true enable the extension, false to disable it
   public boolean prepareContext(
      InvokationContext<? super MenuItemAction> context) 
   {
      return true;
   }

   // An extension may create one or more actions
   public MenuItemAction[] getActions(
      InvokationContext<? super MenuItemAction> context) 
   {
      // This cast is always safe with the web client
      JspContext jspContext = (JspContext)context.getClientContext();
      if (helloWorld[0] == null)
      {
         MenuItemBean bean = new MenuItemBean();
         bean.setTitle("Hello factory world!");
         bean.setIcon(jspContext.getRoot() + "/images/info.gif");
         bean.setOnClick("alert('Hello factory world!')");
         helloWorld[0] = bean;
      }
      return helloWorld;
   }
}

And here is the XML configuration file that goes with it.


<?xml version="1.0" encoding="UTF-8" ?>
<extensions xmlns="http://base.thep.lu.se/extensions.xsd">
   <extension
      id="net.sf.basedb.clients.web.menu.extensions.helloworldfactory"
      extends="net.sf.basedb.clients.web.menu.extensions"
      >
      <index>2</index>
      <about>
         <name>Hello factory world</name>
         <description>
            A "Hello world" variant with a custom action factory.
            Everything is hard-coded into the factory.
         </description>
      </about>
      <action-factory>
         <factory-class>
            net.sf.basedb.examples.extensions.HelloWorldFactory
         </factory-class>
      </action-factory>
   </extension>
</extensions>

To install this extension you need to put the compiled HelloWorldFactory.class and the XML file inside a JAR file. The XML file must be located at META-INF/extensions.xml and the class file at net/sf/basedb/examples/extensions/HelloWorldFactory.class.

The above example is a bit artificial and we have not gained anything. Instead, we have lost the ability to easily change the menu since everything is now hardcoded into the factory. To change, for example the title, requires that we recompile the java code. It would be more useful if we could make the factory configurable with parameters. The next example will make the icon and message configurable, and also include the name of the currently logged in user. For example: "Greetings <name of logged in user>!".


package net.sf.basedb.examples.extensions;

import net.sf.basedb.clients.web.extensions.AbstractJspActionFactory;
import net.sf.basedb.clients.web.extensions.menu.MenuItemAction;
import net.sf.basedb.clients.web.extensions.menu.MenuItemBean;
import net.sf.basedb.core.DbControl;
import net.sf.basedb.core.SessionControl;
import net.sf.basedb.core.User;
import net.sf.basedb.util.extensions.ClientContext;
import net.sf.basedb.util.extensions.InvokationContext;
import net.sf.basedb.util.extensions.xml.PathSetter;
import net.sf.basedb.util.extensions.xml.VariableSetter;

/**
   Example menu item factory that creates a "Hello world" menu item
   where the "Hello" part can be changed by the "prefix" setting in the
   XML file, and the "world" part is dynamically replaced with the name
   of the logged in user.
   
   @author nicklas
*/
public class HelloUserFactory 
   extends AbstractJspActionFactory<MenuItemAction>
{
   // To store the URL to the icon
   private String icon;
   
   // The default prefix is Hello
   private String prefix = "Hello";
   
   // A public, no-argument constructor is required
   public HelloUserFactory()
   {}
   
   /**
      Creates a menu item that displays: {prefix} {name of user}!
   */
   public MenuItemAction[] getActions( 
      InvokationContext<? super MenuItemAction> context) 
   {
      String userName = getUserName(context.getClientContext());
      MenuItemBean helloUser = new MenuItemBean();
      helloUser.setTitle(prefix + " " + userName + "!");
      helloUser.setIcon(icon);
      helloUser.setOnClick("alert('" + prefix + " " + userName + "!')");
      return new MenuItemAction[] { helloUser };
   }

   /**
      Get the name of the logged in user.
   */
   private String getUserName(ClientContext context)
   {
      SessionControl sc = context.getSessionControl();
      DbControl dc = context.getDbControl();
      User current = User.getById(dc, sc.getLoggedInUserId());
      return current.getName();
   }
   
   /**
      Sets the icon to use. Path conversion is enabled.
   */
   @VariableSetter
   @PathSetter
   public void setIcon(String icon)
   {
      this.icon = icon;
   }
   
   /**
      Sets the prefix to use. If not set, the
      default value is "Hello".
   */
   public void setPrefix(String prefix)
   {
      this.prefix = prefix == null ? "Hello" : prefix;
   }
}

The are two new parts in this factory. The first is the getUserName() method which is called from getActions(). Note that the getActions() method always create a new MenuItemBean. It can no longer be cached since the title and javascript code depends on which user is logged in.

The second new part is the setIcon() and setPrefix() methods. The extensions system uses java reflection to find the existance of the methods if <icon> and/or <prefix> tags are present in the <parameters> tag for a factory, the methods are automatically called with the value inside the tag as it's argument.

The VariableSetter and PathSetter annotations on the setIcon() are used to enable "smart" convertions of the value. Note that in the XML file you only have to specify /images/info.gif as the URL to the icon, but in the hardcoded factory you have to do: jspContext.getRoot() + "/images/info.gif". In this case, it is the PathSetter which automatically adds the the JSP root directory to all URL:s starting with /. The VariableSetter can do the same thing but you would have to use $ROOT$ instead. Eg. $ROOT$/images/info.gif. The PathSetter only looks at the first characteer, while the VariableSetter looks in the entire string.

Here is an example of an extension configuration that can be used with the new factory.


<extensions xmlns="http://base.thep.lu.se/extensions.xsd">
   <extension
      id="net.sf.basedb.clients.web.menu.extensions.hellouser"
      extends="net.sf.basedb.clients.web.menu.extensions"
      >
      <index>3</index>
      <about>
         <name>Greetings user</name>
         <description>
            A "Hello world" variant with a custom action factory
            that displays "Greetings {name of user}" instead. We also
            make the icon configurable.
         </description>
      </about>
      <action-factory>
         <factory-class>
            net.sf.basedb.examples.extensions.HelloUserFactory
         </factory-class>
         <parameters>
            <prefix>Greetings</prefix>
            <icon>/images/take_ownership.png</icon>
         </parameters>
      </action-factory>
   </extension>
</extensions>

[Note] Be aware of multi-threading issues

When you are creating custom action and renderer factories be aware that multiple threads may use a single factory instance at the same time. Action and renderer objects only needs to be thread-safe if the factories re-use the same objects.