Skip to content

Commit

Permalink
Adapt locales support for GraalVM >= 24.2
Browse files Browse the repository at this point in the history
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
  • Loading branch information
zakkak committed Sep 25, 2024
1 parent ffd0c78 commit 818b953
Show file tree
Hide file tree
Showing 18 changed files with 273 additions and 12 deletions.
2 changes: 1 addition & 1 deletion .github/native-tests.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
{
"category": "Misc2",
"timeout": 75,
"test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some",
"test-modules": "hibernate-validator, test-extension/tests, logging-gelf, mailer, native-config-profile, locales/all, locales/some, locales/default",
"os-name": "ubuntu-latest"
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ public interface NativeConfig {

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

/**
* Defines the user country used for building the native executable.
* It also serves as the default Locale country for the native executable application runtime.
* With GraalVM versions prior to GraalVM for JDK 24 it also serves as the default Locale country for the native executable
* application runtime.
* e.g. US or FR as defined by ISO 3166-1 alpha-2 codes.
* <p>
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ public static final class Version implements Comparable<Version> {
public static final Version VERSION_23_0_0 = new Version("GraalVM 23.0.0", "23.0.0", "17", Distribution.GRAALVM);
public static final Version VERSION_23_1_0 = new Version("GraalVM 23.1.0", "23.1.0", "21", Distribution.GRAALVM);
public static final Version VERSION_24_0_0 = new Version("GraalVM 24.0.0", "24.0.0", "22", Distribution.GRAALVM);
public static final Version VERSION_24_1_0 = new Version("GraalVM 24.1.0", "24.1.0", "23", Distribution.GRAALVM);
public static final Version VERSION_24_2_0 = new Version("GraalVM 24.2.0", "24.2.0", "24", Distribution.GRAALVM);

/**
* The minimum version of GraalVM supported by Quarkus.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -749,6 +749,14 @@ public NativeImageInvokerInfo build() {
}
}

if (!Locale.getDefault().equals(localesBuildTimeConfig.defaultLocale)
&& graalVMVersion.compareTo(GraalVM.Version.VERSION_24_2_0) >= 0) {
log.warn(
"Your application is setting the 'quarkus.default-locale' configuration key. " +
"Starting with GraalVM/Mandrel for JDK 24 this configuration is being ignored and the " +
"default locale is always set at runtime based on the system default locale.");
}

final String userLanguage = LocaleProcessor.nativeImageUserLanguage(nativeConfig, localesBuildTimeConfig);
if (!userLanguage.isEmpty()) {
nativeImageArgs.add("-J-Duser.language=" + userLanguage);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,15 @@ public static String nativeImageIncludeLocales(NativeConfig nativeConfig, Locale
return "all";
}

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

return additionalLocales.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ public class LocalesBuildTimeConfig {
* For instance, the Hibernate Validator extension makes use of it.
* <p>
* Native-image build uses this property to derive {@code user.language} and {@code user.country} for the application's
* runtime.
* runtime. Starting with GraalVM for JDK 24 this option will not result in setting the default runtime locale.
* Instead, the Java configuration {@code user.language} and {@code user.country} will be used as the
* default when running the native image.
*/
@ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-" + DEFAULT_COUNTRY, defaultValueDocumentation = "Build system locale")
@ConfigItem(defaultValue = DEFAULT_LANGUAGE + "-"
+ DEFAULT_COUNTRY, defaultValueDocumentation = "Build system locale prior to GraalVM for JDK 24, or system default locale at image runtime with GraalVM for JDK 24 and better")
public Locale defaultLocale;
}
97 changes: 97 additions & 0 deletions integration-tests/locales/default/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-integration-test-locales-parent</artifactId>
<version>999-SNAPSHOT</version>
</parent>
<artifactId>quarkus-integration-test-locales-default</artifactId>
<name>Quarkus - Integration Tests - Locales - Default</name>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-integration-test-locales-app</artifactId>
<version>${project.version}</version>
</dependency>

<!-- test dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>

<!-- Minimal test dependencies to *-deployment artifacts for consistent build order -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator-deployment</artifactId>
<version>${project.version}</version>
<type>pom</type>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<resources>
<resource>
<directory>src/test/resources</directory>
<filtering>true</filtering>
</resource>
</resources>
<plugins>
<plugin>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<forkCount>1</forkCount>
<reuseForks>false</reuseForks>
</configuration>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package io.quarkus.locales.it;

import jakarta.validation.constraints.Pattern;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import org.jboss.logging.Logger;

@Path("")
public class DefaultLocaleResource extends LocalesResource {
private static final Logger LOG = Logger.getLogger(DefaultLocaleResource.class);

// @Pattern validation does nothing when placed in LocalesResource.
@GET
@Path("/hibernate-validator-test-validation-message-locale/{id}/")
@Produces(MediaType.TEXT_PLAIN)
public Response validationMessageLocale(
@Pattern(regexp = "A.*", message = "{pattern.message}") @PathParam("id") String id) {
LOG.infof("Triggering test: id: %s", id);
return Response.ok(id).build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package io.quarkus.locales.it;

import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;

import org.apache.http.HttpStatus;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import io.quarkus.test.junit.DisableIfBuiltWithGraalVMNewerThan;
import io.quarkus.test.junit.DisableIfBuiltWithGraalVMOlderThan;
import io.quarkus.test.junit.GraalVMVersion;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.restassured.RestAssured;

/**
* For the Native test cases to function, the operating system has to have locales support installed. A barebone system with
* only C.UTF-8 default locale available won't be able to pass the tests.
* <p>
* For example, this package satisfies the dependency on a RHEL 9 type of OS: glibc-all-langpacks
*/
@QuarkusIntegrationTest
public class LocalesIT {

private static final Logger LOG = Logger.getLogger(LocalesIT.class);

@Test
@DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_0)
public void testDefaultLocaleBefore24_2() {
RestAssured.given().when()
.get("/default/de-CH")
.then()
.statusCode(HttpStatus.SC_OK)
/*
* "l-Iżvizzera" is the correct name for Switzerland in Maltese language.
* Maltese is the default language as per quarkus.default-locale=mt-MT.
*/
.body(is("l-Iżvizzera"))
.log().all();
}

@Test
@DisableIfBuiltWithGraalVMOlderThan(value = GraalVMVersion.GRAALVM_24_2_0)
public void testDefaultLocaleAfter24_1() {
RestAssured.given().when()
.get("/default/de-CH")
.then()
.statusCode(HttpStatus.SC_OK)
/*
* "l-Iżvizzera" is the correct name for Switzerland in Maltese language.
* Maltese is the default build-time language as per quarkus.default-locale=mt-MT, but not at run-time.
* Note that this test will fail if the default run-time language is Maltese on the test machine,
* this is unfortunate but also unlikely given the small population of Malta.
*/
.body(not("l-Iżvizzera"))
.log().all();
}

/**
* @see integration-tests/hibernate-validator/src/test/java/io/quarkus/it/hibernate/validator/HibernateValidatorFunctionalityTest.java
*/
@ParameterizedTest
@CsvSource(value = {
// French locale is included, so it's used, because Croatian locale is not included
// and thus its property file ValidationMessages_hr_HR.properties is ignored.
"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",
// Silent fallback to lingua franca.
"invalid string|Value is not in line with the pattern",
// French locale is available and included.
"en-US;q=0.25,hr-HR;q=1,fr-FR;q=0.5|La valeur ne correspond pas à l'échantillon"
}, delimiter = '|')
public void testValidationMessageLocale(String acceptLanguage, String expectedMessage) {
RestAssured.given()
.header("Accept-Language", acceptLanguage)
.when()
.get("/hibernate-validator-test-validation-message-locale/1")
.then()
.body(containsString(expectedMessage));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.quarkus.locales.it;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class LocalesTest {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pattern.message=Value is not in line with the pattern
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pattern.message=La valeur ne correspond pas à l'échantillon
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pattern.message=Vrijednost ne zadovoljava uzorak
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
quarkus.locales=de,fr-FR,ja,uk-UA
# Note that quarkus.native.user-language is deprecated and solely quarkus.default-locale should be
# used in your application properties. This test uses it only to verify compatibility.
quarkus.default-locale=mt-MT
1 change: 1 addition & 0 deletions integration-tests/locales/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<modules>
<module>app</module>
<module>all</module>
<module>default</module>
<module>some</module>
</modules>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import io.quarkus.test.junit.DisableIfBuiltWithGraalVMNewerThan;
import io.quarkus.test.junit.DisableIfBuiltWithGraalVMOlderThan;
import io.quarkus.test.junit.GraalVMVersion;
import io.quarkus.test.junit.QuarkusIntegrationTest;
import io.restassured.RestAssured;

Expand Down Expand Up @@ -81,7 +84,8 @@ public void testTimeZones(String zone, String language, String name) {
}

@Test
public void testDefaultLocale() {
@DisableIfBuiltWithGraalVMNewerThan(value = GraalVMVersion.GRAALVM_24_1_0)
public void testDefaultLocaleBefore24_2() {
RestAssured.given().when()
.get("/default/de-CH")
.then()
Expand All @@ -94,6 +98,21 @@ public void testDefaultLocale() {
.log().all();
}

@Test
@DisableIfBuiltWithGraalVMOlderThan(value = GraalVMVersion.GRAALVM_24_2_0)
public void testDefaultLocaleAfter24_1() {
RestAssured.given().when()
.get("/default/de-CH")
.then()
.statusCode(HttpStatus.SC_OK)
/*
* "Schweiz" is the correct name for Switzerland in German.
* German is the default language as per the `quarkus.test.arg-line` in application.properties.
*/
.body(is("Schweiz"))
.log().all();
}

@Test
public void testMissingLocaleSorryItaly() {
RestAssured.given().when()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ quarkus.locales=de,fr-FR,ja,uk-UA
# used in your application properties. This test uses it only to verify compatibility.
quarkus.native.user-language=cs
quarkus.default-locale=en-US
quarkus.test.arg-line=-Duser.language=de
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@

public enum GraalVMVersion {
GRAALVM_23_1_0(GraalVM.Version.VERSION_23_1_0),
GRAALVM_24_0_0(GraalVM.Version.VERSION_24_0_0);
GRAALVM_24_0_0(GraalVM.Version.VERSION_24_0_0),
GRAALVM_24_1_0(GraalVM.Version.VERSION_24_1_0),
GRAALVM_24_2_0(GraalVM.Version.VERSION_24_2_0);

private final GraalVM.Version version;

Expand Down

0 comments on commit 818b953

Please sign in to comment.