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