Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
Re: [bean-validation-dev] Provides a way to implement a dynamic clock provider?

Hello Marko,

Thank you for your reply.

Yes, I can achieve this through custom class validators or `@AssertTrue` methods. If I have these `@AssertTrue` methods in many places, I think this is boilerplate code, because they only serve a very simple, single purpose, which is to dynamically provide a `Clock`. I would prefer to have a single-responsibility class that handles providing the `Clock` (based on some marker or other method, like an annotation) rather than scattering it across various classes in `@AssertTrue` methods. Nevertheless, I still appreciate the viable solution you provided.

Regarding your explanation of the separation of responsibilities between field constraints and class constraints, and the design of strong encapsulation, I completely agree. A field should not be aware of the class or other fields’ existence, and providing a "loose" way for it to access things outside of its responsibilities would lead to confusion and increased design complexity. Therefore, I realized how poorly designed the `<T> Clock getClock(Object, Field, Method, T, Class<?>...)` method was.

Currently, the validation issues only tend to occur when handling cross-midnight checks, so it’s bearable. As a result, I’ve decided to ignore it for now. However, I still think that this (controlling the `Clock` used for validating annotations like `@Past`) is a flaw or gap.

Kind regards,
Xi Minghui

On 10/19/2024 12:50 AM, Marko Bekhta wrote:
Hello,

Thanks for sharing the use case; I can better understand what you are trying to accomplish now. 
In general, the constraint should have a limited scope, i.e. if someone says

@MyClassConstraint
class MyClass {
    @MyFieldConstraint
    String string;

    int number;
}

then `@MyClassConstraint` has "access to the entire `MyClass` while `@MyFieldConstraint` is only limited to the `string`, and cannot/should not know anything about other fields in the class, or wherever it is located.
Now, in your case, you have a property in your bean that drives the validation of the other property... Hence, you could apply such validation at a class level, e.g., create a class constraint that reads the preferred timezone and makes the decision on whether the birthdate is correct or not. A more simple alternative to this would be to have a simple:

boolean isValidBirthdate(){
    return ... 
}

This way, you do not need to create additional constraint annotations and validators; you just make your check within this "getter method". 

Have a nice day,
Marko


On Thu, 17 Oct 2024 at 23:43, Xi Minghui <ximinghui@xxxxxxxx> wrote:

Hi Marko,

I'm glad to receive your response. I appreciate you explaining the internals; I've learned much from this.

My goal isn't to change the groups or alter the validation logic. I'm aiming to provide a more flexible mechanism for selecting the appropriate Clock based on the object being validated. To clarify my use case, let me explain it through the code:

import java.time.*;
import jakarta.validation.constraints.*;
import lombok.Data;
@Data
public class User {
    @Email
    @NotBlank
    private String email;
    @NotBlank
    private String password;
    @NotNull
    private String name;
    /**
     * Problem:
     * <p>
     * Assume the server is currently using the {@code Europe/Berlin} time zone.
     * A user in New York (whose preferred time zone is {@code America/New_York})
     * submits a request to set their birthday to [2024-02-15] at [2024-02-15T18:54:32-05:00].
     * According to the business rules and the {@code @Past} constraint,
     * this value (2024-02-15) should fail validation. However, it will pass because
     * the validator refers to the {@code Europe/Berlin} clock [2024-02-16T00:54:32+01:00]
     * when validating the {@code @Past} constraint.
     */
    @Past
    private LocalDate birthday;
    private String address;
    /**
     * Each instance has a time zone set by the user
     */
    @NotNull
    private ZoneId preferredTimeZone = ZoneId.of("Etc/UTC");
    public int getAge() {
        // Omit some calculation code
        return -1;
    }
    /**
     * This {@code @TimeZoneForValidation} is the custom annotation I want to make.
     * <p>
     * If present on a class, the {@code @TimeZoneForValidation} annotation will be consulted by
     * all date or time-type fields/parameters with date or time annotations in that class.
     * <p>
     * For example:
     * <code>
     *     <pre>
     *         // Applies to all date/time annotations (@Past, @PastOrPresent, @Future, @FutureOrPresent) in the class
     *         @TimeZoneForValidation("Europe/Berlin")
     *         public class User {
     *             // ...
     *
     *             @Past
     *             private LocalDate field1;
     *
     *             // Higher priority, overrides the time zone declared on the class
     *             @Past
     *             @TimeZoneForValidation("Asia/Shanghai")
     *             private LocalDateTime field2;
     *
     *             @Past
     *             private Date field3;
     *
     *             // ...
     *         }
     *     </pre>
     * </code>
     * <p>
     *
     *
     * {@code @TimeZoneForValidation} can also be applied to methods that return {@code Clock},
     * {@code ZoneId}, or {@code TimeZone}. Its effect is the same as when applied to a class,
     * but this approach provides greater power and flexibility in programming. For a given class,
     * multiple {@code @TimeZoneForValidation} methods are not allowed, nor is it permitted to
     * have both {@code @TimeZoneForValidation} methods and a {@code @TimeZoneForValidation} annotation
     * on the class.
     */
    @TimeZoneForValidation
    public Clock getClockProvider() {
        return Clock.system(getPreferredTimeZone());
    }
}


Thank you for sharing your thoughts on how to approach the design and your perspective on avoiding reflection. Reflection is not strictly necessary, and perhaps there's a more elegant way to achieve this without relying on it, which I haven't discovered yet. I’ll explore that direction further.

Kind regards,
Xi Minghui

-------- Message --------

Hello,

Maybe you could provide a bit more information on your particular use case first?

> solution that dynamically decides which Clock to provide based on the object currently being validated

Now, as to the suggested interfaces. `ClockProvider` is exposed to the user through `ConstraintValidatorContext` in `ConstraintValidator#isValid(..)`, which means that at this point, it was already decided that the constraint validator has to be executed (i.e. groups were already evaluated, and constraints relevant for those groups already picked, so you may or may not have access to groups at this stage). The same applies to the field/method parameters. On top of that, validation providers may rely on other means than field/method reflection types. Hence, I'd suggest avoiding reflection types in the API if possible.

I'm also unaware of such a feature in any existing validation providers, so it may make sense to introduce something (if anything at all) there first as an incubating feature, validate the assumptions, get some user feedback and iterate on it before introducing the spec change.

Have a nice day,
Marko

On Tue, 15 Oct 2024 at 16:41, Xi Minghui via bean-validation-dev <bean-validation-dev@xxxxxxxxxxx> wrote:

Sorry for my negligence, just having the validation object is not enough, more information is needed:

package jakarta.validation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.time.Clock;
public interface DynamicClockProvider {
    /**
     * Returns the clock which serves as the reference for {@code now}.
     * <p>
     * Ensure that the {@link Clock} used as the reference for {@code now} is obtained
     * for each verification of {@code @Future}, {@code @FutureOrPresent}, {@code @Past},
     * and {@code @PastOrPresent} constraints.
     *
     * @param fieldHolder if the validation object is a field value of a class, then this
     *                    will be the instance that holds the value of this field
     * @param field if the validation object is a field value of a class, then this will
     *              be the class's field reflection object
     * @param method if the validation object is a method parameter or return, then this
     *               will be the reflection object of this method
     * @param object object to validate
     * @param groups the group or list of groups targeted for validation (defaults to
     *        {@link jakarta.validation.groups.Default Default})
     * @param <T> the type of the object to validate
     * @return the clock which serves as the reference for {@code now}; must not be
     * {@code null}
     *
     * @since 4.0
     */
    <T> Clock getClock(Object fieldHolder,
                       Field field,
                       Method method,
                       T object,
                       Class<?>... groups);
}


Kind regards,
Xi Minghui

On 10/15/2024 9:30 PM, Xi Minghui wrote:
Hi,

I'm trying to implement a solution that dynamically decides which Clock to provide based on the object currently being validated, but the current ClockProvider contract doesn't support this.

I thought about implementing it through the ThreadLocal mechanism, but this would mean that validation relies on threads, and to support asynchronous or concurrent execution, more work would be required, which would increase complexity. I believe the most direct, effective, and simple approach is to pass the current validation object to the ClockProvider, like: change java.time.Clock getClock() to <T> java.time.Clock getClock(T object, Class<?>... groups).


Points to consider for the change:

- Keep functional interface: Ideally, ClockProvider should still be usable with lambda expressions.
- Consider backward compatibility: Think about the pain of code migration, especially with lambda method references.


Proposed Approach 1: Add a New Interface

package jakarta.validation;
import java.time.Clock;
public interface DynamicClockProvider {
    /**
     * Returns the clock which serves as the reference for {@code now}.
     * <p>
     * Ensure that the {@link Clock} used as the reference for {@code now} is obtained
     * for each verification of {@code @Future}, {@code @FutureOrPresent}, {@code @Past},
     * and {@code @PastOrPresent} constraints.
     *
     * @param object object to validate
     * @param groups the group or list of groups targeted for validation (defaults to
     *        {@link jakarta.validation.groups.Default Default})
     * @param <T> the type of the object to validate
     * @return the clock which serves as the reference for {@code now}; must not be
     * {@code null}
     *
     * @since 4.0
     */
    <T> Clock getClock(T object, Class<?>... groups);
}


Proposed Approach 2: Update the ClockProvider

/*
 * Jakarta Validation API
 *
 * License: Apache License, Version 2.0
 * See the license.txt file in the root directory or <http://www.apache.org/licenses/LICENSE-2.0>.
 */
package jakarta.validation;
import java.time.Clock;
/**
 * Contract for obtaining the {@link Clock} used as the reference for {@code now} when
 * validating the {@code @Future}, {@code FutureOrPresent}, {@code Past}, and
 * {@code @PastOrPresent} constraints.
 * <p>
 * The default implementation will return the current system time. Plugging in custom
 * implementations may be useful for instance in batch applications which need to run with a
 * specific logical date, e.g. with yesterday's date when re-running a failed batch job
 * execution.
 * <p>
 * Implementations must be safe for access from several threads at the same time.
 *
 * @author Gunnar Morling
 * @author Guillaume Smet
 * @since 2.0
 */
public interface ClockProvider {
    /**
     * Returns the clock which serves as the reference for {@code now}.
     * <p>
     * Ensure that the {@link Clock} used as the reference for {@code now} is obtained
     * for each verification of {@code @Future}, {@code @FutureOrPresent}, {@code @Past},
     * and {@code @PastOrPresent} constraints.
     *
     * @param object object to validate
     * @param groups the group or list of groups targeted for validation (defaults to
     *        {@link jakarta.validation.groups.Default Default})
     * @param <T> the type of the object to validate
     * @return the clock which serves as the reference for {@code now}; must not be
     * {@code null}
     *
     * @since 4.0
     */
    <T> Clock getClock(T object, Class<?>... groups);
}


What do you think of this? I would greatly appreciate your feedback and any other ideas you might have.

Kind regards,
Xi Minghui

_______________________________________________
bean-validation-dev mailing list
bean-validation-dev@xxxxxxxxxxx
To unsubscribe from this list, visit https://www.eclipse.org/mailman/listinfo/bean-validation-dev

Back to the top