Tuesday, August 31, 2010

More Typesafe JPA2 mappedBy

I've been eating my own dog food lately, reworking some of my client projects to use the annotation processor suggested by Dan Allen. It's been working out nicely, allowing me to refactor...

public class Person {

   public String getName() { ... }

   @UiComesAfter( "name" )
   public Set<Address> getAddresses() { ... }
}

...into...

public class Person {

   public String getName() { ... }

   @UiComesAfter( Person_.name )
   public Set<Address> getAddresses() { ... }
}

But today I realised it has another cool side effect. I can refactor...

public class Person {

   public String getName() { ... }

   @UiComesAfter( Person_.name )
   @OneToMany( mappedBy = "person" )
   public Set<Address> getAddresses() { ... }
}

...into...

public class Person {

   public String getName() { ... }

   @UiComesAfter( Person_.name )
   @OneToMany( mappedBy = Address_.person )
   public Set<Address> getAddresses() { ... }
}

...giving me more typesafe (well, typo-safe :) JPA2 annotations! The existing JPA2 metamodel, being designed mainly for Criteria queries, doesn't seem to support this yet. But I don't see why it couldn't be added for a future release?

Comments welcome!

Wednesday, August 4, 2010

Customizing Which Form Fields Are Displayed

That concludes my little series on customizing which field forms are displayed in Metawidget. I've tried to show that field ordering is a suprisingly deep topic, with lots of different approaches and preferences. In short, there is no 'right' way to do field ordering. The only 'right' way is for a UI generator to have something simple out-of-the-box, then be sufficiently pluggable that people can adjust it to their architecture and tastes.

To recap, we have explored field ordering:

These are all in addition to Metawidget's out-of-the-box UiComesAfter, ComesAfterInspectionResultProcessor and Stub widgets.

Feedback, and additional field ordering preferences, very welcome!

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!