Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Compile time validation of method arguments

I've found some similar questions here, but the incomplete answers didn't help and produced more confusion than clarifying anything, so here's my try to give a more structured question and get hopefully answers that will help more users.

My simplified example: I have a Java class with two different constructors

public class ObjectOfInterest {
  public ObjectOfInterest(String string, Integer int) { ... }
  public ObjectOfInterest(String string1, String string2) { ... }
  ...
}

I need some compile time validation on the calls of these constructors. The parameter string2 has to be some literal, and I want to mark the calls as warning depending on the content (i.e. give a warning, when it's no literal or when the literal has not the correct format).

Unfortunately documentation for validation in Java with Eclipse isn't easy to understand, sometimes outdated, most often it seems to me incomplete, and it seems that there's no working example out there that's short enough to be used in a tutorial.

My goal: First step: I'd like to have a validator that marks calls of that two parameter version with a warning - just to get started somewhere and get to understand the basics.

What I've found so far: The few examples I've seen make a public class MyValidator implements IValidator, ISourceValidator where IValidator needs to implement a method public void validate(IValidationContext arg0, IReporter arg1) throws ValidationException and seem to be from an old version of the validation framework (sometimes I found just empty method with comment useless), and ISourceValidator needs to implement a method public void validate(IRegion arg0, IValidationContext arg1, IReporter arg2) - this seems to be the more up to date version.

Then you have to add some extension points to some plugin.xml (I'm not totally clear where this plugin.xml is).

Where I stab in the dark: it's totally unclear how to work with IRegion, IValidationContext, and IReporter - maybe I'm on the totally wrong way, but what do I get here? How to find calls of that constructor within validation?

I'd extend this question after the first steps get more clear. Outlook, I'll want to add the possibility of quick fixes to the two String version of the constructor and manipulate the code this way - but that's at least two steps ahead, details coming later.

like image 499
outofmind Avatar asked Mar 07 '17 09:03

outofmind


2 Answers

First I have to say that what you are trying to goes beyond normal Java programming. There is no normal way do validation at compile time, except what can be achieved using normal types.

What you want to do also goes beyond what can be done using annotation processors. Annotation processors are kind of semi-normal, in that they are standardised and a part of the Java framework. They are classes that are run during compilation, they get as input the signature of classes and methods, and can be used for validation and code generation.

If you still what to do this there are abnormal ways, however:


Eclipse Plug-in Solution

The solution you seem to be trying is to write an Eclipse plug-in that uses the Eclipse Java tools to do validation. That should work, I don't know how easy it will be, and the validation will only work for users who are using Eclipse.


Checker Solution

The tool which seems best according to my (limited) knowledge is this:

The Checker Framework for static analysis.

It is used for exactly the kind of things that you want to do. It seems reasonably well-documented and easy to set up. It is used for example for doing nullness analysis and compile time validation of regex syntax. There is a tutorial which sounds pretty similar to your thing. There is a chapter in the manual on creating a new checker.

It would probably take a lot of time and effort to make a solution using this, but I also think it seems very interesting!

like image 74
Lii Avatar answered Sep 29 '22 14:09

Lii


NOTE:

This may not the exact solution. But I am trying to discuss possible methods to work around the problem. I have mentioned alternative methods which may help to work. Please join for the discussion.

I would like to try with Java Annotation Processors which introduced in JDK-5 and standardized in JDK-6 under JSR-269 specifications.

It's required to annotate files (java @interfaces) which should validate according to the rules using a custom annotation. If it's not possible to annotate each file, it will have to annotate the package that contain classes to be validated (it's possible to iterate through inner packages also). Following example will demonstrate how to validate classes using annotated classes and annotation processors. I have uploaded sample project into a github repository. Please view project repository on github https://github.com/hjchanna/compile_validation_java

STEP 01: create an annotation interface (@interface)

package com.mac.compile_validation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author hjchanna
 */
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface CompileValidation {

}

STEP 02: create a Processor class, which introduces compile validation rules to the compiler

The processor class should extend from javax.annotation.processing.AbstractProcessor and it should be annotated with @SupportedAnnotationTypes and @SupportedSourceVersion annotations. Please modify the CompileValidationProcessor class according to the exact requirement or validation rules.

package com.mac.compile_validation;

import java.util.List;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.tools.Diagnostic;

/**
 *
 * @author hjchanna
 */
@SupportedAnnotationTypes("com.mac.compile_validation.CompileValidation")
@SupportedSourceVersion(SourceVersion.RELEASE_8)
public class CompileValidationProcessor extends AbstractProcessor {

    /**
     * Processes a set of annotation types on type elements originating from the
     * prior round and returns whether or not these annotation types are claimed
     * by this processor. If {@code
     * true} is returned, the annotation types are claimed and subsequent
     * processors will not be asked to process them; if {@code false} is
     * returned, the annotation types are unclaimed and subsequent processors
     * may be asked to process them. A processor may always return the same
     * boolean value or may vary the result based on chosen criteria.
     *
     * The input set will be empty if the processor supports {@code
     * "*"} and the root elements have no annotations. A {@code
     * Processor} must gracefully handle an empty set of annotations.
     *
     * @param annotations the annotation types requested to be processed
     * @param roundEnv environment for information about the current and prior
     * round
     * @return whether or not the set of annotation types are claimed by this
     * processor
     */
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //Iterate through compiling files which annotated with @CompileValidation
        for (Element elem : roundEnv.getElementsAnnotatedWith(CompileValidation.class)) {
            //find type element for element
            TypeElement typeElement = findEnclosingTypeElement(elem);

            //required parameter types
            TypeElement stringType = processingEnv.getElementUtils().getTypeElement("java.lang.String");
            TypeElement integerType = processingEnv.getElementUtils().getTypeElement("java.lang.Integer");

            //find construtors according to your scenario
            ExecutableElement conA = findConstructor(typeElement, stringType.asType(), integerType.asType());
            ExecutableElement conB = findConstructor(typeElement, stringType.asType(), stringType.asType());

            //check availability of constructors, if not available it should show a warning message in compile time
            if (conA == null || conB == null) {
                String message = "Type " + typeElement + " has malformed constructors.";
                processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, message);
            }

        }
        return true; // no further processing of this annotation type
    }

    /**
     * Returns a constructor which have two parameters and parameter types equal
     * to paramA and paramB. Return null if required constructor is not
     * available.
     *
     * @param typeElement like the class which may constructors encapsulated
     * @param paramA first parameter of required constructor
     * @param paramB second parameter of required constructor
     * @return constructor which have required parameters
     */
    private static ExecutableElement findConstructor(TypeElement typeElement, TypeMirror paramA, TypeMirror paramB) {
        List<ExecutableElement> executableElements = ElementFilter.constructorsIn(typeElement.getEnclosedElements());

        for (ExecutableElement executableElement : executableElements) {
            List<VariableElement> variableElements = (List<VariableElement>) executableElement.getParameters();

            //match constructor params and length
            if (variableElements.size() == 2
                    && variableElements.get(0).asType().equals(paramA)
                    && variableElements.get(1).asType().equals(paramB)) {
                return executableElement;
            }
        }

        return null;
    }

    /**
     * Returns the TypeElement of element e.
     *
     * @param e Element which contain TypeElement
     * @return Type element
     */
    public static TypeElement findEnclosingTypeElement(Element e) {
        while (e != null && !(e instanceof TypeElement)) {
            e = e.getEnclosingElement();
        }

        return TypeElement.class.cast(e);
    }
}

STEP 03: create processing service link file

Then it's required to add a class with name javax.annotation.processing.Processor into the resource path of the project (/src/main/resources/META-INF/services). The file contains only class name of the Processor. According to previous example the configuration file content as follows.

com.mac.compile_validation.CompileValidationProcessor

Previous method is applicable to maven projects. It's possible to inject the configuration file manually into the META-INF/services folder of output .jar file if needed.

STEP 04: disable validation for the current project

Disable annotation processing for current project. If it is enabled, it will fail to build current project since the compiler try to locate the Processor class to validate. But it's still not compiled. So it will fail to build the project because of itself. Add following code to the pom.xml (inside <build> -> <plugin>).

<compilerArgument>-proc:none</compilerArgument>

It's now almost finished. Only thing have to do further is adding build output .jar file dependency into the original project.

It's time to test the project. Annotate required classes with custom annotation which created previously (CompileValidation). It will show a warning if it's fail to validate annotated classes. My output as follows.

build output

ALTERNATIVE SOLUTIONS

  • It's possible to use PMD, which is a java source code scanner. It provides ways to define rules using xml configuration.
  • Try to validate classes using java reflection when it boots up. (this is not what you asked. but it is a good practice to validate stuff before start working as spring, hibernate and other reputed frameworks do.)
  • Try Java Instrumentation API, but only possible reaction is crashing the application if it violated the rule. It's not a good practice.
like image 38
Channa Jayamuni Avatar answered Sep 29 '22 16:09

Channa Jayamuni