Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
package com.github.joschi.jadconfig;

import com.github.joschi.jadconfig.documentation.Documentation;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import java.util.Arrays;
import java.util.Optional;
import java.util.Set;

@javax.annotation.processing.SupportedAnnotationTypes("com.github.joschi.jadconfig.Parameter")
@javax.annotation.processing.SupportedSourceVersion(SourceVersion.RELEASE_21)
public class ParameterDocumentationValidator extends AbstractProcessor {

private static final int LINE_LENGTH_WARNING = 120;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
}


@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
annotations.stream()
.flatMap(annotation -> roundEnv.getElementsAnnotatedWith(annotation).stream())
.map(element -> (VariableElement) element)
.forEach(this::processField);
return false; // do not claim this annotation, let other processors handle it as well
}

private void processField(VariableElement field) {
final String fieldName = getFieldName(field);

final AnnotationMirror parameterAnnotationMirror = getAnnotationMirror(field, Parameter.class)
.orElseThrow(() -> new IllegalStateException("This should not happen, field should be always annotated with @Parameter"));

final Optional<? extends AnnotationMirror> documentationAnnotationMirror = getAnnotationMirror(field, Documentation.class);

final String parameterName = getParameterValue(parameterAnnotationMirror);

documentationAnnotationMirror.ifPresentOrElse(documentationAnnotation -> {
final boolean isDocumentationVisible = isDocumentationVisible(documentationAnnotation);
final Optional<String> documentationText = getDocumentationText(documentationAnnotation);

if (isDocumentationVisible) {

documentationText.ifPresentOrElse(docs -> {
if (Documentation.MISSING.equals(docs)) {
processingEnv.getMessager().printWarning("Property " + parameterName + " assigned to field " + fieldName + " has TDB documentation, please write proper doc!", field);
}

final int maxLineLength = maxLineLength(docs);
if (maxLineLength > LINE_LENGTH_WARNING) {
processingEnv.getMessager().printWarning("Property " + parameterName + " assigned to field " + fieldName + " has too long lines (" + maxLineLength + "), please consider splitting it into multiple lines.", field);
}
}, () -> processingEnv.getMessager().printError("Property " + parameterName + " assigned to field " + fieldName + " has no documentation available. Please, add @Documentation annotation value!", field));

}
}, () -> processingEnv.getMessager().printError("Property " + parameterName + " assigned to field " + fieldName + " has no documentation available. Please, add @Documentation annotation!", field));
}

private int maxLineLength(String docs) {
return Arrays.stream(docs.split("\n"))
.map(String::trim)
.mapToInt(String::length)
.max()
.orElse(0);
}

private Optional<String> getDocumentationText(AnnotationMirror documentationAnnotationMirror) {
return documentationAnnotationMirror.getElementValues().entrySet().stream()
.filter(entry -> entry.getKey().getSimpleName().toString().equals("value"))
.map(entry -> (String) entry.getValue().getValue())
.findFirst()
.filter(value -> !StringUtils.isEmpty(value));

}

private static String getParameterValue(AnnotationMirror annotationMirror) {
return annotationMirror.getElementValues().entrySet().stream()
.filter(entry -> entry.getKey().getSimpleName().toString().equals("value"))
.map(entry -> (String) entry.getValue().getValue())
.findFirst()
.orElseThrow(() -> new IllegalStateException("Value is mandatory!"));
}

private static boolean isDocumentationVisible(AnnotationMirror annotationMirror) {
return annotationMirror.getElementValues().entrySet().stream()
.filter(entry -> entry.getKey().getSimpleName().toString().equals("visible"))
.map(entry -> (Boolean) entry.getValue().getValue())
.findFirst()
.orElse(true);
}

private static Optional<? extends AnnotationMirror> getAnnotationMirror(Element element, Class<?> annotationClass) {
return element.getAnnotationMirrors()
.stream()
.filter(mirror -> mirror.getAnnotationType().toString().equals(
annotationClass.getCanonicalName()))
.findFirst();
}

private static String getFieldName(VariableElement field) {
final String className = getParentClassName(field);
return className + "#" + field.getSimpleName().toString();
}

private static String getParentClassName(VariableElement field) {
final Element enclosingElement = field.getEnclosingElement();
if (enclosingElement.getKind() == ElementKind.CLASS) {
final TypeElement classElement = (TypeElement) enclosingElement;
return classElement.getQualifiedName().toString();
} else {
return enclosingElement.getSimpleName().toString();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Documentation {

String MISSING = "tbd";

/**
* We don't want to expose some configuration fields to users. They are internal, required for system packages functionality
* or deprecated. Set to false if you want to hide this field from documentation.
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
com.github.joschi.jadconfig.ParameterTypesValidator
com.github.joschi.jadconfig.ParameterDocumentationValidator
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.github.joschi.jadconfig;

import io.toolisticon.cute.Cute;
import io.toolisticon.cute.CuteApi;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class ParameterDocumentationValidatorTest {

CuteApi.BlackBoxTestSourceFilesInterface compileTestBuilder;

@BeforeEach
public void init() {
compileTestBuilder = Cute
.blackBoxTest()
.given()
.processor(ParameterDocumentationValidator.class);
}

@Test
public void testDocumentationProcessor() {
compileTestBuilder
.andSourceFiles("/com/github/joschi/jadconfig/MyDocumentationValidatorTestClass.java")
.whenCompiled().thenExpectThat()
.compilationFails()
.andThat()
.compilerMessage()
.ofKindError().atLine(18).atColumn(22).equals("Property my_duration assigned to field MyDocumentationValidatorTestClass#myDurationField has no documentation available. Please, add @Documentation annotation!")
.executeTest();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import com.github.joschi.jadconfig.Parameter;
import com.github.joschi.jadconfig.documentation.Documentation;

import java.time.Duration;

public class MyDocumentationValidatorTestClass {

@Documentation(visible = false)
@Parameter(value = "my_hidden_property")
private String hiddenProperty;

@Documentation("configure some int value")
@Parameter(value = "my_int", required = true)
private int myIntField = 10;

// here's no @Documentation annotation, should lead to an error
@Parameter(value = "my_duration")
private Duration myDurationField = Duration.ofDays(1);

}