Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit b39239a

Browse files
authored
341 - custom constraint message (#395)
1 parent d658385 commit b39239a

File tree

13 files changed

+298
-93
lines changed

13 files changed

+298
-93
lines changed

samples/java-webmvc/src/main/java/capital/scalable/restdocs/example/items/ItemUpdateRequest.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class ItemUpdateRequest {
3535
* Some information about the item.
3636
*/
3737
@NotBlank(groups = English.class)
38-
@Length(max = 20)
38+
@Length(max = 20, message = "Must be max ${max} characters")
3939
@Size.List({
4040
@Size(min = 2, max = 10, groups = German.class),
4141
@Size(min = 4, max = 12, groups = English.class)

samples/shared/src/main/java/capital/scalable/restdocs/example/constraints/Id.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
@Pattern(regexp = "[a-zA-Z0-9]{20}")
4141
public @interface Id {
4242

43-
String message() default "Must be a valid ID";
43+
String message() default "";
4444

4545
Class<?>[] groups() default {};
4646

samples/shared/src/main/java/capital/scalable/restdocs/example/constraints/OneOf.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
@Constraint(validatedBy = OneOfValidator.class)
3939
public @interface OneOf {
4040

41-
String message() default "Must be one of ${value}";
41+
String message() default "";
4242

4343
Class<?>[] groups() default {};
4444

@@ -54,4 +54,3 @@
5454
OneOf[] value();
5555
}
5656
}
57-

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/constraints/ConstraintAndGroupDescriptionResolver.java

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,21 +23,18 @@
2323
import static java.util.Collections.singletonMap;
2424
import static org.apache.commons.lang3.StringUtils.isBlank;
2525
import static org.apache.commons.lang3.StringUtils.trimToEmpty;
26-
import static org.slf4j.LoggerFactory.getLogger;
2726

2827
import java.util.ArrayList;
2928
import java.util.Collections;
3029
import java.util.List;
31-
import java.util.MissingResourceException;
3230

3331
import capital.scalable.restdocs.i18n.SnippetTranslationResolver;
34-
import org.slf4j.Logger;
3532
import org.springframework.restdocs.constraints.Constraint;
3633
import org.springframework.restdocs.constraints.ConstraintDescriptionResolver;
3734

3835
public class ConstraintAndGroupDescriptionResolver implements
3936
ConstraintDescriptionResolver, GroupDescriptionResolver {
40-
private static final Logger log = getLogger(ConstraintAndGroupDescriptionResolver.class);
37+
4138
static final String GROUPS = "groups";
4239
static final String VALUE = "value";
4340

@@ -98,12 +95,6 @@ private String fallbackGroupDescription(Class group, String constraintDescriptio
9895
}
9996

10097
private String resolvePlainDescription(Constraint constraint) {
101-
try {
102-
return delegate.resolveDescription(constraint);
103-
} catch (MissingResourceException e) {
104-
log.debug("No description found for constraint {}: {}. " +
105-
"Fallback to group description.", constraint.getName(), e.getMessage());
106-
return "";
107-
}
98+
return delegate.resolveDescription(constraint);
10899
}
109100
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*-
2+
* #%L
3+
* Spring Auto REST Docs Core
4+
* %%
5+
* Copyright (C) 2015 - 2020 Scalable Capital GmbH
6+
* %%
7+
* Licensed under the Apache License, Version 2.0 (the "License");
8+
* you may not use this file except in compliance with the License.
9+
* You may obtain a copy of the License at
10+
*
11+
* http://www.apache.org/licenses/LICENSE-2.0
12+
*
13+
* Unless required by applicable law or agreed to in writing, software
14+
* distributed under the License is distributed on an "AS IS" BASIS,
15+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16+
* See the License for the specific language governing permissions and
17+
* limitations under the License.
18+
* #L%
19+
*/
20+
package capital.scalable.restdocs.constraints;
21+
22+
import static org.apache.commons.lang3.StringUtils.isNotBlank;
23+
import static org.slf4j.LoggerFactory.getLogger;
24+
25+
import java.util.Locale;
26+
import java.util.MissingResourceException;
27+
import java.util.ResourceBundle;
28+
29+
import org.slf4j.Logger;
30+
import org.springframework.restdocs.constraints.Constraint;
31+
import org.springframework.restdocs.constraints.ConstraintDescriptionResolver;
32+
import org.springframework.util.PropertyPlaceholderHelper;
33+
import org.springframework.util.StringUtils;
34+
35+
/**
36+
* Same as {@link org.springframework.restdocs.constraints.ResourceBundleConstraintDescriptionResolver}
37+
* but with ability to evaluate constraint's message attribute before falling back to property files.
38+
*/
39+
public class DynamicResourceBundleConstraintDescriptionResolver implements ConstraintDescriptionResolver {
40+
41+
private static final Logger log = getLogger(ConstraintAndGroupDescriptionResolver.class);
42+
43+
private final PropertyPlaceholderHelper propertyPlaceholderHelper = new PropertyPlaceholderHelper("${", "}");
44+
45+
private final ResourceBundle defaultDescriptions;
46+
47+
private final ResourceBundle userDescriptions;
48+
49+
public DynamicResourceBundleConstraintDescriptionResolver() {
50+
this(getBundle("ConstraintDescriptions"));
51+
}
52+
53+
public DynamicResourceBundleConstraintDescriptionResolver(ResourceBundle resourceBundle) {
54+
this.defaultDescriptions = getBundle("DefaultConstraintDescriptions");
55+
this.userDescriptions = resourceBundle;
56+
}
57+
58+
private static ResourceBundle getBundle(String name) {
59+
try {
60+
return ResourceBundle.getBundle(
61+
org.springframework.restdocs.constraints.ResourceBundleConstraintDescriptionResolver
62+
.class.getPackage().getName() + "." + name,
63+
Locale.getDefault(), Thread.currentThread().getContextClassLoader());
64+
} catch (MissingResourceException ex) {
65+
return null;
66+
}
67+
}
68+
69+
/**
70+
* First resolves based on overridden message on constraint itself, then falls back to resource bundle resolution
71+
*/
72+
@Override
73+
public String resolveDescription(Constraint constraint) {
74+
String message = (String) constraint.getConfiguration().get("message");
75+
if (isNotBlank(message) && !message.startsWith("{")) {
76+
return this.propertyPlaceholderHelper.replacePlaceholders(message,
77+
new ConstraintPlaceholderResolver(constraint));
78+
}
79+
80+
try {
81+
String key = constraint.getName() + ".description";
82+
return this.propertyPlaceholderHelper.replacePlaceholders(getDescription(key),
83+
new ConstraintPlaceholderResolver(constraint));
84+
} catch (MissingResourceException e) {
85+
log.debug("No description found for constraint {}: {}.", constraint.getName(), e.getMessage());
86+
return "";
87+
}
88+
89+
}
90+
91+
private String getDescription(String key) {
92+
try {
93+
if (this.userDescriptions != null) {
94+
return this.userDescriptions.getString(key);
95+
}
96+
} catch (MissingResourceException ex) {
97+
// Continue and return default description, if available
98+
}
99+
return this.defaultDescriptions.getString(key);
100+
}
101+
102+
private static final class ConstraintPlaceholderResolver implements PropertyPlaceholderHelper.PlaceholderResolver {
103+
104+
private final Constraint constraint;
105+
106+
private ConstraintPlaceholderResolver(Constraint constraint) {
107+
this.constraint = constraint;
108+
}
109+
110+
@Override
111+
public String resolvePlaceholder(String placeholderName) {
112+
Object replacement = this.constraint.getConfiguration().get(placeholderName);
113+
if (replacement == null) {
114+
return null;
115+
}
116+
if (replacement.getClass().isArray()) {
117+
return StringUtils.arrayToDelimitedString((Object[]) replacement, ", ");
118+
}
119+
return replacement.toString();
120+
}
121+
122+
}
123+
124+
}

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/JacksonResultHandlers.java

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,48 +27,67 @@
2727
import static capital.scalable.restdocs.OperationAttributeHelper.setTypeMapping;
2828

2929
import capital.scalable.restdocs.constraints.ConstraintReaderImpl;
30+
import capital.scalable.restdocs.constraints.DynamicResourceBundleConstraintDescriptionResolver;
3031
import capital.scalable.restdocs.i18n.SnippetTranslationManager;
3132
import capital.scalable.restdocs.i18n.SnippetTranslationResolver;
3233
import capital.scalable.restdocs.javadoc.JavadocReaderImpl;
3334
import com.fasterxml.jackson.databind.ObjectMapper;
3435
import org.springframework.restdocs.constraints.ConstraintDescriptionResolver;
35-
import org.springframework.restdocs.constraints.ResourceBundleConstraintDescriptionResolver;
3636
import org.springframework.test.web.servlet.MvcResult;
3737
import org.springframework.test.web.servlet.ResultHandler;
3838
import org.springframework.web.method.HandlerMethod;
3939

4040
public abstract class JacksonResultHandlers {
4141

4242
public static ResultHandler prepareJackson(ObjectMapper objectMapper) {
43-
return new JacksonPreparingResultHandler(objectMapper, new TypeMapping(), SnippetTranslationManager.getDefaultResolver(), new ResourceBundleConstraintDescriptionResolver());
43+
return new JacksonPreparingResultHandler(objectMapper, new TypeMapping(),
44+
SnippetTranslationManager.getDefaultResolver(),
45+
new DynamicResourceBundleConstraintDescriptionResolver());
4446
}
4547

46-
public static ResultHandler prepareJackson(ObjectMapper objectMapper, SnippetTranslationResolver translationResolver) {
47-
return new JacksonPreparingResultHandler(objectMapper, new TypeMapping(), translationResolver, new ResourceBundleConstraintDescriptionResolver());
48+
public static ResultHandler prepareJackson(ObjectMapper objectMapper,
49+
SnippetTranslationResolver translationResolver) {
50+
return new JacksonPreparingResultHandler(objectMapper, new TypeMapping(),
51+
translationResolver,
52+
new DynamicResourceBundleConstraintDescriptionResolver());
4853
}
4954

5055
public static ResultHandler prepareJackson(ObjectMapper objectMapper, TypeMapping typeMapping) {
51-
return new JacksonPreparingResultHandler(objectMapper, typeMapping, SnippetTranslationManager.getDefaultResolver(), new ResourceBundleConstraintDescriptionResolver());
56+
return new JacksonPreparingResultHandler(objectMapper, typeMapping,
57+
SnippetTranslationManager.getDefaultResolver(),
58+
new DynamicResourceBundleConstraintDescriptionResolver());
5259
}
5360

54-
public static ResultHandler prepareJackson(ObjectMapper objectMapper, ConstraintDescriptionResolver constraintDescriptionResolver) {
55-
return new JacksonPreparingResultHandler(objectMapper, new TypeMapping(), SnippetTranslationManager.getDefaultResolver(), constraintDescriptionResolver);
61+
public static ResultHandler prepareJackson(ObjectMapper objectMapper,
62+
ConstraintDescriptionResolver constraintDescriptionResolver) {
63+
return new JacksonPreparingResultHandler(objectMapper, new TypeMapping(),
64+
SnippetTranslationManager.getDefaultResolver(), constraintDescriptionResolver);
5665
}
5766

58-
public static ResultHandler prepareJackson(ObjectMapper objectMapper, TypeMapping typeMapping, SnippetTranslationResolver translationResolver) {
59-
return new JacksonPreparingResultHandler(objectMapper, typeMapping, translationResolver, new ResourceBundleConstraintDescriptionResolver());
67+
public static ResultHandler prepareJackson(ObjectMapper objectMapper, TypeMapping typeMapping,
68+
SnippetTranslationResolver translationResolver) {
69+
return new JacksonPreparingResultHandler(objectMapper, typeMapping,
70+
translationResolver, new DynamicResourceBundleConstraintDescriptionResolver());
6071
}
6172

62-
public static ResultHandler prepareJackson(ObjectMapper objectMapper, SnippetTranslationResolver translationResolver, ConstraintDescriptionResolver constraintDescriptionResolver) {
63-
return new JacksonPreparingResultHandler(objectMapper, new TypeMapping(), translationResolver, constraintDescriptionResolver);
73+
public static ResultHandler prepareJackson(ObjectMapper objectMapper,
74+
SnippetTranslationResolver translationResolver,
75+
ConstraintDescriptionResolver constraintDescriptionResolver) {
76+
return new JacksonPreparingResultHandler(objectMapper, new TypeMapping(),
77+
translationResolver, constraintDescriptionResolver);
6478
}
6579

66-
public static ResultHandler prepareJackson(ObjectMapper objectMapper, TypeMapping typeMapping, ConstraintDescriptionResolver constraintDescriptionResolver) {
67-
return new JacksonPreparingResultHandler(objectMapper, typeMapping, SnippetTranslationManager.getDefaultResolver(), constraintDescriptionResolver);
80+
public static ResultHandler prepareJackson(ObjectMapper objectMapper, TypeMapping typeMapping,
81+
ConstraintDescriptionResolver constraintDescriptionResolver) {
82+
return new JacksonPreparingResultHandler(objectMapper, typeMapping,
83+
SnippetTranslationManager.getDefaultResolver(), constraintDescriptionResolver);
6884
}
6985

70-
public static ResultHandler prepareJackson(ObjectMapper objectMapper, TypeMapping typeMapping, SnippetTranslationResolver translationResolver, ConstraintDescriptionResolver constraintDescriptionResolver) {
71-
return new JacksonPreparingResultHandler(objectMapper, typeMapping, translationResolver, constraintDescriptionResolver);
86+
public static ResultHandler prepareJackson(ObjectMapper objectMapper, TypeMapping typeMapping,
87+
SnippetTranslationResolver translationResolver,
88+
ConstraintDescriptionResolver constraintDescriptionResolver) {
89+
return new JacksonPreparingResultHandler(objectMapper, typeMapping,
90+
translationResolver, constraintDescriptionResolver);
7291
}
7392

7493
private static class JacksonPreparingResultHandler implements ResultHandler {
@@ -78,7 +97,9 @@ private static class JacksonPreparingResultHandler implements ResultHandler {
7897
private final SnippetTranslationResolver translationResolver;
7998
private final ConstraintDescriptionResolver constraintDescriptionResolver;
8099

81-
public JacksonPreparingResultHandler(ObjectMapper objectMapper, TypeMapping typeMapping, SnippetTranslationResolver translationResolver, ConstraintDescriptionResolver constraintDescriptionResolver) {
100+
public JacksonPreparingResultHandler(ObjectMapper objectMapper, TypeMapping typeMapping,
101+
SnippetTranslationResolver translationResolver,
102+
ConstraintDescriptionResolver constraintDescriptionResolver) {
82103
this.objectMapper = new SardObjectMapper(objectMapper);
83104
this.typeMapping = typeMapping;
84105
this.translationResolver = translationResolver;
@@ -95,7 +116,8 @@ public void handle(MvcResult result) throws Exception {
95116
setObjectMapper(result.getRequest(), objectMapper);
96117
initRequestPattern(result.getRequest());
97118
setJavadocReader(result.getRequest(), JavadocReaderImpl.createWithSystemProperty());
98-
setConstraintReader(result.getRequest(), ConstraintReaderImpl.create(objectMapper, translationResolver, constraintDescriptionResolver));
119+
setConstraintReader(result.getRequest(),
120+
ConstraintReaderImpl.create(objectMapper, translationResolver, constraintDescriptionResolver));
99121
setTypeMapping(result.getRequest(), typeMapping);
100122
}
101123
}

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/webflux/WebTestClientInitializer.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import capital.scalable.restdocs.constraints.ConstraintReader;
2323
import capital.scalable.restdocs.constraints.ConstraintReaderImpl;
24+
import capital.scalable.restdocs.constraints.DynamicResourceBundleConstraintDescriptionResolver;
2425
import capital.scalable.restdocs.i18n.SnippetTranslationManager;
2526
import capital.scalable.restdocs.jackson.TypeMapping;
2627
import capital.scalable.restdocs.javadoc.JavadocReader;
@@ -30,7 +31,6 @@
3031
import org.springframework.context.ApplicationContext;
3132
import org.springframework.context.ConfigurableApplicationContext;
3233
import org.springframework.core.Ordered;
33-
import org.springframework.restdocs.constraints.ResourceBundleConstraintDescriptionResolver;
3434
import org.springframework.restdocs.snippet.Snippet;
3535
import org.springframework.web.method.HandlerMethod;
3636
import org.springframework.web.reactive.DispatcherHandler;
@@ -119,7 +119,8 @@ public static Snippet prepareSnippets(ApplicationContext context, TypeMapping ty
119119

120120
// create ConstraintReader and put it in operation attributes:
121121
operation.getAttributes().put(ConstraintReader.class.getName(),
122-
ConstraintReaderImpl.create(objectMapper, SnippetTranslationManager.getDefaultResolver(), new ResourceBundleConstraintDescriptionResolver()));
122+
ConstraintReaderImpl.create(objectMapper, SnippetTranslationManager.getDefaultResolver(),
123+
new DynamicResourceBundleConstraintDescriptionResolver()));
123124

124125
// create TypeMapping and put it in operation attributes:
125126
operation.getAttributes().put(TypeMapping.class.getName(),

spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/constraints/ConstraintAndGroupDescriptionResolverTest.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,7 @@ public void noDescriptionIsNotResolved() {
7373
Map<String, Object> configuration = new HashMap<>();
7474
configuration.put(GROUPS, new Class<?>[]{});
7575
Constraint constraint = new Constraint("Constraint", configuration);
76-
when(delegate.resolveDescription(eq(constraint)))
77-
.thenThrow(MissingResourceException.class);
76+
when(delegate.resolveDescription(eq(constraint))).thenReturn("");
7877
// when
7978
String description = resolver.resolveDescription(constraint);
8079
// then
@@ -87,8 +86,7 @@ public void noDescriptionWithGroupsIsResolved() {
8786
Map<String, Object> configuration = new HashMap<>();
8887
configuration.put(GROUPS, new Class<?>[]{Update.class});
8988
Constraint constraint = new Constraint("Constraint", configuration);
90-
when(delegate.resolveDescription(eq(constraint)))
91-
.thenThrow(MissingResourceException.class);
89+
when(delegate.resolveDescription(eq(constraint))).thenReturn("");
9290
// when
9391
String description = resolver.resolveDescription(constraint);
9492
// then
@@ -131,8 +129,8 @@ public void groupDescriptionIsNotResolved() {
131129
configuration.put(GROUPS, new Class<?>[]{Update.class});
132130
Constraint constraint = new Constraint("Constraint", configuration);
133131
when(delegate.resolveDescription(eq(constraint))).thenReturn("Must be it");
134-
when(delegate.resolveDescription(not(eq(constraint))))
135-
.thenThrow(MissingResourceException.class);
132+
when(delegate.resolveDescription(not(eq(constraint)))).thenReturn("");
133+
136134
// when
137135
String description = resolver.resolveDescription(constraint);
138136
// then

0 commit comments

Comments
 (0)