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);
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. |