Question

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...

Was it helpful?

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

}

OTHER TIPS

You cannot modify your code using annotation processing. However, you can create new classes, which can subclass the classes you have and they can contain additional annotations.

If you want to modify your code, you need a library that modifies your code either at compile time or at load time (for example as a special class loader).

I don't know what library would be the best for your case, but BCEL seems to be capable of the task.

See also:

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top