Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions framework/fit/java/fit-builtin/plugins/fit-i18n-registry/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.fitframework.plugin</groupId>
<artifactId>fit-plugin-parent</artifactId>
<version>3.6.0-SNAPSHOT</version>
</parent>

<artifactId>fit-i18n-registry</artifactId>

<name>FIT i18n Registry</name>
<description>
FIT Framework Service provide locale resolver registry.
</description>
<url>https://github.com/ModelEngine-Group/fit-framework</url>

<dependencies>
<!-- FIT core -->
<dependency>
<groupId>org.fitframework</groupId>
<artifactId>fit-api</artifactId>
</dependency>
<dependency>
<groupId>org.fitframework</groupId>
<artifactId>fit-util</artifactId>
</dependency>

<!-- Services -->
<dependency>
<groupId>org.fitframework.service</groupId>
<artifactId>fit-http-classic</artifactId>
</dependency>

<!-- Test -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-antrun-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<configuration>
<target>
<copy file="${project.build.directory}/${project.build.finalName}.jar"
todir="../../../../../../build/plugins"/>
</target>
</configuration>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*---------------------------------------------------------------------------------------------
* 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.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.fit.http.util.i18n.LocaleResolver;
import modelengine.fitframework.annotation.Component;
import modelengine.fitframework.annotation.Scope;
import modelengine.fitframework.util.LocaleContext;
import modelengine.fitframework.util.LocaleContextHolder;

import java.util.List;
import java.util.Locale;

/**
* 地区解析过滤器。
*
* @author 阮睿
* @since 2025-08-01
*/
@Component
public class LocaleResolveFilter implements HttpServerFilter {
private List<String> matchPatterns = List.of("/**");

private List<String> mismatchPatterns = List.of();

private Scope scope = Scope.GLOBAL;

private LocaleResolverRegistry localeResolverRegistry;

/**
* 构造函数。
*
* @param localeResolverRegistry 表示地区解析器注册中心的 {@link LocaleResolverRegistry}。
*/
public LocaleResolveFilter(LocaleResolverRegistry localeResolverRegistry) {
this.localeResolverRegistry = localeResolverRegistry;
}

@Override
public String name() {
return "LocaleResolveFilter";
}

@Override
public int priority() {
return 0;
}

@Override
public List<String> matchPatterns() {
return this.matchPatterns;
}

@Override
public List<String> mismatchPatterns() {
return this.mismatchPatterns;
}

@Override
public void doFilter(HttpClassicServerRequest request, HttpClassicServerResponse response,
HttpServerFilterChain chain) throws DoHttpServerFilterException {
try {
// 如果参数中带有地区,说明用户想使用新地区执行后续的操作,直接设置地区。
String paramLocale = request.queries().first("locale").orElse(null);
Locale responseLocale = null;
// 使用责任链解析 locale
LocaleResolver localeResolver = this.localeResolverRegistry.dispatch(request);
if (paramLocale != null && !paramLocale.trim().isEmpty()) {
responseLocale = Locale.forLanguageTag(paramLocale);
LocaleContextHolder.setLocaleContext(new LocaleContext(responseLocale));
}
// 如果参数中不包含地区,则解析请求所带的地区参数。
else {
Locale locale = localeResolver.resolveLocale(request);
LocaleContextHolder.setLocaleContext(new LocaleContext(locale));
}

// 继续执行后续过滤器。
chain.doFilter(request, response);

// responseLocale 是用户期望设置的地区,不受 server 端处理的影响。
localeResolver.setLocale(response, responseLocale);
} finally {
LocaleContextHolder.clear();
}

}

@Override
public Scope scope() {
return this.scope;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*---------------------------------------------------------------------------------------------
* 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.i18n;

import static modelengine.fitframework.inspection.Validation.notBlank;
import static modelengine.fitframework.inspection.Validation.notNull;

import modelengine.fit.http.server.HttpClassicServerRequest;
import modelengine.fit.http.server.dispatch.MappingTree;
import modelengine.fit.http.server.dispatch.support.DefaultMappingTree;
import modelengine.fit.http.util.i18n.DefualtLocaleResolver;
import modelengine.fit.http.util.i18n.LocaleResolver;
import modelengine.fit.http.util.i18n.RegisterLocaleResolverException;
import modelengine.fitframework.annotation.Component;
import modelengine.fitframework.ioc.BeanContainer;
import modelengine.fitframework.ioc.BeanFactory;
import modelengine.fitframework.plugin.Plugin;
import modelengine.fitframework.plugin.PluginStartedObserver;
import modelengine.fitframework.plugin.PluginStoppingObserver;

import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import modelengine.fitframework.resource.UrlUtils;
import modelengine.fitframework.util.OptionalUtils;
import modelengine.fitframework.util.StringUtils;
import modelengine.fitframework.util.wildcard.PathPattern;
import modelengine.fitframework.util.wildcard.Pattern;

/**
* 地区解析器注册中心
*
* @author 阮睿
* @since 2025-09-07
*/
@Component
public class LocaleResolverRegistry implements PluginStartedObserver, PluginStoppingObserver {
private static final char PATH_SEPARATOR = '/';

// 参考 DefaultHttpDispatcher 的三层结构
private final Map<String, LocaleResolver> noPathVariableResolvers = new ConcurrentHashMap<>();
private final Map<String, MappingTree<LocaleResolver>> pathVariableResolvers = new ConcurrentHashMap<>();
private final Map<String, LocaleResolver> wildcardResolvers = new ConcurrentHashMap<>();

private final LocaleResolver defaultLocaleResolver = new DefualtLocaleResolver();

/**
* 构造函数。
*/
public LocaleResolverRegistry() {
// 与 DefaultHttpDispatcher 保持一致,使用 ConcurrentHashMap 提供线程安全。
this.pathVariableResolvers.put(DefaultMappingTree.PATH_SEPARATOR, new DefaultMappingTree<>());
}

/**
* 注册地区解析器。
*
* @param resolver 表示将要被注册地区解析器的 {@link LocaleResolver}。
*/
public void register(LocaleResolver resolver) {
notNull(resolver, "The locale resolver cannot be null.");
String pathPattern = MappingTree.convertToMatchedPathPattern(resolver.getUrlPattern());
notBlank(pathPattern, "The path pattern cannot be blank.");
LocaleResolver preResolver;
if (pathPattern.contains("**")) {
preResolver = this.wildcardResolvers.put(pathPattern, resolver);
} else if (pathPattern.contains("*")) {
preResolver = this.pathVariableResolvers.get(DefaultMappingTree.PATH_SEPARATOR)
.register(pathPattern, resolver)
.orElse(null);
} else {
preResolver = this.noPathVariableResolvers.put(pathPattern, resolver);
}
if (preResolver != null) {
String message = StringUtils.format("Locale resolver has been registered. [pattern={0}]", pathPattern);
throw new RegisterLocaleResolverException(message);
}
}

/**
* 取消注册地区解析器。
*
* @param resolver 表示待取消注册地区解析器的 {@link LocaleResolver}。
*/
public void unregister(LocaleResolver resolver) {
notNull(resolver, "The locale resolver cannot be null.");
String pathPattern = MappingTree.convertToMatchedPathPattern(resolver.getUrlPattern());
notBlank(pathPattern, "The path pattern cannot be blank.");
if (pathPattern.contains("**")) {
this.wildcardResolvers.remove(pathPattern);
} else if (pathPattern.contains("*")) {
this.pathVariableResolvers.get(DefaultMappingTree.PATH_SEPARATOR).unregister(pathPattern);
;
} else {
this.noPathVariableResolvers.remove(pathPattern);
}
}

/**
* 分派地区解析器。
*
* @param request 表示用于查找对应 {@link LocaleResolver} 请求的 {@link HttpClassicServerRequest}。
* @return 返回匹配的 {@link LocaleResolver}。
*/
public LocaleResolver dispatch(HttpClassicServerRequest request) {
String path = UrlUtils.decodePath(request.path());
return (LocaleResolver) OptionalUtils.get(() -> selectFromNoPathVariableResolvers(path))
.orElse(() -> selectFromPathVariableResolvers(path))
.orElse(() -> selectFromWildcardResolvers(path))
.orDefault(this.defaultLocaleResolver);
}

/**
* 从无路径参数的解析器中查找匹配的解析器。
*
* @param path 表示待匹配路径的 {@link String}。
* @return 表示匹配对应路径解析器的 {@link Optional}{@code <}{@link LocaleResolver}{@code >}。。
*/
private Optional<LocaleResolver> selectFromNoPathVariableResolvers(String path) {
LocaleResolver resolver = this.noPathVariableResolvers.get(path);
return Optional.ofNullable(resolver);
}

/**
* 从路径参数的解析器中查找匹配的解析器。
*
* @param path 待匹配路径的 {@link String}。
* @return 匹配对应路径解析器的 {@link Optional}{@code <}{@link LocaleResolver}{@code >}。
*/
private Optional<LocaleResolver> selectFromPathVariableResolvers(String path) {
return this.pathVariableResolvers.get(DefaultMappingTree.PATH_SEPARATOR).search(path);
}

/**
* 从通配符的解析器中查找匹配的解析器。
*
* @param path 待匹配路径的 {@link String}。
* @return 匹配对应路径解析器的 {@link Optional}{@code <}{@link LocaleResolver}{@code >}。
*/
private Optional<LocaleResolver> selectFromWildcardResolvers(String path) {
for (Map.Entry<String, LocaleResolver> entry : this.wildcardResolvers.entrySet()) {
PathPattern pattern = Pattern.forPath(entry.getKey(), PATH_SEPARATOR);
if (pattern.matches(path)) {
return Optional.of(entry.getValue());
}
}
return Optional.empty();
}

/**
* 当插件启动时,注册插件中的地区解析器。
*
* @param plugin 待注册插件的 {@link Plugin}。
*/
@Override
public void onPluginStarted(Plugin plugin) {
BeanContainer container = plugin.container();
List<LocaleResolver> localeResolvers = container.all(LocaleResolver.class)
.stream()
.map(BeanFactory::<LocaleResolver>get)
.collect(Collectors.toList());
for (LocaleResolver localeResolver : localeResolvers) {
this.register(localeResolver);
}
}

/**
* 当插件停止时,取消注册插件中的地区解析器。
*
* @param plugin 待取消注册插件的 {@link Plugin}。
*/
@Override
public void onPluginStopping(Plugin plugin) {
BeanContainer container = plugin.container();
List<LocaleResolver> localeResolvers = container.all(LocaleResolver.class)
.stream()
.map(BeanFactory::<LocaleResolver>get)
.collect(Collectors.toList());
for (LocaleResolver localeResolver : localeResolvers) {
this.unregister(localeResolver);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fit:
beans:
packages:
- 'modelengine.fitframework.i18n'
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.fitframework.plugin</groupId>
<artifactId>fit-i18n-registry</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Loading