Search code examples
javahibernateannotationsbean-validationannotation-processing

Annotation processing for adding message attribute to existing JSR-303 annotations on fields


I have a data transfer object that's annotated with JSR-303 constraints like...

public class AssetOwnedDailyLocatableId implements Serializable, AssetOwned, HasOperatingDay, Locatable {

private static final long serialVersionUID = 1L;

@NotEmpty
@Size(min = 1, max = 30)
private String locationName;

@NotEmpty
private String operatingDay;

@NotEmpty
@Size(min = 1, max = 30)
private String assetOwner;

I am attempting to use Annotation Processing to enrich each JSR-303 constraint with a message attribute whose value would be equal to the constraint-name.class-name.member-name.

E.g., using the above, the final generated output for the locationName field's annotations would look like...

@NotEmpty(message="{NotEmpty.AssetOwnedDailyLocatableId.locationName}")
@Size(min = 1, max = 30, message="{Size.AssetOwnedDailyLocatableId.locationName}")
private String locationName;

Why? Because I want complete control over custom validation messaging. I have well over hundreds of data transfer objects that I would like to process with something like...

/**
 * ViolationConstraint message processor. During compile time it scans all DTO
 * classes that have <code>javax.validation.constrants.*</code> or 
 * <code>org.hibernate.validator.constraints.*</code>annotated
 * fields, then enriches the annotation with a <code>message</code> attribute
 * where its value will be <code>constraint-name.class-name.field-name</code>.
 * 
 * @param <T>
 *            any JSR-303 annotation type
 * 
 */
@SupportedSourceVersion(SourceVersion.RELEASE_6)
@SupportedAnnotationTypes(value = { "javax.validation.constraints.*", "org.hibernate.validator.constraints.*" })
public class ValidationMessagesProcessor<T extends Annotation> extends AbstractProcessor {

private static final String JAVAX_PATH = "javax.validation.constraints.*";
private static final String HIBERNATE_PATH = "org.hibernate.validator.constraints/*";

private PackageUtil<T> util;

public ValidationMessagesProcessor() {
    super();
    util = new PackageUtil<T>();
}

/* (non-Javadoc)
 * @see javax.annotation.processing.AbstractProcessor#process(java.util.Set, javax.annotation.processing.RoundEnvironment)
 */
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
    if (!roundEnvironment.processingOver()) {
        String message;
        message = ValidationMessagesProcessor.class.getName() + " will begin processing now...";
        processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message);
        try {
            final List<Class<T>> annotationTypes = new ArrayList<Class<T>>();
            final List<Class<T>> jxTypes = util.listMatchingClasses(JAVAX_PATH);
            final List<Class<T>> hibTypes = util.listMatchingClasses(HIBERNATE_PATH);
            annotationTypes.addAll(jxTypes);
            annotationTypes.addAll(hibTypes);

            for (final Element e : roundEnvironment.getRootElements()) {

                // TODO Do the real work!

                /*message = "... JSR-303 annotation '" + a.annotationType().getClass().getName() + "' found in "
                        + e.getSimpleName();
                processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, message); */
            }
        } catch (final IOException ioe) {
            message = "Failed to locate javax.validation.constraints or org.hibernate.validator.constraints classes on classpath!";
            processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, message);
        }
    }
    return true; // no further processing of this annotation type
}

}

I want to know if the above approach is feasible, or if I should try something else (that might be simpler). Furthermore, if it is feasible, some direction on what to implement within the //TODO section of the processor above. So far I've consulted...


Solution

  • So I opted for authoring a utility based on Eclipse JDT.

    Took me a while to hunt down all the dependent libs to make this work. For anyone else interested here's the Maven dependencies:

        <!-- Validation API and Impl -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>${validation-api.version}</version>
        </dependency>
        <!-- Hibernate validator impl -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
        </dependency>
    
        <!-- Required to power classpath scanning for JSR-303 classes within JAR packages -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>${spring.framework.version}</version>
        </dependency>
    
        <!-- Required to employ all Eclipse JDT capabilities -->
        <!-- This specific collection of artifact versions is known to work together -->
        <!-- Take caution when upgrading versions! -->
        <dependency>
            <groupId>org.eclipse.tycho</groupId>
            <artifactId>org.eclipse.jdt.core</artifactId>
            <version>3.8.1.v20120502-0834</version>
        </dependency>
        <dependency>
            <groupId>org.eclipse.tycho</groupId>
            <artifactId>org.eclipse.osgi</artifactId>
            <version>3.8.0.v20120430-1750</version>
        </dependency>
        <dependency>
            <groupId>org.jibx.config.3rdparty.org.eclipse</groupId>
            <artifactId>org.eclipse.core.resources</artifactId>
            <version>3.7.100.v20110510-0712</version>
        </dependency>
        <dependency>
            <groupId>org.jibx.config.3rdparty.org.eclipse</groupId>
            <artifactId>org.eclipse.jdt.core</artifactId>
            <version>3.7.0.v_B61</version>
        </dependency>
        <dependency>
            <groupId>org.jibx.config.3rdparty.org.eclipse</groupId>
            <artifactId>org.eclipse.core.runtime</artifactId>
            <version>3.7.0.v20110110</version>
        </dependency>
        <dependency>
            <groupId>org.jibx.config.3rdparty.org.eclipse</groupId>
            <artifactId>org.eclipse.equinox.common</artifactId>
            <version>3.6.0.v20110523</version>
        </dependency>
        <dependency>
            <groupId>org.jibx.config.3rdparty.org.eclipse</groupId>
            <artifactId>org.eclipse.text</artifactId>
            <version>3.5.100.v20110505-0800</version>
        </dependency>
        <dependency>
            <groupId>org.jibx.config.3rdparty.org.eclipse</groupId>
            <artifactId>org.eclipse.core.jobs</artifactId>
            <version>3.5.100.v20110404</version>
        </dependency>
        <dependency>
            <groupId>org.jibx.config.3rdparty.org.eclipse</groupId>
            <artifactId>org.eclipse.core.contenttype</artifactId>
            <version>3.4.100.v20110423-0524</version>
        </dependency>
        <dependency>
            <groupId>org.jibx.config.3rdparty.org.eclipse</groupId>
            <artifactId>org.eclipse.equinox.preferences</artifactId>
            <version>3.4.0.v20110502</version>
        </dependency>
    

    I authored four classes one with main harness and the others a facade and utils.

    The harness:

    import java.io.File;
    import java.io.IOException;
    import java.lang.annotation.Annotation;
    import java.util.ArrayList;
    import java.util.Collection;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    import org.eclipse.jdt.core.dom.AST;
    import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
    import org.eclipse.jdt.core.dom.CompilationUnit;
    import org.eclipse.jdt.core.dom.Expression;
    import org.eclipse.jdt.core.dom.FieldDeclaration;
    import org.eclipse.jdt.core.dom.IExtendedModifier;
    import org.eclipse.jdt.core.dom.MemberValuePair;
    import org.eclipse.jdt.core.dom.NormalAnnotation;
    import org.eclipse.jdt.core.dom.StringLiteral;
    import org.eclipse.jface.text.BadLocationException;
    import org.eclipse.jface.text.Document;
    import org.eclipse.text.edits.MalformedTreeException;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.spp.im.mui.commons.jdt.JDTFacade;
    import org.spp.im.mui.commons.util.FileUtil;
    import org.spp.im.mui.commons.util.PackageUtil;
    import org.springframework.util.CollectionUtils;
    
    /**
     * A utility that scans all DTO classes that have
     * <code>javax.validation.constrants.*</code> or
     * <code>org.hibernate.validation.constraints.*</code> annotated fields, then
     * enriches the annotation with a <code>message</code> attribute where its value
     * will be <code>constraint-name.class-name.field-name</code>.
     * 
     * @author cphillipson
     * @param <T>
     *            any JSR-303 annotation type
     * 
     */
     public class ConstraintMessageUtil<T extends Annotation> {
    
    private static Logger log = LoggerFactory.getLogger(ConstraintMessageUtil.class);
    
    private static final String JAVAX_PATH = "/javax/validation/constraints/*";
    private static final String HIBERNATE_PATH = "/org/hibernate/validator/constraints/*";
    
    private PackageUtil<T> util;
    private JDTFacade<T> facade;
    
    public ConstraintMessageUtil() {
        util = new PackageUtil<T>();
        facade = new JDTFacade<T>();
    }
    
    public void process(String sourcePath) throws Exception {
    
        // step #1: build a set of JSR-303 constraint classes
        final Set<Class<T>> annotationTypes = new HashSet<Class<T>>();
        try {
            final List<Class<T>> jxTypes = util.listMatchingClasses(JAVAX_PATH);
            final List<Class<T>> hibTypes = util.listMatchingClasses(HIBERNATE_PATH);
            annotationTypes.addAll(jxTypes);
            annotationTypes.addAll(hibTypes);
            // remove @Valid from the mix
            annotationTypes.remove(Valid.class);
            Assert.isTrue(!annotationTypes.contains(Valid.class));
        } catch (final IOException ioe) {
    
        }
    
        // step #2: get all files recursively from source path
        final Collection<File> allJavaSourceInDirectory = FileUtil.getAllJavaSourceInDirectory(new File(sourcePath),
                true);
    
        // step #3: filter files to just the ones that contain annotations
        final List<File> annotatedSources = new ArrayList<File>();
        if (!CollectionUtils.isEmpty(allJavaSourceInDirectory)) {
            boolean containsJsr303Annotation;
            String typeName;
            for (final File f : allJavaSourceInDirectory) {
                for (final Class<T> annotationType : annotationTypes) {
                    typeName = annotationType.getName();
                    containsJsr303Annotation = FileUtil.isContentInFile(f, typeName);
                    if (containsJsr303Annotation) {
                        annotatedSources.add(f);
                        break; // at least one annotation found, move along
                    }
                }
            }
        }
    
        // step #4: for each annotated source file parse and rewrite with
        // enriched message for each JSR-303 annotation
        enrichJavaSourceFilesWithMessageAttributesForConstraintTypeAnnotatedFields(annotatedSources, annotationTypes);
    
    }
    
    // note: probably could have implemented an ASTVisitor, but...
    protected void enrichJavaSourceFilesWithMessageAttributesForConstraintTypeAnnotatedFields(
            List<File> annotatedSources, Set<Class<T>> constraintTypes) throws IOException, MalformedTreeException,
            BadLocationException {
        if (!CollectionUtils.isEmpty(annotatedSources)) {
            // reusable local variables... a veritable cornucopia
            Set<FieldDeclaration> fieldCandidates;
            Document document;
            String contents;
            String constraintName;
            String className;
            String fieldName;
            StringBuilder sb;
            AbstractTypeDeclaration td;
            IExtendedModifier[] modifiers;
            CompilationUnit unit;
            AST ast;
            MemberValuePair mvp;
            Expression exp;
            NormalAnnotation na;
    
            // iterate over all java source containing jsr-303 annotated fields
            for (final File source : annotatedSources) {
                unit = facade.generateCompilationUnitForFile(source);
                ast = unit.getAST();
                // get the set of fields which are annotated
                fieldCandidates = facade.obtainAnnotatedFieldsFromClassInCompilationUnit(unit, constraintTypes);
                log.info(source.getName() + " contains " + fieldCandidates.size()
                        + " fields with constraint annotations.");
                // iterate over each annotated field
                for (final FieldDeclaration fd : fieldCandidates) {
                    modifiers = (IExtendedModifier[]) fd.modifiers().toArray(
                            new IExtendedModifier[fd.modifiers().size()]);
                    int i = 0;
                    // iterate over modifiers for the field
                    for (final IExtendedModifier modifier : modifiers) {
                        // interested in Eclipse JDT's DOM form of Annotation
                        if (modifier instanceof org.eclipse.jdt.core.dom.Annotation) {
                            // construct the key-value pair
                            sb = new StringBuilder();
                            constraintName = ((org.eclipse.jdt.core.dom.Annotation) modifier).getTypeName().toString();
                            // Ignore @Valid annotations
                            if (!constraintName.equals(Valid.class.getSimpleName())) {
                                td = (AbstractTypeDeclaration) fd.getParent();
                                className = td.getName().toString();
                                fieldName = fd.fragments().get(0).toString();
                                // field may have an assignment, so strip it
                                if (fieldName.contains("=")) {
                                    final int end = fieldName.indexOf("=");
                                    fieldName = fieldName.substring(0, end).trim();
                                }
                                sb.append("{");
                                sb.append(constraintName);
                                sb.append(".");
                                sb.append(className);
                                sb.append(".");
                                sb.append(fieldName);
                                sb.append("}");
                                // construct new properties, and instead of
                                // updating
                                // the existing annotation, replace it
                                mvp = ast.newMemberValuePair();
                                mvp.setName(ast.newSimpleName("message"));
                                exp = ast.newStringLiteral();
                                ((StringLiteral) exp).setLiteralValue(sb.toString());
                                mvp.setValue(exp);
                                na = ast.newNormalAnnotation();
                                na.setTypeName(ast.newSimpleName(constraintName));
                                na.values().add(mvp);
                                // don't forget to add the original annotation's
                                // member-value pairs to the new annotation
                                if (modifier instanceof NormalAnnotation) {
                                    final NormalAnnotation ona = (NormalAnnotation) modifier;
                                    final List<?> values = ona.values();
                                    for (int j = 0; j < values.size(); j++) {
                                        final MemberValuePair omvp = (MemberValuePair) values.get(j);
                                        mvp = ast.newMemberValuePair();
                                        mvp.setName(ast.newSimpleName(omvp.getName().toString()));
                                        // a value can be a String, Number or
                                        // reference to a constant
                                        switch (omvp.getValue().getNodeType()) {
                                            case ASTNode.NUMBER_LITERAL:
                                                mvp.setValue(ast.newNumberLiteral(omvp.getValue().toString()));
                                                break;
                                            case ASTNode.STRING_LITERAL:
                                                exp = ast.newStringLiteral();
                                                ((StringLiteral) exp).setLiteralValue(omvp.getValue().toString());
                                                mvp.setValue(exp);
                                                break;
                                            case ASTNode.QUALIFIED_NAME:
                                                final QualifiedName oqn = (QualifiedName) omvp.getValue();
                                                exp = ast.newQualifiedName(ast.newName(oqn.getQualifier().toString()),
                                                        ast.newSimpleName(oqn.getName().toString()));
                                                mvp.setValue(exp);
                                                break;
                                        }
                                        na.values().add(mvp);
                                    }
                                }
                                fd.modifiers().remove(i);
                                fd.modifiers().add(i, na);
                                log.info("@" + constraintName + " on " + fieldName + " in " + className
                                        + " has been enriched with a 'message' attribute whose value is now '"
                                        + sb.toString() + "'.");
                            }
                            i++;
                        }
                    }
                }
                contents = FileUtil.toString(source);
                document = new Document(contents);
                facade.saveUpdatesToFile(unit, document, source);
            }
        }
    }
    
    public static void main(String args[]) {
        final ConstraintMessageUtil util = new ConstraintMessageUtil();
        try {
            // e.g., on Windows,
            // "D:\\workspaces\\alstom-grid\\SPP-MUI\\spp-im-mui-dto\\src\\main\\java\\org\\spp\\im\\mui\\dto"
            util.process(args[0]);
        } catch (final Exception e) {
            e.printStackTrace();
        }
    }
    }
    

    The utils:

    import java.io.IOException;
    import java.util.LinkedList;
    import java.util.List;
    
    import org.springframework.core.io.Resource;
    import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
    import org.springframework.util.AntPathMatcher;
    
    /**
     * Package utility. Provides handy methods for finding classes (of a particular
     * type) within a package on the classpath.
     * 
     * @author cphillipson
     * 
     * @param <T>
     *            types of classes to be found in package
     */
    class PackageUtil<T> {
    
    public List<Class<T>> listMatchingClasses(String matchPattern) throws IOException {
        final List<Class<T>> classes = new LinkedList<Class<T>>();
        final PathMatchingResourcePatternResolver scanner = new PathMatchingResourcePatternResolver();
        scanner.setPathMatcher(new AntPathMatcher());
        final Resource[] resources = scanner.getResources("classpath:" + matchPattern);
        for (final Resource resource : resources) {
            final Class<T> clazz = getClassFromResource(resource);
            classes.add(clazz);
        }
        return classes;
    }
    
    public Class<T> getClassFromResource(Resource resource) {
        Class<T> result = null;
        try {
            String resourceUri = resource.getURI().toString();
            resourceUri = resourceUri.substring(0, resourceUri.indexOf(".class")).replace("/", ".");
            if (resourceUri.contains("!")) { // class was found in an archive
                resourceUri = resourceUri.substring(resourceUri.indexOf("!") + 2);
            }
            // try printing the resourceUri before calling forName, to see if it
            // is OK.
            result = (Class<T>) Class.forName(resourceUri);
        } catch (final Exception ex) {
            ex.printStackTrace();
        }
        return result;
    }
    }
    
    
    /**
    * A collection of special-purposed methods for working with files and
    * directories. Wraps Apache Commons I/O.
    * 
    * @author cphillipson
    * 
    */
    public class FileUtil {
    
    public static Collection<File> getAllJavaSourceInDirectory(File directory, boolean recursive) {
        // scans directory (and sub-directories if recursive flag is true) for
        // .java files, returns a collection of files
        return FileUtils.listFiles(directory, new String[] { "java" }, recursive);
    }
    
    public static boolean isContentInFile(File file, String fragment) throws IOException {
        boolean result = false;
        final String contents = toString(file);
        if (contents.contains(fragment)) { // does file contain fragment?
            result = true;
        }
        return result;
    }
    
    public static String toString(File file) throws IOException {
        final String result = FileUtils.readFileToString(file, "utf8");
        return result;
    }
    
    public static void toFile(File file, String content) throws IOException {
        FileUtils.writeStringToFile(file, content, "utf8");
    }
    }
    

    The facade:

    import java.io.File;
    import java.io.IOException;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Set;
    
    import org.eclipse.jdt.core.dom.AST;
    import org.eclipse.jdt.core.dom.ASTNode;
    import org.eclipse.jdt.core.dom.ASTParser;
    import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
    import org.eclipse.jdt.core.dom.Annotation;
    import org.eclipse.jdt.core.dom.BodyDeclaration;
    import org.eclipse.jdt.core.dom.CompilationUnit;
    import org.eclipse.jdt.core.dom.FieldDeclaration;
    import org.eclipse.jdt.core.dom.IExtendedModifier;
    import org.eclipse.jface.text.BadLocationException;
    import org.eclipse.jface.text.Document;
    import org.eclipse.text.edits.MalformedTreeException;
    import org.eclipse.text.edits.TextEdit;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.spp.im.mui.commons.util.FileUtil;
    
    /**
     * Abstract syntax tree facade. Backed by Eclipse JDT, this facade provides a
     * number of conveniences, like the ability to:
     * <ul>
     * <li>generate an {@link CompilationUnit} from a source {@File}</li>
     * <li>save updates in a {@link Document} managed by {@link CompilationUnit} to
     * a {@link File}</li>
     * </ul>
     * and much more. Credit goes to <a href=
     * "http://svn.apache.org/repos/asf/openejb/branches/eclipse-plugins-1.0.0.alpha/plugins/org.apache.openejb.devtools.core/src/main/java/org/apache/openejb/devtools/core/JDTFacade.java"
     * >Apache OpenEJB DevTools JDTFacade source</a> for providing much of the
     * inspiration for this implementation.
     * 
     * @author cphillipson
     * @param <T>
     *            any annotation type
     * 
     */
     public class JDTFacade<T extends java.lang.annotation.Annotation> {
    
    private static Logger log = LoggerFactory.getLogger(JDTFacade.class);
    
    public CompilationUnit generateCompilationUnitForFile(File file) throws IOException {
        final String source = FileUtil.toString(file);
        final Document document = new Document(source);
        final ASTParser parser = ASTParser.newParser(AST.JLS4);
        parser.setSource(document.get().toCharArray());
        final CompilationUnit unit = (CompilationUnit) parser.createAST(null /* no ProgressMonitor */);
        unit.recordModifications();
        return unit;
    }
    
    public void saveUpdatesToFile(CompilationUnit unit, Document document, File file) throws MalformedTreeException,
            IOException, BadLocationException {
        final TextEdit edits = unit.rewrite(document, null /* no options */);
        edits.apply(document);
        boolean writeable = true; // should always be able to write to file...
        if (!file.canWrite()) { // .. but just in case we cannot...
            writeable = file.setWritable(true);
        }
        if (writeable) {
            FileUtil.toFile(file, document.get());
            log.info("Successfully wrote updates to " + file.getName());
        } else {
            log.warn("Unable to write to " + file.getName());
        }
    }
    
    public Set<FieldDeclaration> obtainAnnotatedFieldsFromClassInCompilationUnit(CompilationUnit unit,
            Set<Class<T>> annotationTypes) {
        final Set<FieldDeclaration> fields = new HashSet<FieldDeclaration>();
        final List<AbstractTypeDeclaration> types = unit.types();
        IExtendedModifier[] modifiers;
        for (final AbstractTypeDeclaration type : types) {
            if (type.getNodeType() == ASTNode.TYPE_DECLARATION) {
                // Class def found
                final List<BodyDeclaration> bodies = type.bodyDeclarations();
                for (final BodyDeclaration body : bodies) {
                    if (body.getNodeType() == ASTNode.FIELD_DECLARATION) {
                        final FieldDeclaration field = (FieldDeclaration) body;
                        modifiers = (IExtendedModifier[]) field.modifiers().toArray(new IExtendedModifier[0]);
                        for (final IExtendedModifier modifier : modifiers) {
                            if (!(modifier instanceof Annotation)) {
                                continue;
                            }
                            final Annotation annotationModifer = (Annotation) modifier;
                            for (final Class<T> clazz : annotationTypes) {
                                if (annotationModifer.getTypeName().toString().equals(clazz.getCanonicalName())
                                        || annotationModifer.getTypeName().toString().equals(clazz.getSimpleName())) {
                                    fields.add(field);
                                    break;
                                }
                            }
                        }
                    }
                }
            }
        }
        return fields;
    }
    
    }