@@ -4,18 +4,25 @@ import {{javaxPackage}}.validation.Constraint
44import { {javaxPackage} }.validation.ConstraintValidator
55import { {javaxPackage} }.validation.ConstraintValidatorContext
66import { {javaxPackage} }.validation.Payload
7- import { {javaxPackage} }.validation.constraintvalidation.SupportedValidationTarget
8- import { {javaxPackage} }.validation.constraintvalidation.ValidationTarget
97import org.springframework.data.domain.Pageable
108
119/**
12- * Validates that sort properties in a [Pageable] parameter match the allowed values.
10+ * Validates that sort properties in the annotated [Pageable] parameter match the allowed values.
1311 *
14- * This annotation can only be applied to methods that have a [Pageable] parameter.
15- * The validator checks that each sort property and direction combination in the [Pageable]
16- * matches one of the strings specified in [allowedValues].
12+ * Apply directly on a `pageable: Pageable` parameter. The validator checks that each sort
13+ * property and direction combination in the [Pageable] matches one of the strings specified
14+ * in [allowedValues].
1715 *
18- * Expected value format: `"property,direction"` (e.g. `"id,asc"`, `"name,desc"`).
16+ * Two formats are accepted in [allowedValues]:
17+ * - `"property,direction"` — permits only the specific direction (e.g. `"id,asc"`, `"name,desc"`).
18+ * Direction matching is case-insensitive: `"id,ASC"` and `"id,asc"` are treated identically.
19+ * - `"property"` — permits any direction for that property (e.g. `"id"` matches `sort=id,asc`
20+ * and `sort=id,desc`). Note: because Spring always normalises a bare `sort=id` to ascending
21+ * before the validator runs, bare property names in [allowedValues] effectively allow all
22+ * directions — the original omission of a direction cannot be detected.
23+ *
24+ * Both formats may be mixed freely. For example `["id", "name,desc"]` allows `id` in any
25+ * direction but restricts `name` to descending only.
1926 *
2027 * @property allowedValues The allowed sort strings (e.g. `["id,asc", "id,desc"]`)
2128 * @property groups Validation groups (optional)
@@ -25,34 +32,38 @@ import org.springframework.data.domain.Pageable
2532@MustBeDocumented
2633@Retention(AnnotationRetention.RUNTIME)
2734@Constraint(validatedBy = [SortValidator::class])
28- @Target(AnnotationTarget.FUNCTION )
35+ @Target(AnnotationTarget.VALUE_PARAMETER )
2936annotation class ValidSort(
3037 val allowedValues: Array<String >,
3138 val groups: Array<kotlin .reflect.KClass <* >> = [],
3239 val payload: Array<kotlin .reflect.KClass <out Payload >> = [],
3340 val message: String = "Invalid sort column"
3441)
3542
36- @SupportedValidationTarget(ValidationTarget.PARAMETERS)
37- class SortValidator : ConstraintValidator<ValidSort , Array <Any? >> {
43+ class SortValidator : ConstraintValidator<ValidSort , Pageable > {
3844
3945 private lateinit var allowedValues: Set< String>
4046
4147 override fun initialize(constraintAnnotation: ValidSort) {
42- allowedValues = constraintAnnotation.allowedValues.toSet()
48+ allowedValues = constraintAnnotation.allowedValues.map { entry ->
49+ DIRECTION_ASC_SUFFIX.replace(entry, " ,asc" )
50+ .let { DIRECTION_DESC_SUFFIX.replace(it, " ,desc" ) }
51+ }.toSet()
52+ }
53+
54+ private companion object {
55+ val DIRECTION_ASC_SUFFIX = Regex(" ,ASC$" , RegexOption.IGNORE_CASE)
56+ val DIRECTION_DESC_SUFFIX = Regex(" ,DESC$" , RegexOption.IGNORE_CASE)
4357 }
4458
45- override fun isValid(parameters: Array<Any ? >?, context: ConstraintValidatorContext): Boolean {
46- val pageable = parameters?.filterIsInstance< Pageable> ()?.firstOrNull()
47- ?: throw IllegalStateException(
48- " @ValidSort can only be used on methods with a Pageable parameter. " +
49- " Ensure the annotated method has a parameter of type org.springframework.data.domain.Pageable."
50- )
59+ override fun isValid(pageable: Pageable?, context: ConstraintValidatorContext): Boolean {
60+ if (pageable == null || pageable.sort.isUnsorted) return true
5161
5262 val invalid = pageable.sort
5363 .foldIndexed(emptyMap< Int, String> ()) { index, acc, order ->
5464 val sortValue = " ${order.property},${order.direction.name.lowercase()}"
55- if (sortValue ! in allowedValues) acc + (index to order.property)
65+ // Accept " property,direction" (exact match) OR " property" alone (any direction allowed)
66+ if (sortValue ! in allowedValues && order.property ! in allowedValues) acc + (index to order.property)
5667 else acc
5768 }
5869 .toSortedMap()
0 commit comments