diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java new file mode 100644 index 000000000..36aa928e2 --- /dev/null +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.validation; + +import jakarta.validation.MessageInterpolator; +import modelengine.fitframework.inspection.Validation; +import modelengine.fitframework.util.ObjectUtils; +import modelengine.fitframework.util.i18n.LocaleContextHolder; + +import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; + +import java.util.Locale; + +/** + * 检验消息处理的代理类。 + *

+ * 从 {@link LocaleContextHolder} 中获取当前线程设置的 {@link Locale} 并委托 {@link MessageInterpolator} 去处理消息。 + *

+ * + * @author 阮睿 + * @since 2025-07-31 + */ +public class LocaleContextMessageInterpolator implements MessageInterpolator { + private final MessageInterpolator targetInterpolator; + private Locale locale; + + /** + * 构造函数。 + * + * @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。 + */ + public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator) { + this.targetInterpolator = targetInterpolator; + this.locale = Locale.getDefault(); + } + + /** + * 构造函数,默认使用 {@link ParameterMessageInterpolator} 作为目标检验消息处理对象。 + */ + public LocaleContextMessageInterpolator() { + this.targetInterpolator = new ParameterMessageInterpolator(); + this.locale = Locale.getDefault(); + } + + /** + * 构造函数。 + * + * @param locale 表示当前设置默认的 {@link Locale}。 + */ + public LocaleContextMessageInterpolator(Locale locale) { + this.targetInterpolator = new ParameterMessageInterpolator(); + this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault); + } + + /** + * 构造函数。 + * + * @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。 + * @param locale 表示当前设置默认的 {@link Locale}。 + */ + public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator, Locale locale) { + this.targetInterpolator = targetInterpolator; + this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault); + } + + /** + * 设置默认的 {@link Locale}。 + * + * @param locale 默认设置的 {@link Locale}。 + */ + public void setLocale(Locale locale) { + this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault); + } + + @Override + public String interpolate(String messageTemplate, Context context) { + if (LocaleContextHolder.getLocale() != null) { + return this.targetInterpolator.interpolate(messageTemplate, context, LocaleContextHolder.getLocale()); + } + return this.targetInterpolator.interpolate(messageTemplate, context, this.locale); + } + + @Override + public String interpolate(String messageTemplate, Context context, Locale locale) { + Validation.notNull(locale, "Locale cannot be null."); + return this.targetInterpolator.interpolate(messageTemplate, context, locale); + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java deleted file mode 100644 index fc22cc98a..000000000 --- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java +++ /dev/null @@ -1,86 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. - * This file is a part of the ModelEngine Project. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -package modelengine.fitframework.validation; - -import jakarta.validation.MessageInterpolator; - -import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; - -import java.util.Locale; - -/** - * 地区消息插值器。 - *

- * 作为 Jakarta 消息插值器的代理类,提供地区设置能力。 - *

- * - * @author 阮睿 - * @since 2025-08-18 - */ -public class LocaleMessageInterpolator implements MessageInterpolator { - private final MessageInterpolator target; - - private Locale locale; - - /** - * 构造函数,使用指定的目标消息插值器初始化实例。 - * - * @param target 表示目标消息插值器的 {@link MessageInterpolator}。 - */ - public LocaleMessageInterpolator(MessageInterpolator target) { - this.target = target; - this.locale = Locale.getDefault(); - } - - /** - * 构造函数,使用指定的地区初始化实例。 - * - * @param locale 表示指定地区的 {@link Locale}。 - */ - public LocaleMessageInterpolator(Locale locale) { - this.locale = locale; - this.target = new ParameterMessageInterpolator(); - } - - /** - * 构造函数,使用指定的目标消息插值器和地区初始化实例。 - * - * @param target 表示被代理的目标消息插值器的 {@link MessageInterpolator}。 - * @param locale 表示当前消息插值器要使用语言的相关地区的 {@link Locale}。 - */ - public LocaleMessageInterpolator(MessageInterpolator target, Locale locale) { - this.target = target; - this.locale = locale; - } - - /** - * 构造函数,使用默认地区初始化实例。 - */ - public LocaleMessageInterpolator() { - this.locale = Locale.getDefault(); - this.target = new ParameterMessageInterpolator(); - } - - @Override - public String interpolate(String messageTemplate, Context context) { - return this.target.interpolate(messageTemplate, context, this.locale); - } - - @Override - public String interpolate(String messageTemplate, Context context, Locale locale) { - return this.target.interpolate(messageTemplate, context, locale); - } - - /** - * 设置地区。 - * - * @param locale 表示当前消息插值器要使用语言的相关地区的 {@link Locale}。 - */ - public void setLocale(Locale locale) { - this.locale = locale; - } -} diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java index ab4805c70..3f71554d0 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/main/java/modelengine/fitframework/validation/ValidationHandler.java @@ -43,10 +43,10 @@ public class ValidationHandler implements AutoCloseable { private final ValidatorFactory validatorFactory; private final Validator validator; - private final LocaleMessageInterpolator messageInterpolator; + private final LocaleContextMessageInterpolator messageInterpolator; public ValidationHandler() { - this.messageInterpolator = new LocaleMessageInterpolator(); + this.messageInterpolator = new LocaleContextMessageInterpolator(); this.validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() .messageInterpolator(this.messageInterpolator) @@ -58,7 +58,7 @@ public ValidationHandler() { /** * 设置校验信息语言。 * - * @param locale 校验语言 {@link Locale}。 + * @param locale 表示校验语言的 {@link Locale}。 */ public void setLocale(Locale locale) { this.messageInterpolator.setLocale(locale); @@ -163,4 +163,4 @@ private boolean isJakartaConstraintAnnotation(Annotation annotation) { return "jakarta.validation".equals(packageName) && "Constraint".equals(className); }); } -} +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java new file mode 100644 index 000000000..1a1fa4a31 --- /dev/null +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import modelengine.fit.http.client.HttpClassicClientResponse; +import modelengine.fit.http.entity.Entity; +import modelengine.fit.http.entity.ObjectEntity; +import modelengine.fitframework.annotation.Fit; +import modelengine.fitframework.test.annotation.MvcTest; +import modelengine.fitframework.test.domain.mvc.MockMvc; +import modelengine.fitframework.test.domain.mvc.request.MockMvcRequestBuilders; +import modelengine.fitframework.test.domain.mvc.request.MockRequestBuilder; +import modelengine.fitframework.validation.data.Company; +import modelengine.fitframework.validation.data.LocaleValidationController; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Map; + +/** + * 表示评估国际化校验的测试类。 + * + * @author 阮睿 + * @since 2025-08-01 + */ +@MvcTest(classes = {LocaleValidationController.class}) +@DisplayName("测试地区化验证消息功能") +public class LocaleValidationControllerTest { + @Fit + private MockMvc mockMvc; + + private HttpClassicClientResponse response; + + @AfterEach + void teardown() throws IOException { + if (this.response != null) { + this.response.close(); + } + } + + @Test + @DisplayName("测试法文地区的验证消息") + void shouldReturnFrenchValidationMessage() { + Company invalidCompany = new Company(null); + + MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple") + .header("Accept-Language", "fr") + .jsonEntity(invalidCompany) + .responseType(Map.class); + + this.response = this.mockMvc.perform(requestBuilder); + // 获取JSON格式的错误信息 + String errorMessage = ""; + if (this.response.entity().isPresent()) { + Entity entity = this.response.entity().get(); + if (entity instanceof ObjectEntity) { + ObjectEntity objectEntity = (ObjectEntity) entity; + Object errorObj = objectEntity.object(); + if (errorObj instanceof Map) { + Map errorMap = (Map) errorObj; + errorMessage = + errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString(); + } else { + errorMessage = errorObj.toString(); + } + } + } + + assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: ne doit pas être nul"); + assertThat(this.response.statusCode()).isEqualTo(500); + } + + @Test + @DisplayName("测试英文地区的验证消息") + void shouldReturnEnglishValidationMessage() { + Company invalidCompany = new Company(null); + + MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple") + .header("Accept-Language", "en-us") + .jsonEntity(invalidCompany) + .responseType(Map.class); + + this.response = this.mockMvc.perform(requestBuilder); + // 获取JSON格式的错误信息 + String errorMessage = ""; + if (this.response.entity().isPresent()) { + Entity entity = this.response.entity().get(); + if (entity instanceof ObjectEntity) { + ObjectEntity objectEntity = (ObjectEntity) entity; + Object errorObj = objectEntity.object(); + if (errorObj instanceof Map) { + Map errorMap = (Map) errorObj; + errorMessage = + errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString(); + } else { + errorMessage = errorObj.toString(); + } + } + } + + assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null"); + assertThat(this.response.statusCode()).isEqualTo(500); + } + + @Test + @DisplayName("测试URL参数指定地区") + void shouldUseLocaleFromUrlParam() { + Company invalidCompany = new Company(null); + + MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple") + .param("locale", "en-US") + .jsonEntity(invalidCompany) + .responseType(Map.class); + + this.response = this.mockMvc.perform(requestBuilder); + + // 获取JSON格式的错误信息 + String errorMessage = ""; + if (this.response.entity().isPresent()) { + Entity entity = this.response.entity().get(); + if (entity instanceof ObjectEntity) { + ObjectEntity objectEntity = (ObjectEntity) entity; + Object errorObj = objectEntity.object(); + if (errorObj instanceof Map) { + Map errorMap = (Map) errorObj; + errorMessage = + errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString(); + } else { + errorMessage = errorObj.toString(); + } + } + } + + assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null"); + assertThat(this.response.cookies().get("locale").isPresent()); + assertThat(this.response.cookies().get("locale").get().value()).isEqualTo("en-US"); + assertThat(this.response.statusCode()).isEqualTo(500); + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java index 2b5d9def0..084a3b360 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java @@ -341,7 +341,8 @@ class StudentGroupValidationTests { public void givenParametersThenGroupValidateHappened() { // 测试学生年龄验证 - 现在会抛出异常,因为使用了学生分组 Method method = ReflectionUtils.getDeclaredMethod(GroupValidateService.StudentValidateService.class, - "validateStudentAge", int.class); + "validateStudentAge", + int.class); Method handleValidatedMethod = ReflectionUtils.getDeclaredMethod(ValidationHandler.class, "handle", JoinPoint.class, @@ -755,4 +756,21 @@ void testRangeBigDecimalValidation() { ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {new BigDecimal("5.5")}); assertThat(exception.getMessage()).contains("需要在10和100之间"); } -} + + @Nested + @DisplayName("测试 Locale 默认值为 null 时的情况") + public class ValidationHandlerNullTest { + @BeforeEach + void setUp() { + ValidationHandlerTest.this.handler.setLocale(null); + } + + @Test + @DisplayName("测试@Null注解") + void testNullValidation() { + Method method = ReflectionUtils.getDeclaredMethod(ValidateService.class, "testNull", String.class); + ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {"not null"}); + assertThat(exception.getMessage()).isNotNull(); + } + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java index bba16b88b..bc45d868f 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java @@ -32,9 +32,9 @@ public static class StudentValidateService { * @param age 表示年龄的 {@code int}。 */ public void validateStudentAge( - @Min(value = 7, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class) @Max( - value = 20, message = "范围要在7~20之内", - groups = ValidationTestData.StudentGroup.class) int age) { + @Min(value = 7, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class) + @Max(value = 20, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class) + int age) { LOG.debug("Validating student age: {}", age); } } @@ -48,9 +48,9 @@ public static class TeacherValidateService { * @param age 表示年龄的 {@code int}。 */ public void validateTeacherAge( - @Min(value = 22, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class) @Max( - value = 65, message = "范围要在22~65之内", - groups = ValidationTestData.TeacherGroup.class) int age) { + @Min(value = 22, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class) + @Max(value = 65, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class) + int age) { LOG.debug("Validating teacher age: {}", age); } } diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java new file mode 100644 index 000000000..e0df5f8aa --- /dev/null +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.validation.data; + +import modelengine.fit.http.util.i18n.LocaleResolveFilter; +import modelengine.fitframework.annotation.Bean; +import modelengine.fitframework.annotation.Component; + +/** + * 表示地区解析过滤器的配置类。 + * + * @author 阮睿 + * @since 2025-09-11 + */ +@Component +public class LocaleResolveFilterConfig { + /** + * 创建地区解析过滤器 bean 对象。 + * + * @return 表示作为 bean 的地区解析过滤器对象的 {@link LocaleResolveFilter}。 + */ + @Bean + public LocaleResolveFilter localeResolveFilter() { + return new LocaleResolveFilter(); + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java new file mode 100644 index 000000000..872e72659 --- /dev/null +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-jakarta/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java @@ -0,0 +1,35 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.validation.data; + +import jakarta.validation.Valid; +import modelengine.fit.http.annotation.PostMapping; +import modelengine.fit.http.annotation.RequestBody; +import modelengine.fit.http.annotation.RequestMapping; +import modelengine.fitframework.annotation.Component; +import modelengine.fitframework.validation.LocaleContextMessageInterpolator; +import modelengine.fitframework.validation.Validated; +import modelengine.fitframework.validation.ValidationHandler; + +/** + * 用于测试 {@link ValidationHandler} 与 {@link LocaleContextMessageInterpolator} 的集成地区验证控制器。 + * + * @author 阮睿 + * @since 2025-08-01 + */ +@Component +@RequestMapping(path = "/validation/locale", group = "地区验证测试接口") +@Validated +public class LocaleValidationController { + /** + * 使用简单参数测试验证消息的地区化。 + * + * @param company 表示注解验证的测试实体类 {@link Company}。 + */ + @PostMapping(path = "/simple", description = "测试简单参数的地区化验证消息") + public void validateSimpleParam(@RequestBody @Valid Company company) {} +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java new file mode 100644 index 000000000..ef9be86f3 --- /dev/null +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleContextMessageInterpolator.java @@ -0,0 +1,93 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.validation; + +import modelengine.fitframework.inspection.Validation; +import modelengine.fitframework.util.ObjectUtils; +import modelengine.fitframework.util.i18n.LocaleContextHolder; + +import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; + +import java.util.Locale; + +import javax.validation.MessageInterpolator; + +/** + * 检验消息处理的代理类。 + *

+ * 从 {@link LocaleContextHolder} 中获取当前线程设置的 {@link Locale} 并委托 {@link MessageInterpolator} 去处理消息。 + *

+ * + * @author 阮睿 + * @since 2025-07-31 + */ +public class LocaleContextMessageInterpolator implements MessageInterpolator { + private final MessageInterpolator targetInterpolator; + private Locale locale; + + /** + * 构造函数。 + * + * @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。 + */ + public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator) { + this.targetInterpolator = targetInterpolator; + this.locale = Locale.getDefault(); + } + + /** + * 构造函数,默认使用 {@link ParameterMessageInterpolator} 作为目标检验消息处理对象。 + */ + public LocaleContextMessageInterpolator() { + this.targetInterpolator = new ParameterMessageInterpolator(); + this.locale = Locale.getDefault(); + } + + /** + * 构造函数。 + * + * @param locale 表示当前设置默认的 {@link Locale}。 + */ + public LocaleContextMessageInterpolator(Locale locale) { + this.targetInterpolator = new ParameterMessageInterpolator(); + this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault); + } + + /** + * 构造函数。 + * + * @param targetInterpolator 表示目标检验消息处理对象的 {@link MessageInterpolator}。 + * @param locale 表示当前设置默认的 {@link Locale}。 + */ + public LocaleContextMessageInterpolator(MessageInterpolator targetInterpolator, Locale locale) { + this.targetInterpolator = targetInterpolator; + this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault); + } + + /** + * 设置默认的 {@link Locale}。 + * + * @param locale 默认设置的 {@link Locale}。 + */ + public void setLocale(Locale locale) { + this.locale = ObjectUtils.getIfNull(locale, Locale::getDefault); + } + + @Override + public String interpolate(String messageTemplate, Context context) { + if (LocaleContextHolder.getLocale() != null) { + return this.targetInterpolator.interpolate(messageTemplate, context, LocaleContextHolder.getLocale()); + } + return this.targetInterpolator.interpolate(messageTemplate, context, this.locale); + } + + @Override + public String interpolate(String messageTemplate, Context context, Locale locale) { + Validation.notNull(locale, "Locale cannot be null."); + return this.targetInterpolator.interpolate(messageTemplate, context, locale); + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java deleted file mode 100644 index 74438d75c..000000000 --- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/LocaleMessageInterpolator.java +++ /dev/null @@ -1,86 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. - * This file is a part of the ModelEngine Project. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -package modelengine.fitframework.validation; - -import org.hibernate.validator.messageinterpolation.ParameterMessageInterpolator; - -import java.util.Locale; - -import javax.validation.MessageInterpolator; - -/** - * 地区消息插值器。 - *

- * 作为 Jakarta 消息插值器的代理类,提供地区设置能力。 - *

- * - * @author 阮睿 - * @since 2025-08-18 - */ -public class LocaleMessageInterpolator implements MessageInterpolator { - private final MessageInterpolator target; - - private Locale locale; - - /** - * 构造函数,使用指定的目标消息插值器初始化实例。 - * - * @param target 表示目标消息插值器的 {@link MessageInterpolator}。 - */ - public LocaleMessageInterpolator(MessageInterpolator target) { - this.target = target; - this.locale = Locale.getDefault(); - } - - /** - * 构造函数,使用指定的地区初始化实例。 - * - * @param locale 表示指定地区的 {@link Locale}。 - */ - public LocaleMessageInterpolator(Locale locale) { - this.locale = locale; - this.target = new ParameterMessageInterpolator(); - } - - /** - * 构造函数,使用指定的目标消息插值器和地区初始化实例。 - * - * @param target 表示被代理的目标消息插值器的 {@link MessageInterpolator}。 - * @param locale 表示当前消息插值器要使用语言的相关地区的 {@link Locale}。 - */ - public LocaleMessageInterpolator(MessageInterpolator target, Locale locale) { - this.target = target; - this.locale = locale; - } - - /** - * 构造函数,使用默认地区初始化实例。 - */ - public LocaleMessageInterpolator() { - this.locale = Locale.getDefault(); - this.target = new ParameterMessageInterpolator(); - } - - @Override - public String interpolate(String messageTemplate, Context context) { - return this.target.interpolate(messageTemplate, context, this.locale); - } - - @Override - public String interpolate(String messageTemplate, Context context, Locale locale) { - return this.target.interpolate(messageTemplate, context, locale); - } - - /** - * 设置地区。 - * - * @param locale 表示当前消息插值器要使用语言的相关地区的 {@link Locale}。 - */ - public void setLocale(Locale locale) { - this.locale = locale; - } -} diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/ValidationHandler.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/ValidationHandler.java index 40e9086df..195d773a4 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/ValidationHandler.java +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/main/java/modelengine/fitframework/validation/ValidationHandler.java @@ -44,10 +44,10 @@ public class ValidationHandler implements AutoCloseable { private final ValidatorFactory validatorFactory; private final Validator validator; - private final LocaleMessageInterpolator messageInterpolator; + private final LocaleContextMessageInterpolator messageInterpolator; public ValidationHandler() { - this.messageInterpolator = new LocaleMessageInterpolator(); + this.messageInterpolator = new LocaleContextMessageInterpolator(); this.validatorFactory = Validation.byProvider(HibernateValidator.class) .configure() .messageInterpolator(this.messageInterpolator) @@ -164,4 +164,4 @@ private boolean isJavaxConstraintAnnotation(Annotation annotation) { return "javax.validation".equals(packageName) && "Constraint".equals(className); }); } -} +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java new file mode 100644 index 000000000..1a1fa4a31 --- /dev/null +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/LocaleValidationControllerTest.java @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.validation; + +import static org.assertj.core.api.Assertions.assertThat; + +import modelengine.fit.http.client.HttpClassicClientResponse; +import modelengine.fit.http.entity.Entity; +import modelengine.fit.http.entity.ObjectEntity; +import modelengine.fitframework.annotation.Fit; +import modelengine.fitframework.test.annotation.MvcTest; +import modelengine.fitframework.test.domain.mvc.MockMvc; +import modelengine.fitframework.test.domain.mvc.request.MockMvcRequestBuilders; +import modelengine.fitframework.test.domain.mvc.request.MockRequestBuilder; +import modelengine.fitframework.validation.data.Company; +import modelengine.fitframework.validation.data.LocaleValidationController; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.Map; + +/** + * 表示评估国际化校验的测试类。 + * + * @author 阮睿 + * @since 2025-08-01 + */ +@MvcTest(classes = {LocaleValidationController.class}) +@DisplayName("测试地区化验证消息功能") +public class LocaleValidationControllerTest { + @Fit + private MockMvc mockMvc; + + private HttpClassicClientResponse response; + + @AfterEach + void teardown() throws IOException { + if (this.response != null) { + this.response.close(); + } + } + + @Test + @DisplayName("测试法文地区的验证消息") + void shouldReturnFrenchValidationMessage() { + Company invalidCompany = new Company(null); + + MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple") + .header("Accept-Language", "fr") + .jsonEntity(invalidCompany) + .responseType(Map.class); + + this.response = this.mockMvc.perform(requestBuilder); + // 获取JSON格式的错误信息 + String errorMessage = ""; + if (this.response.entity().isPresent()) { + Entity entity = this.response.entity().get(); + if (entity instanceof ObjectEntity) { + ObjectEntity objectEntity = (ObjectEntity) entity; + Object errorObj = objectEntity.object(); + if (errorObj instanceof Map) { + Map errorMap = (Map) errorObj; + errorMessage = + errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString(); + } else { + errorMessage = errorObj.toString(); + } + } + } + + assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: ne doit pas être nul"); + assertThat(this.response.statusCode()).isEqualTo(500); + } + + @Test + @DisplayName("测试英文地区的验证消息") + void shouldReturnEnglishValidationMessage() { + Company invalidCompany = new Company(null); + + MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple") + .header("Accept-Language", "en-us") + .jsonEntity(invalidCompany) + .responseType(Map.class); + + this.response = this.mockMvc.perform(requestBuilder); + // 获取JSON格式的错误信息 + String errorMessage = ""; + if (this.response.entity().isPresent()) { + Entity entity = this.response.entity().get(); + if (entity instanceof ObjectEntity) { + ObjectEntity objectEntity = (ObjectEntity) entity; + Object errorObj = objectEntity.object(); + if (errorObj instanceof Map) { + Map errorMap = (Map) errorObj; + errorMessage = + errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString(); + } else { + errorMessage = errorObj.toString(); + } + } + } + + assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null"); + assertThat(this.response.statusCode()).isEqualTo(500); + } + + @Test + @DisplayName("测试URL参数指定地区") + void shouldUseLocaleFromUrlParam() { + Company invalidCompany = new Company(null); + + MockRequestBuilder requestBuilder = MockMvcRequestBuilders.post("/validation/locale/simple") + .param("locale", "en-US") + .jsonEntity(invalidCompany) + .responseType(Map.class); + + this.response = this.mockMvc.perform(requestBuilder); + + // 获取JSON格式的错误信息 + String errorMessage = ""; + if (this.response.entity().isPresent()) { + Entity entity = this.response.entity().get(); + if (entity instanceof ObjectEntity) { + ObjectEntity objectEntity = (ObjectEntity) entity; + Object errorObj = objectEntity.object(); + if (errorObj instanceof Map) { + Map errorMap = (Map) errorObj; + errorMessage = + errorMap.get("error") != null ? errorMap.get("error").toString() : errorMap.toString(); + } else { + errorMessage = errorObj.toString(); + } + } + } + + assertThat(errorMessage).isEqualTo("validateSimpleParam.company.employees: must not be null"); + assertThat(this.response.cookies().get("locale").isPresent()); + assertThat(this.response.cookies().get("locale").get().value()).isEqualTo("en-US"); + assertThat(this.response.statusCode()).isEqualTo(500); + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java index b23b8dd09..08e311889 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/ValidationHandlerTest.java @@ -59,10 +59,10 @@ public class ValidationHandlerTest { @BeforeEach void setUp() { - handler.setLocale(Locale.CHINA); - when(validated.value()).thenReturn(new Class[0]); - when(fitRuntime.resolverOfAnnotations()).thenReturn(annotationMetadataResolver); - when(beanContainer.runtime()).thenReturn(fitRuntime); + this.handler.setLocale(Locale.CHINA); + when(this.validated.value()).thenReturn(new Class[0]); + when(this.fitRuntime.resolverOfAnnotations()).thenReturn(annotationMetadataResolver); + when(this.beanContainer.runtime()).thenReturn(fitRuntime); } private ConstraintViolationException invokeHandleMethod(Method targetMethod, Object[] args) { @@ -757,4 +757,21 @@ void testRangeBigDecimalValidation() { ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {new BigDecimal("5.5")}); assertThat(exception.getMessage()).contains("需要在10和100之间"); } + + @Nested + @DisplayName("测试 Locale 默认值为 null 时的情况") + public class ValidationHandlerNullTest { + @BeforeEach + void setUp() { + ValidationHandlerTest.this.handler.setLocale(null); + } + + @Test + @DisplayName("测试@Null注解") + void testNullValidation() { + Method method = ReflectionUtils.getDeclaredMethod(ValidateService.class, "testNull", String.class); + ConstraintViolationException exception = invokeHandleMethod(method, new Object[] {"not null"}); + assertThat(exception.getMessage()).isNotNull(); + } + } } diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java index d299bdfd5..ee3dad196 100644 --- a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/GroupValidateService.java @@ -33,9 +33,9 @@ public static class StudentValidateService { * @param age 表示年龄的 {@code int}。 */ public void validateStudentAge( - @Min(value = 7, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class) @Max( - value = 20, message = "范围要在7~20之内", - groups = ValidationTestData.StudentGroup.class) int age) { + @Min(value = 7, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class) + @Max(value = 20, message = "范围要在7~20之内", groups = ValidationTestData.StudentGroup.class) + int age) { LOG.debug("Validating student age: {}", age); } } @@ -49,9 +49,9 @@ public static class TeacherValidateService { * @param age 表示年龄的 {@code int}。 */ public void validateTeacherAge( - @Min(value = 22, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class) @Max( - value = 65, message = "范围要在22~65之内", - groups = ValidationTestData.TeacherGroup.class) int age) { + @Min(value = 22, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class) + @Max(value = 65, message = "范围要在22~65之内", groups = ValidationTestData.TeacherGroup.class) + int age) { LOG.debug("Validating teacher age: {}", age); } } diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java new file mode 100644 index 000000000..e0df5f8aa --- /dev/null +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleResolveFilterConfig.java @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.validation.data; + +import modelengine.fit.http.util.i18n.LocaleResolveFilter; +import modelengine.fitframework.annotation.Bean; +import modelengine.fitframework.annotation.Component; + +/** + * 表示地区解析过滤器的配置类。 + * + * @author 阮睿 + * @since 2025-09-11 + */ +@Component +public class LocaleResolveFilterConfig { + /** + * 创建地区解析过滤器 bean 对象。 + * + * @return 表示作为 bean 的地区解析过滤器对象的 {@link LocaleResolveFilter}。 + */ + @Bean + public LocaleResolveFilter localeResolveFilter() { + return new LocaleResolveFilter(); + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java new file mode 100644 index 000000000..41572320a --- /dev/null +++ b/framework/fit/java/fit-builtin/plugins/fit-validation-hibernate-javax/src/test/java/modelengine/fitframework/validation/data/LocaleValidationController.java @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.validation.data; + +import modelengine.fit.http.annotation.PostMapping; +import modelengine.fit.http.annotation.RequestBody; +import modelengine.fit.http.annotation.RequestMapping; +import modelengine.fitframework.annotation.Component; +import modelengine.fitframework.validation.LocaleContextMessageInterpolator; +import modelengine.fitframework.validation.Validated; +import modelengine.fitframework.validation.ValidationHandler; + +import javax.validation.Valid; + +/** + * 用于测试 {@link ValidationHandler} 与 {@link LocaleContextMessageInterpolator} 的集成地区验证控制器。 + * + * @author 阮睿 + * @since 2025-08-01 + */ +@Component +@RequestMapping(path = "/validation/locale", group = "地区验证测试接口") +@Validated +public class LocaleValidationController { + /** + * 使用简单参数测试验证消息的地区化。 + * + * @param company 表示注解验证的测试实体类 {@link Company}。 + */ + @PostMapping(path = "/simple", description = "测试简单参数的地区化验证消息") + public void validateSimpleParam(@RequestBody @Valid Company company) {} +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/DefualtLocaleResolver.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/DefualtLocaleResolver.java new file mode 100644 index 000000000..f6c703d21 --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/DefualtLocaleResolver.java @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.util.i18n; + +import modelengine.fit.http.Cookie; +import modelengine.fit.http.server.HttpClassicServerRequest; +import modelengine.fit.http.server.HttpClassicServerResponse; +import modelengine.fitframework.util.StringUtils; + +import java.util.Locale; + +/** + * 默认地区解析器。 + * + * @author 阮睿 + * @since 2025-08-01 + */ +public class DefualtLocaleResolver implements LocaleResolver { + private String cookieName = DEFAULT_COOKIE_NAME; + private int cookieMaxAge = DEFAULT_COOKIE_MAX_AGE; + private String cookieDomain = DEFAULT_COOKIE_DOMAIN; + private String cookiePath = DEFAULT_COOKIE_PATH; + private Locale defaultLocale = Locale.getDefault(); + + @Override + public Locale resolveLocale(HttpClassicServerRequest request) { + // 先解析 Cookie,如果没有则解析 Accept-Language 头。 + String newLocale = request.cookies().get(this.cookieName).map(Cookie::value).orElse(null); + if (StringUtils.isNotBlank(newLocale)) { + return Locale.forLanguageTag(newLocale); + } + + String acceptLanguage = request.headers().first("Accept-Language").orElse(null); + if (StringUtils.isNotBlank(acceptLanguage)) { + return Locale.forLanguageTag(acceptLanguage); + } + + return this.defaultLocale; + } + + @Override + public void setLocale(HttpClassicServerResponse response, Locale locale) { + if (locale != null) { + response.cookies() + .add(Cookie.builder() + .name(this.cookieName) + .value(locale.toLanguageTag()) + .maxAge(this.cookieMaxAge) + .domain(this.cookieDomain) + .path(this.cookiePath) + .build()); + } else { + response.cookies().add(Cookie.builder().name(this.cookieName).maxAge(0).build()); + } + } + + /** + * 设置存储地区信息的 Cookie 名称。 + * + * @param cookieName 表示待设置 Cookie 名称的 {@link String}。 + */ + public void setCookieName(String cookieName) { + this.cookieName = cookieName; + } + + /** + * 设置存储地区信息的 Cookie 的最大有效期。 + * + * @param cookieMaxAge 表示待设置的 Cookie 最大有效期的 {@code int}。 + */ + public void setCookieMaxAge(int cookieMaxAge) { + this.cookieMaxAge = cookieMaxAge; + } + + /** + * 设置存储地区信息的 Cookie 的作用域。 + * + * @param cookieDomain 存储地区信息的 Cookie 作用域的 {@link String}。 + */ + public void setCookieDomain(String cookieDomain) { + this.cookieDomain = cookieDomain; + } + + /** + * 设置存储地区信息的 Cookie 的可见 URL 路径。 + * + * @param cookiePath 存储地区信息的 Cookie 作用域的 {@link String}。 + */ + public void setCookiePath(String cookiePath) { + this.cookiePath = cookiePath; + } + + /** + * 设置默认地区。 + * + * @param defaultLocale 表示默认地区的 {@link Locale}。 + */ + public void setDefaultLocale(Locale defaultLocale) { + this.defaultLocale = defaultLocale; + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolveFilter.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolveFilter.java new file mode 100644 index 000000000..e629cfcd5 --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolveFilter.java @@ -0,0 +1,110 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.util.i18n; + +import modelengine.fit.http.server.DoHttpServerFilterException; +import modelengine.fit.http.server.HttpClassicServerRequest; +import modelengine.fit.http.server.HttpClassicServerResponse; +import modelengine.fit.http.server.HttpServerFilter; +import modelengine.fit.http.server.HttpServerFilterChain; +import modelengine.fitframework.annotation.Scope; +import modelengine.fitframework.inspection.Validation; +import modelengine.fitframework.util.StringUtils; +import modelengine.fitframework.util.i18n.LocaleContextHolder; + +import java.util.List; +import java.util.Locale; +import java.util.Optional; + +/** + * 地区解析过滤器,使用 {@link LocaleResolver} 进行地区解析。 + * + * @author 阮睿 + * @since 2025-08-01 + */ +public class LocaleResolveFilter implements HttpServerFilter { + private LocaleResolver localeResolver = null; + private List matchPatterns = List.of("/**"); + private List mismatchPatterns = List.of(); + private Scope scope = Scope.PLUGIN; + + /** + * 构造函数。 + * + * @param localeResolver 表示地区解析器的 {@link LocaleResolver}。 + */ + public LocaleResolveFilter(LocaleResolver localeResolver) { + this.localeResolver = Validation.notNull(localeResolver, "The locale resolver cannot be null."); + } + + /** + * 默认构造函数。 + */ + public LocaleResolveFilter() { + this.localeResolver = new DefualtLocaleResolver(); + } + + @Override + public String name() { + return "LocaleResolveFilter"; + } + + @Override + public int priority() { + return 0; + } + + @Override + public List matchPatterns() { + return this.matchPatterns; + } + + @Override + public List mismatchPatterns() { + return this.mismatchPatterns; + } + + @Override + public void doFilter(HttpClassicServerRequest request, HttpClassicServerResponse response, + HttpServerFilterChain chain) throws DoHttpServerFilterException { + try { + Locale responseLocale = this.resolveLocaleFromParam(request); + // 如果参数中带有地区,说明用户想使用新地区执行后续的操作,直接设置地区。 + if (responseLocale != null) { + LocaleContextHolder.setLocale(responseLocale); + } else { + // 如果参数中不包含地区,则解析请求所带的地区参数。 + Locale locale = this.localeResolver.resolveLocale(request); + LocaleContextHolder.setLocale(locale); + } + + // 继续执行后续过滤器。 + chain.doFilter(request, response); + + if (!response.isCommitted()) { + // responseLocale 是用户期望设置的地区,不受 server 端处理的影响。 + this.localeResolver.setLocale(response, responseLocale); + } + } finally { + LocaleContextHolder.clear(); + } + } + + @Override + public Scope scope() { + return this.scope; + } + + private Locale resolveLocaleFromParam(HttpClassicServerRequest request) { + Optional paramLocale = request.queries().first("locale"); + String localeString = paramLocale.orElse(null); + if (StringUtils.isNotBlank(localeString)) { + return Locale.forLanguageTag(localeString); + } + return null; + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolver.java b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolver.java new file mode 100644 index 000000000..b4e636e42 --- /dev/null +++ b/framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/LocaleResolver.java @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fit.http.util.i18n; + +import modelengine.fit.http.server.HttpClassicServerRequest; +import modelengine.fit.http.server.HttpClassicServerResponse; + +import java.util.Locale; + +/** + * 地区解析器接口,用于从 HTTP 请求中解析用户的地区设置。 + * + * @author 阮睿 + * @since 2025-08-01 + */ +public interface LocaleResolver { + /** + * 表示待设置 cookie 的名称。 + */ + public static final String DEFAULT_COOKIE_NAME = "locale"; + + /** + * 表示待设置 cookie 的自动过期时间。 + */ + public static final int DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; + + /** + * 表示待设置 Cookie 的可见域。 + */ + public static final String DEFAULT_COOKIE_DOMAIN = "/"; + + /** + * 表示待设置 Cookie 的可见 URL 路径。 + */ + public static final String DEFAULT_COOKIE_PATH = "/"; + + /** + * 解析用户的地区设置。 + * + * @param request 表示待解析 HTTP 请求的 {@link HttpClassicServerRequest}。 + * @return 表示解析出来地区信息的 {@link Locale}。 + */ + Locale resolveLocale(HttpClassicServerRequest request); + + /** + * 设置地区到返回响应中。 + * + * @param response 表示待设置地区的 HTTP 响应的 {@link HttpClassicServerResponse}。 + * @param locale 表示待设置地区的 {@link Locale}。 + */ + void setLocale(HttpClassicServerResponse response, Locale locale); +} \ No newline at end of file diff --git a/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/i18n/LocaleContextHolder.java b/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/i18n/LocaleContextHolder.java new file mode 100644 index 000000000..4ae67f9cc --- /dev/null +++ b/framework/fit/java/fit-util/src/main/java/modelengine/fitframework/util/i18n/LocaleContextHolder.java @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.util.i18n; + +import java.util.Locale; + +/** + * 表示存储地区的线程上下文。 + * + * @author 阮睿 + * @since 2025-08-01 + */ +public class LocaleContextHolder { + private static final ThreadLocal LOCALE_CONTEXT_HOLDER = new ThreadLocal<>(); + + /** + * 设置当前线程的地区上下文。 + * + * @param locale 表示待存储在当前线程地区上下文的 {@link Locale}。 + */ + public static void setLocale(Locale locale) { + if (locale != null) { + LOCALE_CONTEXT_HOLDER.set(locale); + } + } + + /** + * 获取当前线程的地区。 + * + * @return 表示当前线程上下文存储地区信息的 {@link Locale}。 + */ + public static Locale getLocale() { + return LOCALE_CONTEXT_HOLDER.get(); + } + + /** + * 清除当前线程的地区上下文。 + */ + public static void clear() { + LOCALE_CONTEXT_HOLDER.remove(); + } +} \ No newline at end of file diff --git a/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/i18n/LocaleContextHolderTest.java b/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/i18n/LocaleContextHolderTest.java new file mode 100644 index 000000000..6c398ae38 --- /dev/null +++ b/framework/fit/java/fit-util/src/test/java/modelengine/fitframework/util/i18n/LocaleContextHolderTest.java @@ -0,0 +1,72 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved. + * This file is a part of the ModelEngine Project. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +package modelengine.fitframework.util.i18n; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Locale; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +/** + * {@link LocaleContextHolder} 的单元测试。 + * + * @author 阮睿 + * @since 2025-09-09 + */ +@DisplayName("测试 LocaleContextHolder") +public class LocaleContextHolderTest { + @AfterEach + void tearDown() { + LocaleContextHolder.clear(); + } + + @Nested + @DisplayName("Test method: setLocale and getLocale") + class TestSetAndGetLocaleContext { + @Test + @DisplayName("Given locale with zh_CN then return the same locale") + void givenLocaleContextWithZhCNThenReturnSameLocaleContext() { + Locale locale = Locale.SIMPLIFIED_CHINESE; + LocaleContextHolder.setLocale(locale); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(locale); + } + + @Test + @DisplayName("Given locale with en_US then return the same locale") + void givenLocaleContextWithEnUSThenReturnSameLocaleContext() { + Locale locale = Locale.US; + LocaleContextHolder.setLocale(locale); + assertThat(LocaleContextHolder.getLocale()).isEqualTo(locale); + } + + @Test + @DisplayName("Given null locale then not set and return null") + void givenNullLocaleContextThenReturnNull() { + LocaleContextHolder.setLocale(null); + assertThat(LocaleContextHolder.getLocale()).isNull(); + } + } + + @Nested + @DisplayName("Test method: clear") + class TestClear { + @Test + @DisplayName("Given existing locale then clear it") + void givenExistingLocaleContextThenClearIt() { + Locale locale = Locale.SIMPLIFIED_CHINESE; + LocaleContextHolder.setLocale(locale); + assertThat(LocaleContextHolder.getLocale()).isNotNull(); + + LocaleContextHolder.clear(); + assertThat(LocaleContextHolder.getLocale()).isNull(); + } + } +} \ No newline at end of file