Skip to content

Change Jakarta annotation attribute map generation #35

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 22, 2023
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
40 changes: 40 additions & 0 deletions blackbox-test/src/test/java/example/jakarta/JCustomer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package example.jakarta;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;
import jakarta.validation.Valid;

@Valid
public class JCustomer {

@NotBlank @Size(max = 5)
final String name;

@NotBlank @Size(max = 7, message = "My custom error message with max {max}")
final String other;

@Size(min = 2, max = 4)
final String minMax;

public JCustomer(String name, String other, String minMax) {
this.name = name;
this.other = other;
this.minMax = minMax;
}

public JCustomer(String name, String other) {
this(name, other, "val");
}

public String getName() {
return name;
}

public String getOther() {
return other;
}

public String minMax() {
return minMax;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package example.jakarta;

import io.avaje.validation.ConstraintViolation;
import io.avaje.validation.ConstraintViolationException;
import io.avaje.validation.Validator;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
import java.util.Locale;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.fail;

class JCustomerMessageTest {

final Validator validator = Validator.builder().build();

@Test
void valid() {
var cust = new JCustomer("Rob", "Other");
validator.validate(cust);
}

@Test
void blank() {
var violation = one(new JCustomer("", "Other"));
assertThat(violation.message()).isEqualTo("must not be blank");
}

@Test
void blankDE() {
var violation = one(new JCustomer("", "Other"), Locale.GERMAN);
assertThat(violation.message()).isEqualTo("darf nicht leer sein");
}

@Test
void sizeMax() {
var violation = one(new JCustomer("NameIsTooLarge", "Other"));
assertThat(violation.message()).isEqualTo("size must be between 0 and 5");
}

@Test
void sizeMaxDE() {
var violation = one(new JCustomer("NameIsTooLarge", "Other"), Locale.GERMAN);
assertThat(violation.message()).isEqualTo("Größe muss zwischen 0 und 5 sein");
}

@Test
void sizeMinMax() {
var violation = one(new JCustomer("valid", "Other", "TooLarge"));
assertThat(violation.message()).isEqualTo("size must be between 2 and 4");
}

@Test
void sizeMinMaxDE() {
var violation = one(new JCustomer("valid", "Other", "TooLarge"), Locale.GERMAN);
assertThat(violation.message()).isEqualTo("Größe muss zwischen 2 und 4 sein");
}

@Test
void sizeMaxCustomMessage() {
var violation = one(new JCustomer("Valid", "OtherTooLargeForThis"));
assertThat(violation.message()).isEqualTo("My custom error message with max 7");
}

@Test
void sizeMaxCustomMessageDE() {
var violation = one(new JCustomer("Valid", "OtherTooLargeForThis"));
assertThat(violation.message()).isEqualTo("My custom error message with max 7");
}

ConstraintViolation one(Object any) {
return one(any, Locale.ENGLISH);
}

ConstraintViolation one(Object any, Locale locale) {
try {
validator.validate(any, locale);
fail("not expected");
return null;
} catch (ConstraintViolationException e) {
var violations = new ArrayList<>(e.violations());
assertThat(violations).hasSize(1);
return violations.get(0);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,92 +1,153 @@
package io.avaje.validation.generator;

import static java.util.stream.Collectors.joining;
import java.util.*;

import java.util.List;
import java.util.Optional;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementFilter;

final class AnnotationUtil {

interface Handler {
String attributes(AnnotationMirror annotationMirror, Element element);
}

static final Handler defaultHandler = new StandardHandler();
static final Map<String, Handler> handlers = new HashMap<>();
static {
final var pattern = new PatternHandler();
handlers.put("avaje.Pattern", pattern);
handlers.put("jakarta.validation.constraints.Pattern", pattern);

Handler jakartaHandler = new JakartaHandler();
handlers.put("jakarta.validation.constraints.NotBlank", jakartaHandler);
handlers.put("jakarta.validation.constraints.Size", jakartaHandler);
}

private AnnotationUtil() {}

public static String getAnnotationAttributMap(AnnotationMirror annotationMirror) {
static String annotationAttributeMap(AnnotationMirror annotationMirror) {
final Element element = annotationMirror.getAnnotationType().asElement();
final Handler handler = handlers.get(element.toString());
return Objects.requireNonNullElse(handler, defaultHandler).attributes(annotationMirror, element);
}

static abstract class BaseHandler implements Handler {
final StringBuilder sb = new StringBuilder("Map.of(");
boolean first = true;
final var patternOp = PatternPrism.isInstance(annotationMirror);

if (patternOp.isPresent()) {
patternOp.ifPresent(p -> pattern(sb, p));
return sb.toString();
@SuppressWarnings("unchecked")
final void writeVal(final StringBuilder sb, final AnnotationValue annotationValue) {
final var value = annotationValue.getValue();
// handle array values
if (value instanceof List) {
sb.append("List.of(");
boolean first = true;

for (final AnnotationValue listValue : (List<AnnotationValue>) value) {
if (!first) {
sb.append(", ");
}
writeVal(sb, listValue);
first = false;
}
sb.append(")");
// Handle enum values
} else if (value instanceof final VariableElement element) {
sb.append(element.asType().toString()).append(".").append(element);
// handle annotation values
} else if (value instanceof AnnotationMirror) {
sb.append("\"Annotation Parameters Not Supported\"");
} else {
sb.append(annotationValue);
}
}
}
static class PatternHandler extends BaseHandler {

for (final ExecutableElement member :
ElementFilter.methodsIn(
annotationMirror.getAnnotationType().asElement().getEnclosedElements())) {
@Override
public String attributes(AnnotationMirror annotationMirror, Element element) {
return new PatternHandler().writeAttributes(annotationMirror);
}

final var value =
Optional.<AnnotationValue>ofNullable(annotationMirror.getElementValues().get(member))
.orElseGet(member::getDefaultValue);
if (value == null) {
continue;
String writeAttributes(AnnotationMirror annotationMirror) {
final var patternOp = PatternPrism.isInstance(annotationMirror);
patternOp.ifPresent(p -> pattern(sb, p));
return sb.toString();
}
private static void pattern(StringBuilder sb, PatternPrism prism) {
if (prism.regexp() != null) {
sb.append("\"regexp\",\"").append(prism.regexp()).append("\"");
}
if (!first) {
sb.append(", ");
if (prism.message() != null) {
sb.append(", \"message\",\"").append(prism.message()).append("\"");
}
sb.append("\"" + member.getSimpleName() + "\"").append(",");
writeVal(sb, value);
first = false;
if (!prism.flags().isEmpty()) {
sb.append(", \"flags\",List.of(").append(String.join(", ", prism.flags())).append(")");
}
sb.append(")");
}
sb.append(")");
return sb.toString();
}

private static void pattern(StringBuilder sb, PatternPrism prism) {
if (prism.regexp() != null) {
sb.append("\"regexp\",\"" + prism.regexp() + "\"");
static class StandardHandler extends BaseHandler {

@Override
public String attributes(AnnotationMirror annotationMirror, Element element) {
return new StandardHandler().writeAttributes(annotationMirror, element);
}

if (prism.message() != null) {
sb.append(", \"message\",\"" + prism.message() + "\"");
String writeAttributes(AnnotationMirror annotationMirror, Element element) {
for (final ExecutableElement member : ElementFilter.methodsIn(element.getEnclosedElements())) {
final AnnotationValue value = annotationMirror.getElementValues().get(member);
final AnnotationValue defaultValue = member.getDefaultValue();
if (value == null && defaultValue == null) {
continue;
}
writeAttribute(member.getSimpleName(), value, defaultValue);
}
sb.append(")");
return sb.toString();
}
if (!prism.flags().isEmpty()) {
sb.append(", \"flags\",List.of(" + prism.flags().stream().collect(joining(", ")) + ")");

void writeAttribute(Name simpleName, AnnotationValue value, AnnotationValue defaultValue) {
writeAttributeKey(simpleName.toString());
if (value != null) {
writeVal(sb, value);
} else {
writeVal(sb, defaultValue);
}
}

sb.append(")");
void writeAttributeKey(String name) {
if (!first) {
sb.append(", ");
}
first = false;
sb.append("\"").append(name).append("\",");
}
}

private static void writeVal(final StringBuilder sb, final AnnotationValue annotationValue) {
final var value = annotationValue.getValue();
// handle array values
if (value instanceof List) {
sb.append("List.of(");
boolean first = true;
static class JakartaHandler extends StandardHandler {

for (final AnnotationValue listValue : (List<AnnotationValue>) value) {
@Override
public String attributes(AnnotationMirror annotationMirror, Element element) {
return new JakartaHandler().writeAttributes(annotationMirror, element);
}

if (!first) {
sb.append(", ");
@Override
void writeAttribute(Name simpleName, AnnotationValue value, AnnotationValue defaultValue) {
final String name = simpleName.toString();
if (value == null) {
if ("message".equals(name)) {
final String msgKey = defaultValue.toString().replace("{jakarta.validation.constraints.", "{avaje.");
writeAttributeKey("message");
sb.append(msgKey);
} else if (!name.equals("payload") && !name.equals("groups")) {
super.writeAttribute(simpleName, null, defaultValue);
}

writeVal(sb, listValue);
first = false;
} else {
super.writeAttribute(simpleName, value, defaultValue);
}
sb.append(")");
// Handle enum values
} else if (value instanceof final VariableElement element) {
sb.append(element.asType().toString() + "." + element.toString());
// handle annotation values
} else if (value instanceof AnnotationMirror) {

sb.append("\"Annotation Parameters Not Supported\"");

} else {
sb.append(annotationValue.toString());
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ final class FieldReader {
.collect(
toMap(
a -> GenericType.parse(a.getAnnotationType().toString()),
AnnotationUtil::getAnnotationAttributMap));
AnnotationUtil::annotationAttributeMap));
final String shortType = genericType.shortType();
adapterShortType = initAdapterShortType(shortType);
adapterFieldName = initShortName();
Expand Down