Tuesday, August 3, 2010

Customizing Which Form Fields Are Displayed: Part 9

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

This one is from Dan Allen, who wanted to enhance the type safety of @UiComesAfter by using string constants generated by an annotation processor. This is similar to the approach JPA 2 takes. It means you can do:

package com.myapp;

import org.metawidget.inspector.annotation.*;

public class Person {

   public String name;

   @UiComesAfter( Person_.name )
   public int age;

   @UiComesAfter( Person_.age )
   public boolean retired;

   @UiLarge
   public String notes;
}

Where Person_ is an interface generated by an annotation processor. Behind the scenes, the generated interface looks like this:

package com.myapp;

import javax.annotation.*;

/**
* Generated interface to allow typesafe references to property names, useful for
* Metawidget's @UiComesAfter.
*/

@Generated( "com.myapp.PropertyNameAnnotationProcessor" )
public interface Person_ {

   static final String age = "age";
   static final String name = "name";
   static final String notes = "notes";
   static final String retired = "retired";
}

A neat feature of this approach is you can integrate it tightly with your IDE. Let's begin with the hard bit, the annotation processor itself. Example implementation below. Points to note:
  • I've been a bit more thorough than in Part 8 and added support for superclasses.

  • You'll need to decide your preference for identifying your model classes, because you don't want this annotation processor generating interfaces for every class! Ideas include: annotating each model class with some top level annotation (such as @Model ) and adjusting the @SupportedAnnotationTypes( "*" ) below; or changing the implementation of the isModelClass method below to scan for your preferred package names.
package com.myapp;

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

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

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

   private final static char CLASS_NAME_SUFFIX = '_';

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

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

            writeType( typeElement );
         }
      } catch ( Throwable t ) {
         processingEnv.getMessager().printMessage( Kind.ERROR, t.getMessage() );
      }

      return true;
   }

   protected boolean isModelClass( TypeElement typeElement ) {

      return typeElement.getQualifiedName().toString().startsWith( "com.myapp." );
   }

   private String writeType( TypeElement typeElement )
      throws IOException {

      // If the type has properties...

      Set<String> propertyNames = readProperties( typeElement );

      if ( propertyNames == null ) {
         return null;
      }

      // ...check if needs to extend a superclass...

      String superclassName = null;

      if ( typeElement.getSuperclass().getKind() == TypeKind.DECLARED ) {

         TypeElement superclassElement = (TypeElement) ( (DeclaredType) typeElement.getSuperclass() ).asElement();

         // (note: we don't actually writeType() for the superclass, else we'll get a
         // "Attempt to recreate a file for type" from javac)

         if ( readProperties( superclassElement ) != null ) {
            superclassName = superclassElement.getQualifiedName().toString();
         }
      }

      // ...then generate a new source file..

      String qualifiedName = typeElement.getQualifiedName().toString() + CLASS_NAME_SUFFIX;
      PrintWriter writer = new PrintWriter( processingEnv.getFiler().createSourceFile( qualifiedName ).openWriter() );

      try {
         writer.write( "package " );
         int lastDot = qualifiedName.lastIndexOf( '.' );
         writer.write( qualifiedName.substring( 0, lastDot ) );
         writer.write( ";\r\n\r\nimport javax.annotation.*;\r\n\r\n/**" );
         writer.write( "\r\n * Generated interface to allow typesafe references to property names, useful for" );
         writer.write( "\r\n * Metawidget's &#064UiComesAfter." );
         writer.write( "\r\n * <p>" );
         writer.write( "\r\n * <strong>This interface is automatically generated by " );
         writer.write( getClass().getSimpleName() );
         writer.write( "!" );
         writer.write( "\r\n * You do not need to maintain it, and should never edit it.</strong>" );
         writer.write( "\r\n */" );
         writer.write( "\r\n\r\n@Generated( \"" );
         writer.write( getClass().getName() );

         // ...containing an interface...

         writer.write( "\" )\r\npublic interface " );
         writer.write( qualifiedName.substring( lastDot + 1, qualifiedName.length() ) );

         if ( superclassName != null ) {
            writer.write( "\r\n\textends " );
            writer.write( superclassName );
            writer.write( CLASS_NAME_SUFFIX );
         }

         writer.write( " {\r\n" );

         // ...write all its properties as constants...

         for ( String propertyName : propertyNames ) {

            writer.write( "\r\n\tstatic final String " );
            writer.write( propertyName );
            writer.write( " = \"" );
            writer.write( propertyName );
            writer.write( "\";" );
         }

         // ...and close it

         writer.write( "\r\n}" );
      } finally {
         writer.close();
      }

      return qualifiedName;
   }

   private Set<String> readProperties( TypeElement typeElement ) {

      // If the type is public...

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

      // ...and not generated...

      if ( typeElement.getAnnotation( Generated.class ) != null ) {
         return null;
      }

      // ...and not a model class...

      if ( !isModelClass( typeElement ) ) {
         return null;
      }

      // ...and not an enum...

      if ( typeElement.getKind() == ElementKind.ENUM ) {
         return null;
      }

      // ...and not an interface...

      if ( typeElement.getKind() == ElementKind.INTERFACE ) {
         return null;
      }

      // ...and has fields...

      Set<String> propertyNames = new TreeSet<String>();

      for ( VariableElement fieldElement : ElementFilter.fieldsIn( typeElement.getEnclosedElements() ) ) {

         // ...that are public...

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

         // ...and not static...

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

         // ...remember them

         propertyNames.add( fieldElement.getSimpleName().toString() );
      }

      // ...or has methods...

      for ( ExecutableElement executableElement : ElementFilter.methodsIn( typeElement.getEnclosedElements() ) ) {

         // ...that are public...

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

         // ...and not static...

         if ( executableElement.getModifiers().contains( Modifier.STATIC ) ) {
            continue;
         }

         // ...that 'look' like a getter...

         if ( executableElement.getParameters().size() != 0 ) {
            continue;
         }

         if ( executableElement.getReturnType().getKind() == TypeKind.VOID ) {
            continue;
         }

         // ...and are not an override of a superclass getter...

         if ( executableElement.getAnnotation( Override.class ) != null ) {
            continue;
         }

         // ...that are legal property names...

         String simpleName = executableElement.getSimpleName().toString();

         if ( simpleName.startsWith( "get" ) ) {
            simpleName = simpleName.substring( "get".length() );
         } else if ( simpleName.startsWith( "is" ) ) {
            simpleName = simpleName.substring( "is".length() );
         } else {
            continue;
         }

         simpleName = Character.toLowerCase( simpleName.charAt( 0 ) ) + simpleName.substring( 1 );

         // ...remember them

         propertyNames.add( simpleName );
      }

      // If no properties remembered, do not generate anything

      if ( propertyNames.isEmpty() ) {
         return null;
      }

      return propertyNames;
   }
}

Phew! This is ready to run, but for extra kudos let's integrate it into Eclipse. First you need to create a file under META-INF/services called javax.annotation.processing.Processor. The file contains just one line:

com.myapp.PropertyNameAnnotationProcessor

Next you need to package this file and the PropertyNameAnnotationProcessor.class into a JAR. Here's an Ant script:

<project name="property-name-processor" default="pack">

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

      <jar destfile="${builddir}/property-name-processor.jar">
         <fileset dir="src">
            <include name="META-INF/services/*"/>
         </fileset>
         <fileset dir="${builddir}">
            <include name="**/*Processor.class"/>
         </fileset>
      </jar>

   </target>
   
</project>

Then, in Eclipse, you must enable annotation processing under your Project Properties:

And add this JAR into your Factory Path:
Nearly there! Now just define the Person class:

package com.myapp;

import org.metawidget.inspector.annotation.*;

public class Person {

   public String name;

   @UiComesAfter( Person_.name )
   public int age;

   @UiComesAfter( Person_.age )
   public boolean retired;

   @UiLarge
   public String notes;
}

And the Main class:

package com.myapp;

import javax.swing.*;
import org.metawidget.swing.*;

public class Main {

   public static void main( String[] args ) {

      Person person = new Person();

      SwingMetawidget metawidget = new SwingMetawidget();
      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 );
   }
}

And hit Run!

0 comments: