Skip to content

Commit

Permalink
Use a Quarkus-specific clock provider that is reinitialized at runtime
Browse files Browse the repository at this point in the history
- so that the actual runtime system timezone is picked up and clock-based (date/time) constraints are correctly evaluated
  • Loading branch information
marko-bekhta committed Sep 6, 2024
1 parent b37b8b7 commit 0fdf360
Show file tree
Hide file tree
Showing 7 changed files with 115 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveFieldBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
import io.quarkus.deployment.builditem.nativeimage.RuntimeReinitializedClassBuildItem;
import io.quarkus.deployment.logging.LogCleanupFilterBuildItem;
import io.quarkus.deployment.pkg.steps.NativeOrNativeSourcesBuild;
import io.quarkus.deployment.recording.RecorderContext;
Expand Down Expand Up @@ -594,6 +595,12 @@ public void build(
hibernateValidatorBuildTimeConfig)));
}

@BuildStep
public RuntimeReinitializedClassBuildItem reinitClockProviderSystemTimezone() {
return new RuntimeReinitializedClassBuildItem(
"io.quarkus.hibernate.validator.runtime.clockprovider.HibernateValidatorClockProviderSystemZoneIdHolder");
}

@BuildStep
void indexAdditionalConstrainedClasses(List<AdditionalConstrainedClassBuildItem> additionalConstrainedClasses,
BuildProducer<AdditionalConstrainedClassesIndexBuildItem> additionalConstrainedClassesIndex) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import io.quarkus.arc.runtime.BeanContainer;
import io.quarkus.arc.runtime.BeanContainerListener;
import io.quarkus.hibernate.validator.ValidatorFactoryCustomizer;
import io.quarkus.hibernate.validator.runtime.clockprovider.HibernateValidatorClockProvider;
import io.quarkus.hibernate.validator.runtime.jaxrs.ResteasyConfigSupport;
import io.quarkus.runtime.LocalesBuildTimeConfig;
import io.quarkus.runtime.ShutdownContext;
Expand Down Expand Up @@ -129,6 +130,11 @@ public void created(BeanContainer container) {
InstanceHandle<ClockProvider> configuredClockProvider = Arc.container().instance(ClockProvider.class);
if (configuredClockProvider.isAvailable()) {
configuration.clockProvider(configuredClockProvider.get());
} else {
// If user didn't provide a custom clock provider we want to set our own.
// This provider ensure the correct behavior in a native mode as it does not
// cache the time zone at a build time.
configuration.clockProvider(HibernateValidatorClockProvider.INSTANCE);
}

// Hibernate Validator-specific configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package io.quarkus.hibernate.validator.runtime.clockprovider;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneId;

import jakarta.validation.ClockProvider;

/**
* A Quarkus-specific clock provider that can provide a clock based on a runtime system time zone.
*/
public class HibernateValidatorClockProvider implements ClockProvider {

public static final HibernateValidatorClockProvider INSTANCE = new HibernateValidatorClockProvider();

private static final HibernateClock clock = new HibernateClock();

private HibernateValidatorClockProvider() {
}

@Override
public Clock getClock() {
return clock;
}

private static class HibernateClock extends Clock {

@Override
public ZoneId getZone() {
// we delegate getting the zone id value to a helper class that is reinitialized at runtime
// allowing to pick up an actual runtime timezone.
return HibernateValidatorClockProviderSystemZoneIdHolder.SYSTEM_ZONE_ID;
}

@Override
public Clock withZone(ZoneId zone) {
return Clock.system(zone);
}

@Override
public Instant instant() {
return Instant.now();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.quarkus.hibernate.validator.runtime.clockprovider;

import java.time.ZoneId;

/**
* A helper class holding a system timezone.
* <p>
* It is reloaded at runtime to provide the runtime-system time zone
* to the constraints based on a {@link jakarta.validation.ClockProvider}.
*/
class HibernateValidatorClockProviderSystemZoneIdHolder {
static final ZoneId SYSTEM_ZONE_ID = ZoneId.systemDefault();
}
18 changes: 18 additions & 0 deletions integration-tests/hibernate-validator/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,24 @@
<user.language>en</user.language>
</systemPropertyVariables>
</configuration>
<executions>
<!--
This additional execution runs the tests with non-default timezone to test
how various Clock-based constraints would behave in a native mode.
-->
<execution>
<id>test-nondefault-timezone</id>
<configuration>
<systemPropertyVariables>
<quarkus.test.arg-line>--env TZ=Europe/Helsinki</quarkus.test.arg-line>
</systemPropertyVariables>
</configuration>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
Expand All @@ -23,6 +24,7 @@
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.PastOrPresent;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.groups.ConvertGroup;
import jakarta.ws.rs.Consumes;
Expand Down Expand Up @@ -321,6 +323,17 @@ public MyBeanWithGroups testRestEndPointValidationGroups_Delete(@PathParam("id")
return result;
}

@GET
@Path("/rest-end-point-clock-based-constraints")
@Produces(MediaType.TEXT_PLAIN)
public String testClockBasedConstraints() {
ResultBuilder result = new ResultBuilder();

result.append(formatViolations(validator.validate(new Task())));

return result.build();
}

private String formatViolations(Set<? extends ConstraintViolation<?>> violations) {
if (violations.isEmpty()) {
return "passed";
Expand Down Expand Up @@ -447,4 +460,9 @@ private static class NestedBeanWithoutConstraints {
@SuppressWarnings("unused")
private String property;
}

public static class Task {
@PastOrPresent
public LocalDateTime created = LocalDateTime.now();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -532,4 +532,12 @@ public void testRestEndPointValidationGroups_result() {
response.body(containsString("must not be null"));
}
}

@Test
void testClockBasedConstraints() {
RestAssured.when()
.get("/hibernate-validator/test/rest-end-point-clock-based-constraints")
.then()
.body(is("passed"));
}
}

0 comments on commit 0fdf360

Please sign in to comment.