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 @UiComesAfter." );
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!