ignore

Adding Custom Action Bean Annotations

Custom action bean annotations are a powerful feature that are used in a few places in Strecks. Action bean annotations allow you to extend the contract between the action bean and the action controller without changing the interface which the action bean and the action controller use to communicate with each other. A good example is in allowing different forms of navigation to be used with the same controller.

Adding a custom action bean annotations is a bit more complex than adding other forms of annotations, such as validation, data binding and dependency injection. The best way to see how it works is to show an existing working example. The example we use for this is the @DispatchMethod annotation, which by the LookupDispatchActionController implementations when implementing Streck's version of LookupDispatchAction. If you are not familiar with LookupDispatchAction, its purpose is to allow individual buttons in a multi-button form to be mapped to methods within an action class.

The basic mechanisms behind custom action bean annotations are as follows:

All of this may sound somewhat abstract, so our concrete example should help to clarify.

The Action Bean

We mentioned that the action bean identifies its collaborating action controller. The code below shows an example usage of the @DispatchMethod annotation.

@Controller(name = BasicLookupDispatchController.class)
public class ExampleBasicLookupSubmitAction implements BasicSubmitAction
{
    public void preBind()
    {
    }

    public String execute()
    {
      //code omitted
        return "success";
    }

    @DispatchMethod(key = "button.add")
    public String insert()
    {
      //code omitted
        return "success";
    }

    @DispatchMethod(key = "button.delete")
    public String delete()
    {
      //code omitted
        return "success";
    }

    public String cancel()
    {
      //code omitted
        return "success";
    }
}

The @DispatchMethod is used to denote the resource key which will be mapped to the annotated method. In Struts's LookupDispatchAction, this role is performed by a implementing a method returning a Map containing this mapping. Notice how ExampleBasicLookupSubmitAction identifies BasicLookupDispatchController as the action controller.

The Action Bean Annotation Reader

ActionBeanAnnotationReader represents functionality for extracting some information from annotations contained within an action bean, and defines two methods:

public interface ActionBeanAnnotationReader<T extends Object>
{
    public boolean readAnnotations(Class actionBeanClass);
    public void populateController(T controller);
}

The job of the first method is to read the annotations in of an action bean Class instance, while populateController() is the method used to transfer the extracted information to the controller.

The class which implements the logic for extracting message key to method name mappings from the @DispatchMethod annotation is DispatchMethodLookupReader, which is shown in essence below:

public class DispatchMethodLookupReader 
 	implements ActionBeanAnnotationReader<LookupDispatchActionController>
{
    private Map<String, String> keyMethodMap = new HashMap<String, String>();

    public boolean readAnnotations(Class actionBeanClass)
    {
        Method[] methods = actionBeanClass.getMethods();

        boolean found = false;
        for (Method method : methods)
        {
            DispatchMethod annotation = method.getAnnotation(DispatchMethod.class);
            if (annotation != null)
            {
                String key = annotation.key();
                String methodName = method.getName();
                keyMethodMap.put(key, methodName);
                found = true;
            }
        }
        return found;
    }

    public void populateController(LookupDispatchActionController controller)
    {
        controller.setKeyMethodMap(Collections.unmodifiableMap(keyMethodMap));
    }
}

The implementation is pretty straightforward - readAnnotations() simply builds the key to method map which in plain Struts would be returned via a concrete method implementation. The return value of readAnnotations() is used to indicate whether the annotation reader found the annotations it was looking for. populateController() is then used to pass the keyMethodMap on to the controller, which clearly needs to be implement the LookupDispatchActionController interface.

The Action Controller

Our action controller has two requirements. It needs some way of configuring which action bean annotation readers should be used to inspect the collaborating action bean's annotations, and also must have a way to receive the information extracted. To these ends, there are two important things to notice about the implementation of BasicLookupDispatchController, shown below:

@ActionInterface(name = BasicSubmitAction.class)
@ReadDispatchLookups
public class BasicLookupDispatchController 
	extends BasicDispatchController 
	implements LookupDispatchActionController
{
    private Map<String, String> keyMethodMap;

    public void setKeyMethodMap(Map<String, String> keyMethodMap)
    {
        this.keyMethodMap = keyMethodMap;
    }

    @Override
    protected ViewAdapter executeAction(Object actionBean, ActionContext context)
    {
      //implementation omitted
    }
    
    //rest of class omitted
}

First, the controller action needs to implement an interface which the action bean annotation reader can use to configure the controller action. As we have seen, the interface is LookupDispatchActionController, which has the following form:

public interface LookupDispatchActionController extends ControllerAction
{
    void setKeyMethodMap(Map<String, String> keyMethodMap);
}

The purpose of this interface is to allow the action controller to be populated with the message key to method name mapping read using the @DispatchMethod annotations.

Second, the action controller needs a way of letting Strecks know which ActionBeanAnnotationReaders should scan for annotations. The mechanism is very similar to the Annotation Factory pattern used for identifying data binding and conversion annotations. It is applied through the use of @ReadDispatchLookups to annotate the action controller class definition. The implementation of @ReadDispatchLookups is shown below:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@BeanAnnotationReader(DispatchMethodLookupReader.class)
public @interface ReadDispatchLookups
{
}

The key is in the use of the @BeanAnnotationReader annotation. The role of this annotation is simply to define an ActionBeanAnnotationReader implementation to use to extract action bean metadata. In other words, by using the @ReadDispatchLookups in our action controller, we are saying "use the class @DispatchMethodLookupReader to find annotations and extract information from the action bean, passing this on to the action controller instance".

More Complex Scenarios

The example we have used so far is quite straightforward in that the ActionBeanAnnotationReader simply looks for occurrences of a single annotation. More advanced functionality can be supported by applying the Annotation Factory Pattern to the ActionBeanAnnotationReader implementation. Here, the ActionBeanAnnotationReader is not looking for a particular annotation, but for annotations which themselves are annotated with a factory annotation, that is, an annotation which can be used to identify a factory class which understands how to process the annotation. This mechanism is applied to support pluggable navigation, as well as supporting action bean source configuration (which allows, for example, action beans to be retrieved as Spring beans).

For action bean source configuration, the ActionBeanAnnotationReader implementation is class is BeanSourceAnnotationReader. The essence of this class's readAnnotations() method is shown below:

public boolean readAnnotations(Class actionClass)
{
    Annotation[] annotations = actionClass.getAnnotations();

    boolean found = false;

    for (Annotation annotation : annotations)
    {
        Class<? extends Annotation> annotationType = 
          annotation.annotationType();
        BeanSourceReaderClass beanSourceReaderClass = 
          annotationType.getAnnotation(BeanSourceReaderClass.class);

        if (beanSourceReaderClass != null)
        {
            BeanSourceReader beanSourceReader = 
            	ReflectHelper.createInstance(beanSourceReaderClass.value(),
                    BeanSourceReader.class);
            beanSource = beanSourceReader.readBeanSource(actionClass, annotation);

            found = true;
        }
    }

    return found;
    }
}

Notice how BeanSourceAnnotationReader is not reading the annotations itself, but instead delegates this task to a BeanSourceReader instance. Each BeanSourceReader implementation would look for a particular annotation or set of annotations. For example, Spring-managed action beans are configured using the class SpringBeanSourceReader, shown below:

public class SpringBeanSourceReader implements BeanSourceReader
{
    public ActionBeanSource readBeanSource(Class actionClass, Annotation annotation)
    {
        Assert.notNull(actionClass);
        Assert.notNull(annotation);

        SpringBean actionBean = (SpringBean) annotation;
        return new SpringActionBeanSource(actionClass, actionBean.name());
    }
}

Pluggable navigation works in a similar way. A navigation annotation uses a NavigationReader instance to find particular navigation-related annotations. For example, @NavigateForward annotations use a are read using an instance of NavigateReader, which implements NavigationReader.

We can see this is the case by looking at the implementation of the @NavigateForward annotation itself.

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@NavigationInfo(value = NavigateReader.class)
public @interface NavigateForward
{
    Class<? extends NavigationMarker> handler() 
    	default DefaultNavigationHandlerFactory.class;
}

Here, @NavigationInfo is the factory annotation used to identify the NavigationReader implemention class used to read the annotation.

SourceForge.net logo java.net Member logo Copyright © 2005-2007 Realsolve Solutions