Saturday, July 31, 2010

Customizing Which Form Fields Are Displayed: Part 8

Following on from parts 1, 2, 3, 4, 5, 6 and 7, I thought I'd blog some of the other field ordering preferences I've encountered.

This time I want to look at another 'universal' ordering. Like the JavassistPropertyStyle one, this one orders fields based on the way they are declared in the source code. However this time we're going to use a Java 6 annotation processor to statically pre-generate a helper class that stores the field order. The generated helper class will look like this:

package com.myapp;

class Person_FieldOrder {

   public final static String[] FIELD_ORDER = new String[] { "name", "age", "retired", "notes" };
}

We'll then add a custom FieldOrderPropertyStyle to lookup this helper class and order the fields. Example implementation below.

First a Person class. We'll use a top-level class this time, rather than an inner class, because that way our annotation processor doesn't have to search inner classes:

package com.myapp;

import org.metawidget.inspector.annotation.UiLarge;

public class Person {

   public String name;

   public int age;

   public boolean retired;

   @UiLarge
   public String notes;
}

Next the meat of the example: the annotation processor. Despite their name, annotation processors don't require an annotation to process! By declaring our processor as @SupportedAnnotationTypes( "*" ) we get to inspect every type as it passes through the javac compiler. Then it's just a case of 'walking the tree' and generating a source file containing each type's fields in order:

package com.myapp;

import java.io.*;
import java.util.*;

import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.util.*;
import javax.tools.Diagnostic.*;

@SupportedAnnotationTypes( "*" )
@SupportedSourceVersion( SourceVersion.RELEASE_6 )
public class FieldOrderProcessor
   extends AbstractProcessor {

   @Override
   public boolean process( Set<? extends TypeElement> annotations, RoundEnvironment roundEnv ) {

      // For each public type...

      try {
         for ( TypeElement typeElement : ElementFilter.typesIn( roundEnv.getRootElements() ) ) {

            if ( !typeElement.getModifiers().contains( Modifier.PUBLIC ) ) {
               continue;
            }

            // ...if the type has fields...

            List<VariableElement> fields = ElementFilter.fieldsIn( typeElement.getEnclosedElements() );

            if ( fields.isEmpty() ) {
               continue;
            }

            // ...start a new source file..

            String qualifiedName = processingEnv.getElementUtils().getBinaryName( typeElement ).toString();
            int lastDot = qualifiedName.lastIndexOf( '.' );
            String packageName = qualifiedName.substring( 0, lastDot );
            String simpleName = qualifiedName.substring( lastDot + 1, qualifiedName.length() );
            simpleName += "_FieldOrder";
            PrintWriter writer = new PrintWriter( processingEnv.getFiler().createSourceFile( simpleName ).openWriter() );

            try {
               writer.write( "package " );
               writer.write( packageName );
               writer.write( "; class " );
               writer.write( simpleName );
               writer.write( " { public final static String[] FIELD_ORDER = new String[] { " );

               // ...write all its public fields in order...

               boolean first = true;

               for ( VariableElement fieldElement : fields ) {

                  if ( !fieldElement.getModifiers().contains( Modifier.PUBLIC ) ) {
                     continue;
                  }

                  if ( first ) {
                     first = false;
                  } else {
                     writer.write( ", " );
                  }

                  writer.write( "\"" );
                  writer.write( fieldElement.getSimpleName().toString() );
                  writer.write( "\"" );
               }

               // ...and close it

               writer.write( " }; }" );
            } finally {
               writer.close();
            }

            processingEnv.getMessager().printMessage( Kind.NOTE, getClass().getSimpleName() + " generated " + packageName + "." + simpleName );
         }
      } catch ( IOException e ) {
         processingEnv.getMessager().printMessage( Kind.ERROR, e.getMessage() );
      }

      return false;
   }
}

Phew! That's the hard bit over. Now we need a custom PropertyStyle to recognize these new xxx_FieldOrder classes. It extends JavaBeanPropertyStyle and is very similar to how JavassistPropertyStyle is implemented internally:

package com.myapp;

import java.util.Map;

import org.metawidget.inspector.iface.*;
import org.metawidget.inspector.impl.propertystyle.*;
import org.metawidget.inspector.impl.propertystyle.javabean.*;
import org.metawidget.util.*;

public class FieldOrderPropertyStyle
   extends JavaBeanPropertyStyle {

   @Override
   protected Map<String, Property> inspectProperties( Class<?> clazz ) {

      try {
         // For each set of JavaBean properties...

         Map<String, Property> properties = super.inspectProperties( clazz );

         // ...look up our annotation-processor-created class...

         Class<?> fieldOrderClass = Class.forName( clazz.getName() + "_FieldOrder" );
         String[] fieldOrders = (String[]) fieldOrderClass.getField( "FIELD_ORDER" ).get( null );

         // ...and sort them

         Map<String, Property> sortedProperties = CollectionUtils.newLinkedHashMap();

         for ( String fieldOrder : fieldOrders ) {
            Property property = properties.get( fieldOrder );

            if ( property != null ) {
               sortedProperties.put( fieldOrder, property );
            }
         }

         return sortedProperties;
      } catch ( Exception e ) {
         throw InspectorException.newException( e );
      }
   }
}

Finally we put it all together into a Swing app. This is very similar to the one in part 7, except using FieldOrderPropertyStyle instead of JavassistPropertyStyle:

package com.myapp;

import javax.swing.JFrame;

import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.SwingMetawidget;

public class Main {

   public static void main( String[] args ) {

      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      BaseObjectInspectorConfig config = new BaseObjectInspectorConfig().setPropertyStyle( new FieldOrderPropertyStyle() );
      metawidget.setInspector( new CompositeInspector( new CompositeInspectorConfig().setInspectors(
            new PropertyTypeInspector( config ),
            new MetawidgetAnnotationInspector( config ) ) ) );
      metawidget.setToInspect( person );

      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }
}

Now to run it! This is trickier than normal, because you have to pre-compile the annotation processor then compile the rest of the code whilst applying that annotation processor. In case your IDE doesn't support this sort of thing, here's an Ant script:

<project name="field-order-processor" default="pack">

   <property name="builddir" value="./build"/>
   <property name="classpath" value="${builddir};/metawidget-0.99/metawidget.jar"/>
      
   <target name="pack">
      
      <delete dir="${builddir}"/>
      <mkdir dir="${builddir}"/>
      
      <javac srcdir="src" destdir="${builddir}">
         <include name="**/*Processor.java"/>
      </javac>

      <javac srcdir="src" destdir="${builddir}" classpath="${classpath}" target="1.6">
         <compilerarg value="-processor"/>
         <compilerarg value="com.myapp.FieldOrderProcessor"/>
      </javac>
      
      <java classname="com.myapp.Main" classpath="${classpath}" fork="yes"/>
      
   </target>
   
</project>

Monday, July 26, 2010

Dynamic Form Generator: Metawidget v0.99

Version 0.99 of Metawidget the dynamic form generator is now available, focused on tightening the codebase ready for our v1.0 release! This release includes the following enhancements:
Special thanks to Bernhard Huber and Stuart Douglas for their help with this release!

As always, the best place to start is the Reference Documentation:

http://metawidget.org/doc/reference/en/pdf/metawidget.pdf

Your continued feedback is invaluable to us. Please download it and let us know what you think.

Breaking Changes

There have, unfortunately, been some minor breaking changes with this release compared to v0.95. Specifically:

  • The InspectionResultProcessor interface now works with Strings, not DOM Documents, to maximize the number of scenarios it is applicable to. The new DomInspectionResultProcessor interface can be used to work with Documents

Customizing Which Form Fields Are Displayed: Part 7

Following on from parts 1, 2, 3, 4, 5 and 6, I thought I'd blog some of the other field ordering preferences I've encountered.

This time I want to look at 'universal' ordering that sits not just above per-screen, but also above per-domain. An obvious one, though rarely useful, is alphabetical ordering. This is what JavaBeanPropertyStyle implements by default. Another interesting one is 'source code line number' ordering. Source code line numbers are not usually available in JVM class files, but if you have a technology like Javassist on your classpath you can plug that into Metawidget to get field ordering for free!

Example implementation below. Some points to note:
  • It's in Swing so you can just cut and paste and run it

  • It uses JavassistPropertyStyle (no annotations, Inspectors or InspectionResultProcessors this time!)

  • You must have Javassist on your classpath

  • You must compile your code in debug mode so that line numbering is included
package com.myapp;

import javax.swing.*;

import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.impl.propertystyle.javassist.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.*;

public class Main {

   public static void main( String[] args ) {

      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      BaseObjectInspectorConfig config = new BaseObjectInspectorConfig().setPropertyStyle( new JavassistPropertyStyle() );
      metawidget.setInspector( new CompositeInspector( new CompositeInspectorConfig()
         .setInspectors(
               new PropertyTypeInspector( config ),
               new MetawidgetAnnotationInspector( config )
         )));
      metawidget.setToInspect( person );

      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   static class Person {

      private String mName;
      private int mAge;
      private boolean mRetired;
      private String mNotes;

      public String getName() {
         return mName;
      }

      public void setName( String name ) {
         mName = name;
      }

      public int getAge() {
         return mAge;
      }

      public void setAge( int age ) {
         mAge = age;
      }

      public boolean isRetired() {
         return mRetired;
      }

      public void setRetired( boolean retired ) {
         mRetired = retired;
      }

      @UiLarge
      public String getNotes() {
         return mNotes;
      }

      public void setNotes( String notes ) {
         mNotes = notes;
      }
   }
}

Tuesday, July 20, 2010

Customizing Which Form Fields Are Displayed: Part 6

Following on from parts 1, 2, 3, 4 and 5, I thought I'd blog some of the other field ordering preferences I've encountered.

This one is for those who want field ordering at the global (ie. application) level, but to exclude fields on a per-screen basis. Example implementation below. Some points to note:
  • It's in Swing so you can just cut and paste and run it

  • It defines an ExcludingInspectionResultProcessor that excludes the properties/actions

  • It uses this in a chain with the usual ComesAfterInspectonResultProcessor. Note that unlike Part 2 it adds to the chain after ComesAfterInspectonResultProcessor. This is so that excluding fields does not impact the UiComesAfter ordering. You may prefer this the other way around
package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;

import javax.swing.*;

import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.inspectionresultprocessor.sort.*;
import org.metawidget.inspector.annotation.*;
import org.metawidget.swing.*;
import org.metawidget.util.*;
import org.w3c.dom.*;

public class Main {

   public static void main( String[] args ) {

      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.addInspectionResultProcessor( new ComesAfterInspectionResultProcessor<SwingMetawidget>() );
      metawidget.addInspectionResultProcessor( new ExcludingInspectionResultProcessor() );
      metawidget.putClientProperty( "exclude", new String[] { "retired", "age" } );
      metawidget.setToInspect( person );

      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   static class Person {

      public String   name;

      @UiComesAfter( "name" )
      public int      age;

      @UiComesAfter( "age" )
      public boolean   retired;

      @UiComesAfter( "retired" )
      @UiLarge
      public String   notes;
   }

   static class ExcludingInspectionResultProcessor
      implements InspectionResultProcessor<SwingMetawidget> {

      public String processInspectionResult( String inspectionResult, SwingMetawidget metawidget, Object toInspect, String type, String... names ) {

         String[] excludes = (String[]) metawidget.getClientProperty( "exclude" );
         Document document = XmlUtils.documentFromString( inspectionResult );
         Element entity = (Element) document.getDocumentElement().getFirstChild();

         for ( int loop = 0; loop < entity.getChildNodes().getLength(); ) {

            Element trait = (Element) entity.getChildNodes().item( loop );

            if ( !ArrayUtils.contains( excludes, trait.getAttribute( NAME )))
            {
               loop++;
               continue;
            }

            entity.removeChild( trait );
         }

         return XmlUtils.documentToString( document, false );
      }
   }

}

Customizing Which Form Fields Are Displayed: Part 5

Following on from parts 1, 2, 3 and 4, I thought I'd blog some of the other field ordering preferences I've encountered.

This one is from the Naked Objects .ORG team, who provide (though don't necessarily recommend) support for declaring field order at the class level. Example implementation below. Some points to note:
  • It's in Swing so you can just cut and paste and run it

  • It defines a custom annotation (UiFieldOrder) and custom Inspector (FieldOrderInspector) to detect it

  • The Inspector overrides inspectEntity (ie. class-level) rather than inspectTrait (ie. field-level)
  • It defines a FieldOrderInspectionResultProcessor that sorts the properties/actions
package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;

import java.lang.annotation.*;
import java.util.*;

import javax.swing.*;

import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.*;
import org.metawidget.util.*;
import org.w3c.dom.*;

public class Main {

   public static void main( String[] args ) {

      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.setInspector( new CompositeInspector( new CompositeInspectorConfig().setInspectors(
            new PropertyTypeInspector(),
            new MetawidgetAnnotationInspector(),
            new FieldOrderInspector() ) ) );
      metawidget.addInspectionResultProcessor( new FieldOrderInspectionResultProcessor() );
      metawidget.setToInspect( person );

      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   @UiFieldOrder( { "name", "age", "retired", "notes" } )
   static class Person {

      public String   name;

      public int      age;

      public boolean   retired;

      @UiLarge
      public String   notes;
   }

   @Retention( RetentionPolicy.RUNTIME )
   @Target( { ElementType.TYPE } )
   static @interface UiFieldOrder {

      String[] value();
   }


   static class FieldOrderInspector
      extends BaseObjectInspector {

      @Override
      protected Map<String, String> inspectEntity( Class<?> declaredClass, Class<?> actualClass )
         throws Exception {

         Map<String, String> attributes = CollectionUtils.newHashMap();
         UiFieldOrder fieldOrder = actualClass.getAnnotation( UiFieldOrder.class );

         if ( fieldOrder != null ) {
            attributes.put( "field-order", ArrayUtils.toString( fieldOrder.value() ) );
         }

         return attributes;
      }
   }

   static class FieldOrderInspectionResultProcessor
      implements InspectionResultProcessor<SwingMetawidget> {

      public String processInspectionResult( String inspectionResult, SwingMetawidget metawidget, Object toInspect, String type, String... names ) {

         try {
            // Start a new document
            //
            // (Android 1.1 did not cope well with shuffling the nodes of an existing document)

            Document newDocument = XmlUtils.newDocument();
            Element newInspectionResultRoot = newDocument.createElementNS( NAMESPACE, ROOT );

            Document document = XmlUtils.documentFromString( inspectionResult );
            Element inspectionResultRoot = document.getDocumentElement();
            XmlUtils.setMapAsAttributes( newInspectionResultRoot, XmlUtils.getAttributesAsMap( inspectionResultRoot ) );
            newDocument.appendChild( newInspectionResultRoot );

            Element entity = (Element) inspectionResultRoot.getChildNodes().item( 0 );
            Element newEntity = newDocument.createElementNS( NAMESPACE, ENTITY );
            XmlUtils.setMapAsAttributes( newEntity, XmlUtils.getAttributesAsMap( entity ) );
            newInspectionResultRoot.appendChild( newEntity );

            // Look up the field-order (if any)

            String fieldOrder = entity.getAttribute( "field-order" );

            if ( fieldOrder == null ) {
               return inspectionResult;
            }

            String[] fieldOrderArray = ArrayUtils.fromString( fieldOrder );

            for ( String field : fieldOrderArray ) {
               Element trait = XmlUtils.getChildWithAttributeValue( entity, NAME, field );
               newEntity.appendChild( XmlUtils.importElement( newDocument, trait ) );
            }

            // Return the new document

            return XmlUtils.documentToString( newInspectionResultRoot.getOwnerDocument(), false );

         } catch ( Exception e ) {
            throw InspectionResultProcessorException.newException( e );
         }
      }
   }
}

Monday, July 19, 2010

Customizing Which Form Fields Are Displayed: Part 4

Following on from Part 1, Part 2 and Part 3, I thought I'd blog some of the other field ordering preferences I've encountered.

This one is from Dan Haywood, who suggests using a Dewey Decimal approach and ordering fields as 1.1, 1.2, 2.1 etc. This has advantages over a strictly numerical approach in that new fields can be inserted without reordering all the others, and also it lets superclasses interleave their fields amongst subclasses. Example implementation below. Some points to note:
  • It's in Swing so you can just cut and paste and run it

  • It defines a custom annotation (UiDeweyOrder) and custom Inspector (DeweyOrderInspector) to detect it

  • It defines a DeweyOrderInspectionResultProcessor that sorts the properties/actions

  • It defines a DeweyDecimalComparator that compares by Dewey Decimal (how come I couldn't find one of these on the Web already?)

  • It's very similar to the example in Part 3
package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;

import java.lang.annotation.*;
import java.util.*;

import javax.swing.*;

import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.*;
import org.metawidget.util.*;
import org.w3c.dom.*;

public class Main {

   public static void main( String[] args ) {

      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.setInspector( new CompositeInspector( new CompositeInspectorConfig().setInspectors(
            new PropertyTypeInspector(),
            new MetawidgetAnnotationInspector(),
            new DeweyOrderInspector() ) ) );
      metawidget.addInspectionResultProcessor( new DeweyOrderInspectionResultProcessor() );
      metawidget.setToInspect( person );

      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   static class Person {

      @UiDeweyOrder( "1.1" )
      public String   name;

      @UiDeweyOrder( "1.1.1" )
      public int      age;

      @UiDeweyOrder( "1.2.1" )
      public boolean   retired;

      @UiDeweyOrder( "2.1" )
      @UiLarge
      public String   notes;
   }

   @Retention( RetentionPolicy.RUNTIME )
   @Target( { ElementType.FIELD, ElementType.METHOD } )
   static @interface UiDeweyOrder {

      String value();
   }


   static class DeweyOrderInspector
      extends BaseObjectInspector {

      @Override
      protected Map<String, String> inspectTrait( Trait trait )
         throws Exception {

         Map<String, String> attributes = CollectionUtils.newHashMap();
         UiDeweyOrder order = trait.getAnnotation( UiDeweyOrder.class );

         if ( order != null ) {
            attributes.put( "dewey-order", order.value() );
         }

         return attributes;
      }
   }

   static class DeweyOrderInspectionResultProcessor
      implements InspectionResultProcessor<SwingMetawidget> {

      public String processInspectionResult( String inspectionResult, SwingMetawidget metawidget, Object toInspect, String type, String... names ) {

         try {
            // Start a new document
            //
            // (Android 1.1 did not cope well with shuffling the nodes of an existing document)

            Document newDocument = XmlUtils.newDocument();
            Element newInspectionResultRoot = newDocument.createElementNS( NAMESPACE, ROOT );

            Document document = XmlUtils.documentFromString( inspectionResult );
            Element inspectionResultRoot = document.getDocumentElement();
            XmlUtils.setMapAsAttributes( newInspectionResultRoot, XmlUtils.getAttributesAsMap( inspectionResultRoot ) );
            newDocument.appendChild( newInspectionResultRoot );

            Element entity = (Element) inspectionResultRoot.getChildNodes().item( 0 );
            Element newEntity = newDocument.createElementNS( NAMESPACE, ENTITY );
            XmlUtils.setMapAsAttributes( newEntity, XmlUtils.getAttributesAsMap( entity ) );
            newInspectionResultRoot.appendChild( newEntity );

            // Record all traits (ie. properties/actions) that have a dewey-order

            Map<String, Element> traitsWithOrder = new TreeMap<String, Element>( new DeweyDecimalComparator() );
            NodeList traits = entity.getChildNodes();

            for ( int loop = 0, length = traits.getLength(); loop < length; loop++ ) {
               Node node = traits.item( loop );

               if ( !( node instanceof Element ) ) {
                  continue;
               }

               Element trait = (Element) node;

               // (if no dewey-order, move them across to the new document)

               if ( !trait.hasAttribute( "dewey-order" ) ) {
                  newEntity.appendChild( XmlUtils.importElement( newDocument, trait ) );
                  continue;
               }

               traitsWithOrder.put( trait.getAttribute( "dewey-order" ), trait );
            }

            // Output the traits in TreeMap order

            for ( Element trait : traitsWithOrder.values() ) {
               newEntity.appendChild( XmlUtils.importElement( newDocument, trait ) );
            }

            // Return the new document

            return XmlUtils.documentToString( newInspectionResultRoot.getOwnerDocument(), false );

         } catch ( Exception e ) {
            throw InspectionResultProcessorException.newException( e );
         }
      }
   }

   static class DeweyDecimalComparator
      implements Comparator<String> {

      @Override
      public int compare( String order1, String order2 ) {

         // Split the Dewey-Decimal...

         List<Integer> parts1 = CollectionUtils.newArrayList();
         List<Integer> parts2 = CollectionUtils.newArrayList();

         for( StringTokenizer tokenizer = new StringTokenizer( order1, "." ); tokenizer.hasMoreTokens(); )
         {
            parts1.add( Integer.valueOf( tokenizer.nextToken() ));
         }

         for( StringTokenizer tokenizer = new StringTokenizer( order2, "." ); tokenizer.hasMoreTokens(); )
         {
            parts2.add( Integer.valueOf( tokenizer.nextToken() ));
         }

         // ...and compare it

         int loop = 0;

         for( int length = parts1.size(); loop < length; loop++ )
         {
            if ( loop >= parts2.size() ) {
               return 1;
            }

            int compare = parts1.get( loop ) - parts2.get( loop );

            if ( compare != 0 ) {
               return compare;
            }
         }

         if ( loop < parts2.size() ) {
            return -1;
         }

         return 0;
      }
   }
}

Sunday, July 18, 2010

Customizing Which Form Fields Are Displayed: Part 3

Following on from Part 1 and Part 2, I thought I'd blog some of the other field ordering preferences I've encountered.

Out of the box, Metawidget supplies UiComesAfter and ComesAfterInspectionResultProcessor to let you order your fields on a 'per domain' basis. But of course there are other approaches.

This one is from the Naked Objects .NET guys. In this presentation you can see how they use a .NET MemberOrder attribute to specify field order as 1, 2, 3 etc. Example implementation below. Some points to note:
  • It's in Swing so you can just cut and paste and run it

  • It defines a custom annotation (UiOrder) and custom Inspector (OrderInspector) to detect it

  • It defines an OrderInspectionResultProcessor that sorts the properties/actions
package com.myapp;

import static org.metawidget.inspector.InspectionResultConstants.*;

import java.lang.annotation.*;
import java.util.*;

import javax.swing.*;

import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.*;
import org.metawidget.util.*;
import org.w3c.dom.*;

public class Main {

   public static void main( String[] args ) {

      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.setInspector( new CompositeInspector( new CompositeInspectorConfig().setInspectors(
            new PropertyTypeInspector(),
            new MetawidgetAnnotationInspector(),
            new OrderInspector() ) ) );
      metawidget.addInspectionResultProcessor( new OrderInspectionResultProcessor() );
      metawidget.setToInspect( person );

      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   static class Person {

      @UiOrder( 1 )
      public String name;

      @UiOrder( 2 )
      public int age;

      @UiOrder( 3 )
      public boolean retired;

      @UiOrder( 4 )
      @UiLarge
      public String notes;
   }

   @Retention( RetentionPolicy.RUNTIME )
   @Target( { ElementType.FIELD, ElementType.METHOD } )
   static @interface UiOrder {

      int value();
   }


   static class OrderInspector
      extends BaseObjectInspector {

      @Override
      protected Map<String, String> inspectTrait( Trait trait )
         throws Exception {

         Map<String, String> attributes = CollectionUtils.newHashMap();
         UiOrder order = trait.getAnnotation( UiOrder.class );

         if ( order != null ) {
            attributes.put( "order", String.valueOf( order.value() ) );
         }

         return attributes;
      }
   }

   static class OrderInspectionResultProcessor
      implements InspectionResultProcessor<SwingMetawidget> {

      public String processInspectionResult( String inspectionResult, SwingMetawidget metawidget, Object toInspect, String type, String... names ) {

         try {
            // Start a new document
            //
            // (Android 1.1 did not cope well with shuffling the nodes of an existing document)

            Document newDocument = XmlUtils.newDocument();
            Element newInspectionResultRoot = newDocument.createElementNS( NAMESPACE, ROOT );

            Document document = XmlUtils.documentFromString( inspectionResult );
            Element inspectionResultRoot = document.getDocumentElement();
            XmlUtils.setMapAsAttributes( newInspectionResultRoot, XmlUtils.getAttributesAsMap( inspectionResultRoot ) );
            newDocument.appendChild( newInspectionResultRoot );

            Element entity = (Element) inspectionResultRoot.getChildNodes().item( 0 );
            Element newEntity = newDocument.createElementNS( NAMESPACE, ENTITY );
            XmlUtils.setMapAsAttributes( newEntity, XmlUtils.getAttributesAsMap( entity ) );
            newInspectionResultRoot.appendChild( newEntity );

            // Record all traits (ie. properties/actions) that have an order

            Map<Integer, Element> traitsWithOrder = CollectionUtils.newTreeMap();
            NodeList traits = entity.getChildNodes();

            for ( int loop = 0, length = traits.getLength(); loop < length; loop++ ) {
               Node node = traits.item( loop );

               if ( !( node instanceof Element ) ) {
                  continue;
               }

               Element trait = (Element) node;

               // (if no order, move them across to the new document)

               if ( !trait.hasAttribute( "order" ) ) {
                  newEntity.appendChild( XmlUtils.importElement( newDocument, trait ) );
                  continue;
               }

               traitsWithOrder.put( Integer.valueOf( trait.getAttribute( "order" ) ), trait );
            }

            // Output the traits in TreeMap order

            for ( Element trait : traitsWithOrder.values() ) {
               newEntity.appendChild( XmlUtils.importElement( newDocument, trait ) );
            }

            // Return the new document

            return XmlUtils.documentToString( newInspectionResultRoot.getOwnerDocument(), false );

         } catch ( Exception e ) {
            throw InspectionResultProcessorException.newException( e );
         }
      }
   }
}

Wednesday, July 14, 2010

Customizing Which Form Fields Are Displayed: Part 2

Following on from Part 1, Dan asked whether the screen could decide based on some kind of 'view groups', rather like Bean Validation's validation groups.

I actually like this idea a lot: it combines the flexibility of local field ordering with the safety of not hard-coding field names into the screens. Having said that, this is the first time it's been suggested. So I'll wait and see if it becomes popular before deciding whether to provide it 'out of the box' (this blog series will explore a lot of alternate preferences).

In the meantime, example implementation below. Some points to note:
  • It's in Swing so you can just cut and paste and run it

  • It defines a custom annotation (UiViewGroup) and custom Inspector (ViewGroupInspector) to detect it

  • It defines a ViewGroupInspectionResultProcessor that screens out properties/actions

  • It uses this in a chain with the usual ComesAfterInspectonResultProcessor
To see it in action, try running the code and changing the 'putClientProperty' line to use different view groups (ie. 'summary' or 'detail').

package com.myapp;

import java.lang.annotation.*;
import java.util.*;

import javax.swing.*;

import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.inspectionresultprocessor.sort.*;
import org.metawidget.inspector.annotation.*;
import org.metawidget.inspector.composite.*;
import org.metawidget.inspector.impl.*;
import org.metawidget.inspector.propertytype.*;
import org.metawidget.swing.*;
import org.metawidget.util.*;
import org.w3c.dom.*;

public class Main {

   public static void main( String[] args ) {

      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.setInspector( new CompositeInspector( new CompositeInspectorConfig().setInspectors(
         new PropertyTypeInspector(),
         new MetawidgetAnnotationInspector(),
         new ViewGroupInspector() )));
      metawidget.addInspectionResultProcessor( new ViewGroupInspectionResultProcessor() );
      metawidget.addInspectionResultProcessor( new ComesAfterInspectionResultProcessor<SwingMetawidget>() );
      metawidget.putClientProperty( "view-group", "summary" );
      metawidget.setToInspect( person );

      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }

   static class Person {

      @UiViewGroup( { "summary", "detail" } )
      public String name;

      @UiComesAfter( "name" )
      @UiViewGroup( "summary" )
      public int age;

      @UiComesAfter( "name" )
      @UiViewGroup( "detail" )
      public boolean retired;

      @UiComesAfter
      @UiLarge
      public String notes;
   }

   @Retention( RetentionPolicy.RUNTIME )
   @Target( { ElementType.FIELD, ElementType.METHOD } )
   static @interface UiViewGroup {

      String[] value();
   }


   static class ViewGroupInspector
      extends BaseObjectInspector {

      @Override
      protected Map<String, String> inspectTrait( Trait trait )
         throws Exception {

         Map<String, String> attributes = CollectionUtils.newHashMap();
         UiViewGroup viewGroup = trait.getAnnotation( UiViewGroup.class );

         if ( viewGroup != null ) {
            attributes.put( "view-group", ArrayUtils.toString( viewGroup.value() ) );
         }

         return attributes;
      }
   }

   static class ViewGroupInspectionResultProcessor
      implements InspectionResultProcessor<SwingMetawidget> {

      public String processInspectionResult( String inspectionResult, SwingMetawidget metawidget, Object toInspect, String type, String... names ) {

         String viewGroup = (String) metawidget.getClientProperty( "view-group" );
         Document document = XmlUtils.documentFromString( inspectionResult );
         Element entity = (Element) document.getDocumentElement().getFirstChild();

         for ( int loop = 0; loop < entity.getChildNodes().getLength(); ) {

            Element trait = (Element) entity.getChildNodes().item( loop );

            if ( !trait.hasAttribute( "view-group" ))
            {
               loop++;
               continue;
            }

            String[] viewGroups = ArrayUtils.fromString( trait.getAttribute( "view-group" ));

            if ( ArrayUtils.contains( viewGroups, viewGroup )) {
               loop++;
               continue;
            }

            entity.removeChild( trait );
         }

         return XmlUtils.documentToString( document, false );
      }
   }
}

Tuesday, July 13, 2010

Customizing Which Form Fields Are Displayed: Part 1

Talking to Dan Allen about Metawidget recently, he commented:

"One thing that I think would really help people along is to have an example of how to customize form fields displayed on a JSF view. This seems to be one of the first thing any JSF developer wonders about. I know that customization is possible, both at the global and field level, but just having a simple how-to would go a long way"

This is a great point, worthy of a little blog series.

Where To Start: InspectionResultProcessors

Out of the box, Metawidget has a few different options for ordering fields. I'll just mention the most simple ones here.

You can exclude fields on a 'per screen' basis using stub tags:

<m:metawidget value="#{person}">
   <m:stub value="#{person.age}"/>
</m:metawidget>

You can order fields at the 'domain' level using annotations:

package com.myapp;

import org.metawidget.inspector.annotation.*;

public class Person {
   public String name;

   @UiComesAfter( "name" )
   public int age;

   @UiComesAfter( "age" )
   public boolean retired;
}

And you can use XML (which is implicitly ordered):

<entity type="com.myapp.Person">
   <property name="name"/>
   <property name="age"/>
   <property name="retired"/>
</entity>

But after lots of feedback from interviews, adoption studies and forum posts, I realized the issue of 'what fields appear, and what order they appear in' covered a lot of different preferences and requirements. To satisfy these, I introduced the InspectionResultProcessor interface (click to enlarge):


InspectionResultProcessors sit after the Inspectors and before the WidgetBuilders. Out of the box, UiComesAfter is implemented using ComesAfterInspectionResultProcessor. But InspectionResultProcessors have access both to the inspection result and the Metawidget that is about to render it, and this vantage point gives them a number of capabilites.

Swing: Letting The Screen Decide

One capability is to allow the screen to choose which fields it should render. Now, I don't particularly recommend this approach: it means your screen contains hard-coded field names. These won't refactor well, nor will they evolve well as your business objects evolve. But, hey, Metawidget is all about working the way you want to!

So let's do a Swing example first as it's easier to cut and paste and try yourself. Here's a custom InspectionResultProcessor that chooses, and sorts, business object fields based on a JComponent client property. It extends the code from the Metawidget Tutorial:

package com.myapp;
         
import static org.metawidget.inspector.InspectionResultConstants.*;

import javax.swing.*;
import org.metawidget.swing.*;
import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.util.*;
import org.w3c.dom.*;


public class Main {

   public static void main( String[] args ) {
      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      metawidget.addInspectionResultProcessor( new IncludingInspectionResultProcessor() );
      metawidget.putClientProperty( "include", new String[]{ "retired", "age" } );

      metawidget.setToInspect( person );

      JFrame frame = new JFrame( "Metawidget Tutorial" );
      frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );
      frame.getContentPane().add( metawidget );
      frame.setSize( 400, 250 );
      frame.setVisible( true );
   }
   
   static class Person {
      public String name;
      public int age;
      public boolean retired;
   }
   
   static class IncludingInspectionResultProcessor
      implements InspectionResultProcessor<SwingMetawidget> {
   
      public String processInspectionResult( String inspectionResult, SwingMetawidget metawidget, Object toInspect, String type, String... names ) {
   
         String[] includes = (String[]) metawidget.getClientProperty( "include" );
         Document document = XmlUtils.documentFromString( inspectionResult );
         Element entity = (Element) document.getDocumentElement().getFirstChild();      
         int propertiesToCleanup = entity.getChildNodes().getLength();

         // Pull out the names in order

         for( String include : includes ) {
         
            Element property = XmlUtils.getChildWithAttributeValue( entity, NAME, include );

            if ( property == null )
               continue;

            entity.appendChild( property );
            propertiesToCleanup--;
         }

         // Remove the rest

         for( int loop = 0; loop < propertiesToCleanup; loop++ ) {
            entity.removeChild( entity.getFirstChild() );
         }

         return XmlUtils.documentToString( document, false );
      }
   }

}

If this approach happens to be your preference, you may be surprised you have to code an InspectionResultProcessor for it yourself - why doesn't Metawidget support it out of the box? However, you may also be surprised at how many other preferences there are, as we shall see later in this blog series. Metawidget isn't about providing flags for every possible variation: UI requirements are too diverse for that. Instead, Metawidget tries to be pluggable enough, in enough places, that you can always tweak it to suit.

JSF: Letting The Screen Decide

For completeness (and because it's what Dan actually asked for!) let's do a JSF version of the above:

package com.myapp;
         
import static org.metawidget.inspector.InspectionResultConstants.*;

import javax.faces.component.*;
import org.metawidget.faces.*;
import org.metawidget.faces.component.*;
import org.metawidget.inspectionresultprocessor.iface.*;
import org.metawidget.util.*;
import org.w3c.dom.*;

public class IncludingInspectionResultProcessor
   implements InspectionResultProcessor<UIMetawidget> {
   
   public String processInspectionResult( String inspectionResult, UIMetawidget metawidget, Object toInspect, String type, String... names ) {

      UIParameter includeParameter = FacesUtils.findParameterWithName( metawidget, "include" );
   
      if ( includeParameter == null )
         return null;

      String[] includes = ArrayUtils.fromString( (String) includeParameter.getValue() );
      Document document = XmlUtils.documentFromString( inspectionResult );
      Element entity = (Element) document.getDocumentElement().getFirstChild();      
      int propertiesToCleanup = entity.getChildNodes().getLength();

      // Pull out the names in order

      for( String include : includes ) {
      
         Element property = XmlUtils.getChildWithAttributeValue( entity, NAME, include );
      
         if ( property == null )
            continue;

         entity.appendChild( property );
         propertiesToCleanup--;
      }

      // Remove the rest

      for( int loop = 0; loop < propertiesToCleanup; loop++ ) {
         entity.removeChild( entity.getFirstChild() );
      }

      return XmlUtils.documentToString( document, false );
   }
}

You'd then add this into your metawidget.xml:

<metawidget xmlns="http://metawidget.org"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://metawidget.org http://metawidget.org/xsd/metawidget-1.0.xsd" version="1.0">

   <htmlMetawidget xmlns="java:org.metawidget.faces.component.html">
      .
      .
      .
      <inspectionResultProcessors>
         <array>
            <includingInspectionResultProcessor xmlns="java:com.myapp"/>
         </array>
      </inspectionResultProcessors>
      .
      .
      .
   </htmlMetawidget>
</metawidget>

And use it in your page:

<m:metawidget value="#{contact.current}">
   <f:param name="include" value="title,firstname,surname,edit,save,delete"/>
</m:metawidget>

Note this can include actions (like 'edit' and 'save') as well as properties. More InspectionResultProcessor examples to come!