Skip to content

Commit ede2be2

Browse files
committed
Adapt locales support for GraalVM >= 24.2
Starting with GraalVM for JDK 24 (24.2) native image will no longer set the locale default at build time. As a result, the default locale won't be included by default in the native image unless explicitly specified. See oracle/graal#9694 # Conflicts: # core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java # integration-tests/locales/some/src/test/java/io/quarkus/locales/it/LocalesIT.java # test-framework/junit5/src/main/java/io/quarkus/test/junit/GraalVMVersion.java
1 parent e2c1d31 commit ede2be2

File tree

18 files changed

+271
-18
lines changed

18 files changed

+271
-18
lines changed

.github/native-tests.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
{
106106
"category": "Misc2",
107107
"timeout": 75,
108-
"test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some",
108+
"test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some, locales/default",
109109
"os-name": "ubuntu-latest"
110110
},
111111
{

core/deployment/src/main/java/io/quarkus/deployment/pkg/NativeConfig.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,8 @@ public interface NativeConfig {
8888

8989
/**
9090
* Defines the user language used for building the native executable.
91-
* It also serves as the default Locale language for the native executable application runtime.
91+
* With GraalVM versions prior to GraalVM for JDK 24 it also serves as the default Locale language for the native executable
92+
* application runtime.
9293
* e.g. en or cs as defined by IETF BCP 47 language tags.
9394
* <p>
9495
*
@@ -100,7 +101,8 @@ public interface NativeConfig {
100101

101102
/**
102103
* Defines the user country used for building the native executable.
103-
* It also serves as the default Locale country for the native executable application runtime.
104+
* With GraalVM versions prior to GraalVM for JDK 24 it also serves as the default Locale country for the native executable
105+
* application runtime.
104106
* e.g. US or FR as defined by ISO 3166-1 alpha-2 codes.
105107
* <p>
106108
*

core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/GraalVM.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,7 @@ public static final class Version implements Comparable<Version> {
196196
public static final Version VERSION_24_0_0 = new Version("GraalVM 24.0.0", "24.0.0", "22", Distribution.GRAALVM);
197197
public static final Version VERSION_24_0_999 = new Version("GraalVM 24.0.999", "24.0.999", "22", Distribution.GRAALVM);
198198
public static final Version VERSION_24_1_0 = new Version("GraalVM 24.1.0", "24.1.0", "23", Distribution.GRAALVM);
199+
public static final Version VERSION_24_2_0 = new Version("GraalVM 24.2.0", "24.2.0", "24", Distribution.GRAALVM);
199200

200201
/**
201202
* The minimum version of GraalVM supported by Quarkus.

core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/NativeImageBuildStep.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,14 @@ public NativeImageInvokerInfo build() {
747747
}
748748
}
749749

750+
if (!Locale.getDefault().equals(localesBuildTimeConfig.defaultLocale)
751+
&& graalVMVersion.compareTo(GraalVM.Version.VERSION_24_2_0) >= 0) {
752+
log.warn(
753+
"Your application is setting the 'quarkus.default-locale' configuration key. " +
754+
"Starting with GraalVM/Mandrel for JDK 24 this configuration is being ignored and the " +
755+
"default locale is always set at runtime based on the system default locale.");
756+
}
757+
750758
final String userLanguage = LocaleProcessor.nativeImageUserLanguage(nativeConfig, localesBuildTimeConfig);
751759
if (!userLanguage.isEmpty()) {
752760
nativeImageArgs.add("-J-Duser.language=" + userLanguage);

core/deployment/src/main/java/io/quarkus/deployment/steps/LocaleProcessor.java

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ public static String nativeImageUserCountry(NativeConfig nativeConfig, LocalesBu
124124
}
125125

126126
/**
127-
* Additional locales to be included in native-image executable.
127+
* Locales to be included in native-image executable.
128128
*
129129
* @param nativeConfig
130130
* @param localesBuildTimeConfig
@@ -139,17 +139,18 @@ public static String nativeImageIncludeLocales(NativeConfig nativeConfig, Locale
139139
return "all";
140140
}
141141

142-
// We subtract what we already declare for native-image's user.language or user.country.
143-
// Note the deprecated options still count.
144-
additionalLocales.remove(localesBuildTimeConfig.defaultLocale);
142+
// GraalVM for JDK 24 doesn't include the default locale used at build time. We must explicitly include the
143+
// specified locales - including the build-time locale.
144+
// Note the deprecated options still count and take precedence.
145145
if (nativeConfig.userCountry().isPresent() && nativeConfig.userLanguage().isPresent()) {
146-
additionalLocales.remove(new Locale(nativeConfig.userLanguage().get(), nativeConfig.userCountry().get()));
146+
additionalLocales.add(new Locale(nativeConfig.userLanguage().get(), nativeConfig.userCountry().get()));
147147
} else if (nativeConfig.userLanguage().isPresent()) {
148-
additionalLocales.remove(new Locale(nativeConfig.userLanguage().get()));
148+
additionalLocales.add(new Locale(nativeConfig.userLanguage().get()));
149+
} else {
150+
additionalLocales.add(localesBuildTimeConfig.defaultLocale);
149151
}
150152

151153
return additionalLocales.stream()
152-
.filter(l -> !Locale.getDefault().equals(l))
153154
.map(l -> l.getLanguage() + (l.getCountry().isEmpty() ? "" : "-" + l.getCountry()))
154155
.collect(Collectors.joining(","));
155156
}

core/runtime/src/main/java/io/quarkus/runtime/LocalesBuildTimeConfig.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ public class LocalesBuildTimeConfig {
3232
* A special string "all" is translated as ROOT Locale and then used in native-image
3333
* to include all locales. Image size penalty applies.
3434
*/
35-
@ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-"
36-
+ DEFAULT_COUNTRY, defaultValueDocumentation = "Set containing the build system locale")
35+
@ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + DEFAULT_COUNTRY,
36+
defaultValueDocumentation = "Set containing the build system locale")
3737
public Set<Locale> locales;
3838

3939
/**
@@ -44,8 +44,11 @@ public class LocalesBuildTimeConfig {
4444
* For instance, the Hibernate Validator extension makes use of it.
4545
* <p>
4646
* Native-image build uses this property to derive {@code user.language} and {@code user.country} for the application's
47-
* runtime.
47+
* runtime. Starting with GraalVM for JDK 24 this option will not result in setting the default runtime locale.
48+
* Instead, the Java configuration {@code user.language} and {@code user.country} will be used as the
49+
* default when running the native image.
4850
*/
49-
@ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + DEFAULT_COUNTRY, defaultValueDocumentation = "Build system locale")
51+
@ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + DEFAULT_COUNTRY,
52+
defaultValueDocumentation = "Build system locale prior to GraalVM for JDK 24, or system default locale at image runtime with GraalVM for JDK 24 and better")
5053
public Locale defaultLocale;
5154
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
3+
xmlns="http://maven.apache.org/POM/4.0.0"
4+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
5+
<modelVersion>4.0.0</modelVersion>
6+
<parent>
7+
<groupId>io.quarkus</groupId>
8+
<artifactId>quarkus-integration-test-locales-parent</artifactId>
9+
<version>999-SNAPSHOT</version>
10+
</parent>
11+
<artifactId>quarkus-integration-test-locales-default</artifactId>
12+
<name>Quarkus - Integration Tests - Locales - Default</name>
13+
<dependencies>
14+
<dependency>
15+
<groupId>io.quarkus</groupId>
16+
<artifactId>quarkus-rest</artifactId>
17+
</dependency>
18+
<dependency>
19+
<groupId>io.quarkus</groupId>
20+
<artifactId>quarkus-hibernate-validator</artifactId>
21+
</dependency>
22+
<dependency>
23+
<groupId>io.quarkus</groupId>
24+
<artifactId>quarkus-integration-test-locales-app</artifactId>
25+
<version>${project.version}</version>
26+
</dependency>
27+
28+
<!-- test dependencies -->
29+
<dependency>
30+
<groupId>io.quarkus</groupId>
31+
<artifactId>quarkus-junit5</artifactId>
32+
<scope>test</scope>
33+
</dependency>
34+
<dependency>
35+
<groupId>io.rest-assured</groupId>
36+
<artifactId>rest-assured</artifactId>
37+
<scope>test</scope>
38+
</dependency>
39+
40+
<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
41+
<dependency>
42+
<groupId>io.quarkus</groupId>
43+
<artifactId>quarkus-rest-deployment</artifactId>
44+
<version>${project.version}</version>
45+
<type>pom</type>
46+
<scope>test</scope>
47+
<exclusions>
48+
<exclusion>
49+
<groupId>*</groupId>
50+
<artifactId>*</artifactId>
51+
</exclusion>
52+
</exclusions>
53+
</dependency>
54+
<dependency>
55+
<groupId>io.quarkus</groupId>
56+
<artifactId>quarkus-hibernate-validator-deployment</artifactId>
57+
<version>${project.version}</version>
58+
<type>pom</type>
59+
<scope>test</scope>
60+
<exclusions>
61+
<exclusion>
62+
<groupId>*</groupId>
63+
<artifactId>*</artifactId>
64+
</exclusion>
65+
</exclusions>
66+
</dependency>
67+
</dependencies>
68+
69+
<build>
70+
<resources>
71+
<resource>
72+
<directory>src/test/resources</directory>
73+
<filtering>true</filtering>
74+
</resource>
75+
</resources>
76+
<plugins>
77+
<plugin>
78+
<groupId>io.quarkus</groupId>
79+
<artifactId>quarkus-maven-plugin</artifactId>
80+
<executions>
81+
<execution>
82+
<goals>
83+
<goal>build</goal>
84+
</goals>
85+
</execution>
86+
</executions>
87+
</plugin>
88+
<plugin>
89+
<artifactId>maven-surefire-plugin</artifactId>
90+
<configuration>
91+
<forkCount>1</forkCount>
92+
<reuseForks>false</reuseForks>
93+
</configuration>
94+
</plugin>
95+
</plugins>
96+
</build>
97+
</project>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.quarkus.locales.it;
2+
3+
import jakarta.validation.constraints.Pattern;
4+
import jakarta.ws.rs.GET;
5+
import jakarta.ws.rs.Path;
6+
import jakarta.ws.rs.PathParam;
7+
import jakarta.ws.rs.Produces;
8+
import jakarta.ws.rs.core.MediaType;
9+
import jakarta.ws.rs.core.Response;
10+
11+
import org.jboss.logging.Logger;
12+
13+
@Path("")
14+
public class DefaultLocaleResource extends LocalesResource {
15+
private static final Logger LOG = Logger.getLogger(DefaultLocaleResource.class);
16+
17+
// @Pattern validation does nothing when placed in LocalesResource.
18+
@GET
19+
@Path("/hibernate-validator-test-validation-message-locale/{id}/")
20+
@Produces(MediaType.TEXT_PLAIN)
21+
public Response validationMessageLocale(
22+
@Pattern(regexp = "A.*", message = "{pattern.message}") @PathParam("id") String id) {
23+
LOG.infof("Triggering test: id: %s", id);
24+
return Response.ok(id).build();
25+
}
26+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package io.quarkus.locales.it;
2+
3+
import static org.hamcrest.Matchers.containsString;
4+
import static org.hamcrest.Matchers.is;
5+
import static org.hamcrest.Matchers.not;
6+
7+
import org.apache.http.HttpStatus;
8+
import org.jboss.logging.Logger;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.params.ParameterizedTest;
11+
import org.junit.jupiter.params.provider.CsvSource;
12+
13+
import io.quarkus.test.junit.DisableIfBuiltWithGraalVMNewerThan;
14+
import io.quarkus.test.junit.DisableIfBuiltWithGraalVMOlderThan;
15+
import io.quarkus.test.junit.GraalVMVersion;
16+
import io.quarkus.test.junit.QuarkusIntegrationTest;
17+
import io.restassured.RestAssured;
18+
19+
/**
20+
* For the Native test cases to function, the operating system has to have locales support installed. A barebone system with
21+
* only C.UTF-8 default locale available won't be able to pass the tests.
22+
* <p>
23+
* For example, this package satisfies the dependency on a RHEL 9 type of OS: glibc-all-langpacks
24+
*/
25+
@QuarkusIntegrationTest
26+
public class LocalesIT {
27+
28+
private static final Logger LOG = Logger.getLogger(LocalesIT.class);
29+
30+
@Test
31+
@DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_0)
32+
public void testDefaultLocaleBefore24_2() {
33+
RestAssured.given().when()
34+
.get("/default/de-CH")
35+
.then()
36+
.statusCode(HttpStatus.SC_OK)
37+
/*
38+
* "l-Iżvizzera" is the correct name for Switzerland in Maltese language.
39+
* Maltese is the default language as per quarkus.default-locale=mt-MT.
40+
*/
41+
.body(is("l-Iżvizzera"))
42+
.log().all();
43+
}
44+
45+
@Test
46+
@DisableIfBuiltWithGraalVMOlderThan(value = GraalVMVersion.GRAALVM_24_2_0)
47+
public void testDefaultLocaleAfter24_1() {
48+
RestAssured.given().when()
49+
.get("/default/de-CH")
50+
.then()
51+
.statusCode(HttpStatus.SC_OK)
52+
/*
53+
* "l-Iżvizzera" is the correct name for Switzerland in Maltese language.
54+
* Maltese is the default build-time language as per quarkus.default-locale=mt-MT, but not at run-time.
55+
* Note that this test will fail if the default run-time language is Maltese on the test machine,
56+
* this is unfortunate but also unlikely given the small population of Malta.
57+
*/
58+
.body(not("l-Iżvizzera"))
59+
.log().all();
60+
}
61+
62+
/**
63+
* @see integration-tests/hibernate-validator/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java
64+
*/
65+
@ParameterizedTest
66+
@CsvSource(value = {
67+
// French locale is included, so it's used, because Croatian locale is not included
68+
// and thus its property file ValidationMessages_hr_HR.properties is ignored.
69+
"en-US;q=0.25,hr-HR;q=0.9,fr-FR;q=0.5,uk-UA;q=0.1|La valeur ne correspond pas à l'échantillon",
70+
// Silent fallback to lingua franca.
71+
"invalid string|Value is not in line with the pattern",
72+
// French locale is available and included.
73+
"en-US;q=0.25,hr-HR;q=1,fr-FR;q=0.5|La valeur ne correspond pas à l'échantillon"
74+
}, delimiter = '|')
75+
public void testValidationMessageLocale(String acceptLanguage, String expectedMessage) {
76+
RestAssured.given()
77+
.header("Accept-Language", acceptLanguage)
78+
.when()
79+
.get("/hibernate-validator-test-validation-message-locale/1")
80+
.then()
81+
.body(containsString(expectedMessage));
82+
}
83+
84+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.quarkus.locales.it;
2+
3+
import io.quarkus.test.junit.QuarkusTest;
4+
5+
@QuarkusTest
6+
public class LocalesTest {
7+
}

0 commit comments

Comments
 (0)