Skip to content

Commit 1205844

Browse files
rbygraveSentryMan
andauthored
Add support for sharing instances of NotNull NotBlank adapters (#277)
When these are not customised and use the default group and default message, we can use shared "default adapters" instead of creating a new adapter for each instance. This will be beneficial (reduce memory consumption) for the case where lots of NotNull & NotBlank etc used without any customisation of the groups or message. This approach can be extended to also apply to other common validators like Email, UUID, AssertTrue, Positive etc. Co-authored-by: Josiah Noel <32279667+SentryMan@users.noreply.github.com>
1 parent c7832b7 commit 1205844

File tree

5 files changed

+139
-21
lines changed

5 files changed

+139
-21
lines changed

validator/src/main/java/io/avaje/validation/adapter/ValidationContext.java

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ interface Message {
116116
*/
117117
String lookupkey();
118118
}
119+
119120
/** Request to create a Validation Adapter. */
120121
interface AdapterCreateRequest {
121122

@@ -132,7 +133,7 @@ interface AdapterCreateRequest {
132133
Map<String, Object> attributes();
133134

134135
/** Return the attribute for the given key. */
135-
<T> T attribute(String key);
136+
<T> T attribute(String key);
136137

137138
/** Return the message to use */
138139
Message message();
@@ -143,7 +144,19 @@ interface AdapterCreateRequest {
143144
/** Return the target type */
144145
String targetType();
145146

147+
/** Return true if the groups is ONLY the default group */
148+
boolean isDefaultGroupOnly();
149+
146150
/** Clone and return the request with a new value attribute */
147151
AdapterCreateRequest withValue(long value);
148152
}
153+
154+
/** Used to build default ValidationAdapters with the default group and message. */
155+
interface RequestBuilder {
156+
157+
/**
158+
* Build a default AdapterCreateRequest with the appropriate default message.
159+
*/
160+
AdapterCreateRequest defaultRequest(String defaultMessage);
161+
}
149162
}

validator/src/main/java/io/avaje/validation/core/CoreAdapterBuilder.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@
1212
import java.util.concurrent.ConcurrentHashMap;
1313
import java.util.function.Supplier;
1414

15+
import io.avaje.validation.adapter.ConstraintAdapter;
16+
import io.avaje.validation.groups.Default;
1517
import org.jspecify.annotations.Nullable;
1618

1719
import io.avaje.validation.adapter.ValidationAdapter;
1820
import io.avaje.validation.adapter.ValidationContext;
1921
import io.avaje.validation.core.adapters.BasicAdapters;
2022
import io.avaje.validation.core.adapters.FuturePastAdapterFactory;
2123
import io.avaje.validation.core.adapters.NumberAdapters;
22-
import io.avaje.validation.groups.Default;
2324
import io.avaje.validation.spi.AdapterFactory;
2425
import io.avaje.validation.spi.AnnotationFactory;
2526

@@ -41,7 +42,10 @@ final class CoreAdapterBuilder {
4142
this.context = context;
4243
this.factories.addAll(userFactories);
4344
this.annotationFactories.addAll(userAnnotationFactories);
44-
this.annotationFactories.add(BasicAdapters.FACTORY);
45+
// bootstrap the builtin factories potentially with default adapters
46+
// that use the default group and default message
47+
var requestBuilder = new RequestBuilder(context);
48+
this.annotationFactories.add(BasicAdapters.factory(requestBuilder));
4549
this.annotationFactories.add(NumberAdapters.FACTORY);
4650
this.annotationFactories.add(new FuturePastAdapterFactory(clockSupplier, temporalTolerance));
4751
}
@@ -107,7 +111,22 @@ <T> ValidationAdapter<T> buildAnnotation(
107111
return NoOpValidator.INSTANCE;
108112
}
109113

110-
record Request(
114+
private static final class RequestBuilder implements ValidationContext.RequestBuilder {
115+
116+
private final DValidator context;
117+
118+
private RequestBuilder(DValidator context) {
119+
this.context = context;
120+
}
121+
122+
@Override
123+
public ValidationContext.AdapterCreateRequest defaultRequest(String defaultMessage) {
124+
// ConstraintAdapter.class is just a placeholder and not meaningful
125+
return new Request(context, ConstraintAdapter.class, DEFAULT_GROUP, Map.of("message", defaultMessage));
126+
}
127+
}
128+
129+
private record Request(
111130

112131
ValidationContext ctx,
113132
Class<? extends Annotation> annotationType,
@@ -116,6 +135,11 @@ record Request(
116135

117136
) implements ValidationContext.AdapterCreateRequest {
118137

138+
@Override
139+
public boolean isDefaultGroupOnly() {
140+
return DEFAULT_GROUP.equals(groups);
141+
}
142+
119143
@Override
120144
public String targetType() {
121145
return attribute("_type");

validator/src/main/java/io/avaje/validation/core/adapters/BasicAdapters.java

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,75 @@
1414
import io.avaje.validation.adapter.ValidationAdapter;
1515
import io.avaje.validation.adapter.ValidationContext;
1616
import io.avaje.validation.adapter.ValidationContext.AdapterCreateRequest;
17+
import io.avaje.validation.adapter.ValidationContext.RequestBuilder;
1718
import io.avaje.validation.adapter.ValidationRequest;
1819
import io.avaje.validation.spi.AnnotationFactory;
1920

2021
public final class BasicAdapters {
2122
private static final String LENGTH_MAX = "{avaje.Length.max.message}";
23+
private static final String NOT_NULL_MESSAGE = "{avaje.NotNull.message}";
24+
private static final String NULL_MESSAGE = "{avaje.Null.message}";
25+
private static final String NOT_BLANK_MESSAGE = "{avaje.NotBlank.message}";
2226

2327
private BasicAdapters() {}
2428

25-
public static final AnnotationFactory FACTORY =
26-
request ->
27-
switch (request.annotationType().getSimpleName()) {
28-
case "Email" -> new EmailAdapter(request);
29-
case "UUID" -> new UuidAdapter(request);
30-
case "URI" -> new UriAdapter(request);
31-
case "Null" -> new NullableAdapter(request, true);
32-
case "NotNull", "NonNull" -> new NullableAdapter(request, false);
33-
case "AssertTrue" -> new AssertBooleanAdapter(request, true);
34-
case "AssertFalse" -> new AssertBooleanAdapter(request, false);
35-
case "NotBlank" -> new NotBlankAdapter(request);
36-
case "NotEmpty" -> new NotEmptyAdapter(request);
37-
case "Pattern" -> new PatternAdapter(request);
38-
case "Size", "Length" -> new SizeAdapter(request);
39-
case "Valid" -> new ValidAdapter(request);
40-
default -> null;
41-
};
29+
public static AnnotationFactory factory(RequestBuilder requestBuilder) {
30+
return new Factory(requestBuilder);
31+
}
32+
33+
private static final class Factory implements AnnotationFactory {
34+
35+
private final NullableAdapter defaultNotNullAdapter;
36+
private final NullableAdapter defaultNullAdapter;
37+
private final NotBlankAdapter defaultNotBlankAdapter;
38+
39+
Factory(RequestBuilder reqBuilder) {
40+
// create default adapters that will be shared instances (when no groups or message customisation)
41+
this.defaultNotNullAdapter = new NullableAdapter(reqBuilder.defaultRequest(NOT_NULL_MESSAGE), false);
42+
this.defaultNullAdapter = new NullableAdapter(reqBuilder.defaultRequest(NULL_MESSAGE), true);
43+
this.defaultNotBlankAdapter = new NotBlankAdapter(reqBuilder.defaultRequest(NOT_BLANK_MESSAGE));
44+
}
45+
46+
@Override
47+
public ValidationAdapter<?> create(AdapterCreateRequest request) {
48+
return switch (request.annotationType().getSimpleName()) {
49+
case "Email" -> new EmailAdapter(request);
50+
case "UUID" -> new UuidAdapter(request);
51+
case "URI" -> new UriAdapter(request);
52+
case "Null" -> nullable(request);
53+
case "NotNull", "NonNull" -> notNull(request);
54+
case "AssertTrue" -> new AssertBooleanAdapter(request, true);
55+
case "AssertFalse" -> new AssertBooleanAdapter(request, false);
56+
case "NotBlank" -> notBlank(request);
57+
case "NotEmpty" -> new NotEmptyAdapter(request);
58+
case "Pattern" -> new PatternAdapter(request);
59+
case "Size", "Length" -> new SizeAdapter(request);
60+
case "Valid" -> new ValidAdapter(request);
61+
default -> null;
62+
};
63+
}
64+
65+
private ValidationAdapter<?> notBlank(AdapterCreateRequest request) {
66+
if (NotBlankAdapter.isDefault(request)) {
67+
return defaultNotBlankAdapter;
68+
}
69+
return new NotBlankAdapter(request);
70+
}
71+
72+
private ValidationAdapter<?> notNull(AdapterCreateRequest request) {
73+
if (request.isDefaultGroupOnly() && NOT_NULL_MESSAGE.equals(request.attribute("message"))) {
74+
return defaultNotNullAdapter;
75+
}
76+
return new NullableAdapter(request, false);
77+
}
78+
79+
private ValidationAdapter<?> nullable(AdapterCreateRequest request) {
80+
if (request.isDefaultGroupOnly() && NULL_MESSAGE.equals(request.attribute("message"))) {
81+
return defaultNullAdapter;
82+
}
83+
return new NullableAdapter(request, true);
84+
}
85+
}
4286

4387
static sealed class PatternAdapter extends AbstractConstraintAdapter<CharSequence>
4488
permits EmailAdapter {
@@ -153,6 +197,12 @@ private static final class NotBlankAdapter implements ValidationAdapter<CharSequ
153197
}
154198
}
155199

200+
private static boolean isDefault(AdapterCreateRequest request) {
201+
return request.isDefaultGroupOnly()
202+
&& standardMessage(request)
203+
&& maxLength(request) == 0;
204+
}
205+
156206
private static int maxLength(AdapterCreateRequest request) {
157207
final Integer max = request.attribute("max");
158208
return Objects.requireNonNullElse(max, 0);

validator/src/test/java/io/avaje/validation/core/adapters/NotBlankTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ void continueOnInvalid_expect_false() {
3030
assertThat(notBlankMaxAdapter.validate(null, request, "foo")).isFalse();
3131
assertThat(notBlankMaxAdapter.validate("", request, "foo")).isFalse();
3232
}
33+
3334
@Test
3435
void continueOnInvalid_expect_true_when_maxExceeded() {
3536
assertThat(notBlankMaxAdapter.validate("01234", request, "foo")).isTrue();
@@ -50,4 +51,19 @@ void testBlank() {
5051
assertThat(notBlankAdapter.validate("", request)).isFalse();
5152
assertThat(notBlankAdapter.validate(" ", request)).isFalse();
5253
}
54+
55+
@Test
56+
void defaultInstance() {
57+
var adapter0 = ctx.adapter(NotBlank.class, Map.of("message", "{avaje.NotBlank.message}"));
58+
var adapter1 = ctx.adapter(NotBlank.class, Map.of("message", "{avaje.NotBlank.message}"));
59+
var adapter2 = ctx.adapter(NotBlank.class, Map.of("message", "{avaje.NotBlank.message}", "max", 0));
60+
assertThat(adapter1).isSameAs(adapter0).isSameAs(adapter2);
61+
62+
// these are different instances
63+
var adapterDiff1 = ctx.adapter(NotBlank.class, Map.of("message", "Other message"));
64+
var adapterDiff2 = ctx.adapter(NotBlank.class, Map.of("message", "{avaje.NotBlank.message}", "max", 4));
65+
assertThat(adapter0).isNotSameAs(adapterDiff1);
66+
assertThat(adapter0).isNotSameAs(adapterDiff2);
67+
assertThat(adapterDiff1).isNotSameAs(adapterDiff2);
68+
}
5369
}

validator/src/test/java/io/avaje/validation/core/adapters/NullableAdapterTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import static org.assertj.core.api.Assertions.assertThat;
44

55
import java.util.Map;
6+
import java.util.Set;
67

8+
import io.avaje.validation.groups.Default;
79
import org.junit.jupiter.api.Test;
810

911
import io.avaje.validation.adapter.ValidationAdapter;
@@ -54,4 +56,17 @@ void testNotNull() {
5456
assertThat(isValid(notNulladapter, 0)).isTrue();
5557
assertThat(isValid(nonNulladapter, 0)).isTrue();
5658
}
59+
60+
@Test
61+
void defaultInstance() {
62+
var adapter0 = ctx.adapter(NotNull.class, Map.of("message", "{avaje.NotNull.message}"));
63+
var adapter1 = ctx.adapter(NotNull.class, Map.of("message", "{avaje.NotNull.message}"));
64+
var adapter2 = ctx.adapter(NotNull.class, Set.of(Default.class), "{avaje.NotNull.message}", Map.of("message", "{avaje.NotNull.message}"));
65+
assertThat(adapter1).isSameAs(adapter0).isSameAs(adapter2);
66+
67+
// these are different instances
68+
var adapterDiff1 = ctx.adapter(NotNull.class, Map.of("message", "Other message"));
69+
var adapterDiff2 = ctx.adapter(NotNull.class, Set.of(NullableAdapterTest.class), "{avaje.NotNull.message}", Map.of("message", "{avaje.NotNull.message}"));
70+
assertThat(adapter0).isNotSameAs(adapterDiff1).isNotSameAs(adapterDiff2);
71+
}
5772
}

0 commit comments

Comments
 (0)