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

(cherry picked from commit 01bb56c)
  • Loading branch information
marko-bekhta authored and gsmet committed Sep 9, 2024
1 parent a84bb0d commit 8936373
Show file tree
Hide file tree
Showing 7 changed files with 129 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.RuntimeReinitializedDefaultClockProvider;
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(RuntimeReinitializedDefaultClockProvider.INSTANCE);
}

// Hibernate Validator-specific configuration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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}.
* <p>
* Note, that we do not hold the timezone constant in the clock provider itself as we need to "reinitialize" this class,
* so that the timezone is set to the actual runtime-system-timezone.
* Having a constant in the clock provider and asking to reload the provider class leads to native build failure:
* <p>
* <em>
* Error: An object of type 'io.quarkus.hibernate.validator.runtime.clockprovider.HibernateValidatorClockProvider' was found in
* the image heap. This type, however, is marked for initialization at image run time for the following reason: classes are
* initialized at run time by default.
* This is not allowed for correctness reasons: All objects that are stored in the image heap must be initialized at build time.
* </em>
* <p>
* And we do have instances of the clock provider/clock in the Hibernate Validator metadata as we eagerly initialize
* constraints.
*/
class HibernateValidatorClockProviderSystemZoneIdHolder {
static final ZoneId SYSTEM_ZONE_ID = ZoneId.systemDefault();
}
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 RuntimeReinitializedDefaultClockProvider implements ClockProvider {

public static final RuntimeReinitializedDefaultClockProvider INSTANCE = new RuntimeReinitializedDefaultClockProvider();

private static final RuntimeReinitializedDefaultClock clock = new RuntimeReinitializedDefaultClock();

private RuntimeReinitializedDefaultClockProvider() {
}

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

private static class RuntimeReinitializedDefaultClock 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();
}
}
}
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 8936373

Please sign in to comment.