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>

0 comments: