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) 
   {
      JspContext jspContext = (JspContext)context.getClientContext();
      String home = jspContext.getHome(context.getExtension());
      jspContext.addScript(home + "/hello.js");
      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.setId("hello-factory");
         bean.setTitle("Hello factory world!");
         bean.setIcon(jspContext.getRoot() + "/images/info.gif");
         helloWorld[0] = bean;
      }
      return helloWorld;
   }
}

And here are the XML and JavaScript files 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>


var HelloWorldFactory = function()  
{  
   var hello = {};  
    
    /** 
       Executed once when the page is loaded. Typically 
       used to bind events to fixed control elements. 
    */  
    hello.initMenuItems = function()  
    {  
       // Bind event handlers the menu items.   
       // First parameter is the ID of the menu item  
       // Second parameter is the event to react to (=click)  
       // Last parameter is the function to execute  
       Events.addEventHandler('hello-factory', 'click', hello.helloFactoryWorld);  
    }  
      
    // Show 'Hello factory world' message
    hello.helloFactoryWorld = function(event)  
    {  
       alert('Hello factory world');
    }  
     
    return hello;  
}();  
      
//Register the page initializer method with the BASE core  
Doc.onLoad(HelloWorldFactory.initMenuItems);  

To install this extension you need to put the compiled HelloWorldFactory.class, the XML and JavaScript files inside a JAR file. The XML file must be located at META-INF/extensions.xml the JavScript file at resources/hello.js, 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>!". We'll also get rid of the onclick event handler and use proper event binding using javascript.


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>
{
   // The ID attribute of the <div> tag in the final HTML
   private String id;
 
   // 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.setId(id);
      helloUser.setTitle(prefix + " " + userName + "!");
      helloUser.setIcon(icon);
      // Use 'dynamic' attributes for extra info that needs to be included
      // in the HTML
      setParameter("data-user-name", userName);
      setParameter("data-prefix", prefix);
      helloUser.setDynamicActionAttributesSource(this);
      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();
   }
   
   	/**
      Set the ID to use for the <div> tag. This is needed
      so that we can attach a 'click' handler to the menu item
      with JavaScript.
   */
   public void setId(String id)
   {
      this.id = id;
   }
   
   /**
      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 several 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 setId(), setIcon() and setPrefix() methods. The extensions system uses java reflection to find the existance of the methods if <id>, <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.png as the URL to the icon, but in the hardcoded factory you have to do: jspContext.getRoot() + "/images/info.png". 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.png. The PathSetter only looks at the first characteer, while the VariableSetter looks in the entire string.

The third new part is the use of dynamic action attributes. These are extra attributes that have not been defined by the Action interface. To set a dynamic attribute we call the setParameter() method and then MenuItemBean.setDynamicActionAttributesSource() The dynamic attributes are a simple way to output data to the HTML that is later needed by scripts. In this case, the generated HTML may look something like this:


<div id="greetings-user" data-user-name="..." data-prefix="Greetings" ... >
...
</div>

Here is an example of an extension configuration that can be used with the new factory. Notice that the <about> tag now include safe-scripts="1" attribute. This is a way for the devloper to tell the extensions installation wizard that the extensions doesn't use any unsafe code and no warning will be displayed when installing the extensions.


<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 safe-scripts="1">
         <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>
	        <id>greetings-user</id>
            <prefix>Greetings</prefix>
            <icon>/images/take_ownership.png</icon>
            <script>~/scripts/menu-items.js</script>
         </parameters>
      </action-factory>
   </extension>
</extensions>

And the menu-items.js JavaScript file:


var HelloWorldMenu = function()
{
   var menu = {};

   /**
      Executed once when the page is loaded. Typically
      used to bind events to fixed control elements.
   */
   menu.initMenuItems = function()
   {
      // Bind event handlers the menu items. 
      // First parameter is the ID of the menu item
      // Second parameter is the event to react to (=click)
      // Last parameter is the function to execute
      Events.addEventHandler('greetings-user', 'click', menu.greetingsUser);
   }

   // Get the dynamic attributes defined in extensions.xml 
   // and generate an alert message
   menu.greetingsUser = function(event)
   {
      var userName = Data.get(event.currentTarget, 'user-name');
      var prefix = Data.get(event.currentTarget, 'prefix');
      alert(prefix + ' ' + userName + '!');
   }

   return menu;
}();

//Register the page initializer method with the BASE core
Doc.onLoad(HelloWorldMenu.initMenuItems);

[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.