Tuesday, May 11, 2010

Custom annotations: UiFacesComponentProperties

I was recently asked on the Metawidget forum whether we could have a @UiFacesComponentProperties annotation, so that you could fine-tune certain properties of a JSF component from within your business object.

In my opinion, this ties the business object a little bit 'too close' to the UI. However I realise that's a very subjective thing. So while Metawidget does not offer this 'out of the box', you can implement it quite easily. Here's how.

The Annotation

First, define the annotation...

package com.myapp;

import java.lang.annotation.*;

/**
 * Array of name/value pairs of arbitrary UIComponent properties.
 */

@Retention( RetentionPolicy.RUNTIME )
@Target( ElementType.METHOD )
public @interface UiFacesComponentProperties {
   String[] value();
}

...which you can use like this...

@UiFacesLookup( "#{feeType.summary}" )
@UiFacesComponentProperties( { "layout", "pageDirection" } )
public Set<FeeType> getExcludeFees() {
   return mExcludeFees;
}

The Inspector

Next, define an Inspector to detect this annotation and turn it into a Metawidget attribute...

package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;
import java.util.Map;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.impl.propertystyle.*;
import org.metawidget.util.*;
import com.myapp.UiFacesComponentProperties;

public class MyAnnotationInspector
   extends BaseObjectInspector {

   public CoreAnnotationInspector() {
      this( new BaseObjectInspectorConfig() );
   }

   @Override
   protected Map<String, String> inspectProperty( Property property )
      throws Exception {
      Map<String, String> mapAttributes = CollectionUtils.newHashMap();

      UiFacesComponentProperties componentProperties = property.getAnnotation( UiFacesComponentProperties.class );

      if ( componentProperties != null )
         mapAttributes.put( "faces-component-properties", ArrayUtils.toString( componentProperties.value() ) );

   }
}

...and add it into your CompositeInspector chain.

The WidgetProcessor

Finally, declare a WidgetProcessor to act on the Metawidget attribute...

package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;
import static org.metawidget.inspector.propertytype.PropertyTypeInspectionResultConstants.*;
import java.lang.reflect.Method;
import java.util.*;
import javax.el.ValueExpression;
import javax.faces.component.*;

import org.metawidget.faces.component.*;
import org.metawidget.util.*;
import org.metawidget.widgetprocessor.iface.WidgetProcessor;

public class MyWidgetProcessor
   implements WidgetProcessor {

   @Override
   public UIComponent processWidget( UIComponent component, String elementName, Map<String, String> attributes, UIMetawidget metawidget ) {

      List<String> listComponentProperties = CollectionUtils.fromString( attributes.get( "faces-component-properties" ));

      for ( int loop = 0, length = listComponentProperties.size(); loop < length; loop += 2 ) {
            ClassUtils.setProperty( component, listComponentProperties.get( loop ), listComponentProperties.get( loop + 1 ) );
      }

   }
}

...and add both into your WidgetProcessor list in WEB-INF/metawidget.xml:

<htmlMetawidget xmlns="java:org.metawidget.faces.component.html">
   <inspector>
      <compositeInspector xmlns="java:org.metawidget.inspector.composite" config="CompositeInspectorConfig">
         <inspectors>
            <array>
               ...
               <myAnnotationInspector xmlns="java:com.myapp"/>
            </array>
         </inspectors>
      </compositeInspector>
   </inspector>
   ...
   <widgetProcessors>
         <array>
            ...
            <myWidgetProcessor xmlns="java:com.myapp"/>
         </array>
   </widgetProcessors>

Note this version will only work with String properties, and you might want to disable it for read-only scenarios (TRUE.equals(attributes.get(READ_ONLY))), but I'll leave it simple for now.

Feedback welcome!

0 comments: