Skip to content

Commit f1b5130

Browse files
committed
初步实现
1 parent bcb4f86 commit f1b5130

File tree

7 files changed

+360
-4
lines changed

7 files changed

+360
-4
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
6+
<parent>
7+
<groupId>org.fitframework.plugin</groupId>
8+
<artifactId>fit-plugin-parent</artifactId>
9+
<version>3.6.0-SNAPSHOT</version>
10+
</parent>
11+
12+
<artifactId>fit-i18n-registry</artifactId>
13+
14+
<name>FIT i18n Registry</name>
15+
<description>
16+
FIT Framework Service provide locale resolver registry.
17+
</description>
18+
<url>https://github.com/ModelEngine-Group/fit-framework</url>
19+
20+
<dependencies>
21+
<!-- FIT core -->
22+
<dependency>
23+
<groupId>org.fitframework</groupId>
24+
<artifactId>fit-api</artifactId>
25+
</dependency>
26+
<dependency>
27+
<groupId>org.fitframework</groupId>
28+
<artifactId>fit-util</artifactId>
29+
</dependency>
30+
31+
<!-- Services -->
32+
<dependency>
33+
<groupId>org.fitframework.service</groupId>
34+
<artifactId>fit-http-classic</artifactId>
35+
</dependency>
36+
37+
<!-- Test -->
38+
<dependency>
39+
<groupId>org.junit.jupiter</groupId>
40+
<artifactId>junit-jupiter</artifactId>
41+
<scope>test</scope>
42+
</dependency>
43+
<dependency>
44+
<groupId>org.mockito</groupId>
45+
<artifactId>mockito-core</artifactId>
46+
<scope>test</scope>
47+
</dependency>
48+
<dependency>
49+
<groupId>org.assertj</groupId>
50+
<artifactId>assertj-core</artifactId>
51+
<scope>test</scope>
52+
</dependency>
53+
</dependencies>
54+
55+
<build>
56+
<plugins>
57+
<plugin>
58+
<groupId>org.apache.maven.plugins</groupId>
59+
<artifactId>maven-antrun-plugin</artifactId>
60+
<executions>
61+
<execution>
62+
<phase>package</phase>
63+
<configuration>
64+
<target>
65+
<copy file="${project.build.directory}/${project.build.finalName}.jar"
66+
todir="../../../../../../build/plugins"/>
67+
</target>
68+
</configuration>
69+
<goals>
70+
<goal>run</goal>
71+
</goals>
72+
</execution>
73+
</executions>
74+
</plugin>
75+
</plugins>
76+
</build>
77+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package modelengine.fitframework.i18n;
2+
3+
import modelengine.fit.http.server.DoHttpServerFilterException;
4+
import modelengine.fit.http.server.HttpClassicServerRequest;
5+
import modelengine.fit.http.server.HttpClassicServerResponse;
6+
import modelengine.fit.http.server.HttpServerFilter;
7+
import modelengine.fit.http.server.HttpServerFilterChain;
8+
import modelengine.fit.http.util.i18n.DefualtLocaleResolver;
9+
import modelengine.fit.http.util.i18n.LocaleResolver;
10+
import modelengine.fitframework.annotation.Component;
11+
import modelengine.fitframework.annotation.Scope;
12+
import modelengine.fitframework.util.LocaleContext;
13+
import modelengine.fitframework.util.LocaleContextHolder;
14+
15+
import java.util.List;
16+
import java.util.Locale;
17+
18+
/**
19+
* 地区解析过滤器。
20+
*
21+
* @author 阮睿
22+
* @since 2025-08-01
23+
*/
24+
@Component
25+
public class LocaleResolveFilter implements HttpServerFilter {
26+
27+
private LocaleResolver localeResolver = null;
28+
29+
private List<String> matchPatterns = List.of("/**");
30+
31+
private List<String> mismatchPatterns = List.of();
32+
33+
private Scope scope = Scope.GLOBAL;
34+
35+
private LocaleResolverRegistry localeResolverRegistry;
36+
37+
/**
38+
* 构造函数。
39+
*
40+
* @param localeResolver 表示地区解析器的 {@link LocaleResolver}。
41+
*/
42+
public LocaleResolveFilter(LocaleResolver localeResolver, LocaleResolverRegistry localeResolverRegistry) {
43+
localeResolver = localeResolver;
44+
}
45+
46+
/**
47+
* 构造函数。
48+
*/
49+
public LocaleResolveFilter(LocaleResolverRegistry localeResolverRegistry) {
50+
this.localeResolver = new DefualtLocaleResolver();
51+
}
52+
53+
@Override
54+
public String name() {
55+
return "LocaleResolveFilter";
56+
}
57+
58+
@Override
59+
public int priority() {
60+
return 0;
61+
}
62+
63+
@Override
64+
public List<String> matchPatterns() {
65+
return matchPatterns;
66+
}
67+
68+
@Override
69+
public List<String> mismatchPatterns() {
70+
return mismatchPatterns;
71+
}
72+
73+
@Override
74+
public void doFilter(HttpClassicServerRequest request, HttpClassicServerResponse response,
75+
HttpServerFilterChain chain) throws DoHttpServerFilterException {
76+
try {
77+
// 如果参数中带有地区,说明用户想使用新地区执行后续的操作,直接设置地区。
78+
String paramLocale = request.queries().first("locale").orElse(null);
79+
Locale responseLocale = null;
80+
if (paramLocale != null && !paramLocale.trim().isEmpty()) {
81+
responseLocale = Locale.forLanguageTag(paramLocale);
82+
LocaleContextHolder.setLocaleContext(new LocaleContext(responseLocale));
83+
}
84+
// 如果参数中不包含地区,则解析请求所带的地区参数。
85+
else {
86+
// 使用责任链解析locale
87+
Locale locale = this.localeResolverRegistry.resolveLocale(request);
88+
if (locale == null) {
89+
locale = this.localeResolver.resolveLocale(request); // fallback
90+
}
91+
LocaleContextHolder.setLocaleContext(new LocaleContext(locale));
92+
}
93+
94+
// 继续执行后续过滤器。
95+
chain.doFilter(request, response);
96+
97+
// responseLocale 是用户期望设置的地区,不受 server 端处理的影响。
98+
localeResolver.setLocale(response, responseLocale);
99+
} finally {
100+
LocaleContextHolder.clear();
101+
}
102+
103+
}
104+
105+
@Override
106+
public Scope scope() {
107+
return scope;
108+
}
109+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package modelengine.fitframework.i18n;
2+
3+
import modelengine.fit.http.util.i18n.LocaleResolver;
4+
5+
import java.util.List;
6+
import java.util.regex.Pattern;
7+
import java.util.stream.Collectors;
8+
9+
public class LocaleResolverEntry {
10+
private final LocaleResolver resolver;
11+
private final Pattern urlPattern;
12+
private final int priority;
13+
14+
public LocaleResolverEntry(LocaleResolver resolver, String urlPattern, int priority) {
15+
this.resolver = resolver;
16+
this.priority = priority;
17+
this.urlPattern = this.compilePattern(urlPattern);
18+
}
19+
20+
private Pattern compilePattern(String pattern) {
21+
// 支持Ant风格的路径匹配: /api/** -> /api/.*
22+
String regex = pattern
23+
.replace("**", ".*")
24+
.replace("*", "[^/]*")
25+
.replace("?", ".");
26+
return Pattern.compile("^" + regex + "$");
27+
}
28+
29+
public boolean matches(String requestPath) {
30+
return urlPattern.matcher(requestPath).matches();
31+
}
32+
33+
public int getPriority() {
34+
return priority;
35+
}
36+
37+
public LocaleResolver getResolver() {
38+
return resolver;
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package modelengine.fitframework.i18n;
2+
3+
import modelengine.fit.http.server.HttpClassicServerRequest;
4+
import modelengine.fit.http.server.HttpClassicServerResponse;
5+
import modelengine.fit.http.server.dispatch.MappingTree;
6+
import modelengine.fit.http.server.dispatch.support.DefaultMappingTree;
7+
import modelengine.fit.http.util.i18n.LocaleResolver;
8+
import modelengine.fitframework.annotation.Component;
9+
import modelengine.fitframework.ioc.BeanContainer;
10+
import modelengine.fitframework.ioc.BeanFactory;
11+
import modelengine.fitframework.plugin.Plugin;
12+
import modelengine.fitframework.plugin.PluginStartedObserver;
13+
import modelengine.fitframework.plugin.PluginStoppingObserver;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.Optional;
17+
import java.util.concurrent.ConcurrentHashMap;
18+
import java.util.stream.Collectors;
19+
import modelengine.fitframework.resource.UrlUtils;
20+
import modelengine.fitframework.util.OptionalUtils;
21+
import modelengine.fitframework.util.wildcard.PathPattern;
22+
import modelengine.fitframework.util.wildcard.Pattern;
23+
24+
@Component
25+
public class LocaleResolverRegistry implements PluginStartedObserver, PluginStoppingObserver {
26+
private static final char PATH_SEPARATOR = '/';
27+
28+
// 参考DefaultHttpDispatcher的三层结构
29+
private final Map<String, LocaleResolver> noPathVariableResolvers = new ConcurrentHashMap<>();
30+
private final MappingTree<LocaleResolver> pathVariableResolvers = new DefaultMappingTree<>();
31+
private final Map<String, LocaleResolver> wildcardResolvers = new ConcurrentHashMap<>();
32+
33+
34+
public void register(String pluginName, LocaleResolver resolver, String urlPattern) {
35+
String pathPattern = MappingTree.convertToMatchedPathPattern(urlPattern);
36+
if (pathPattern.contains("**")) {
37+
wildcardResolvers.put(pathPattern, resolver);
38+
} else if (pathPattern.contains("*")) {
39+
pathVariableResolvers.register(pathPattern, resolver);
40+
} else {
41+
noPathVariableResolvers.put(pathPattern, resolver);
42+
}
43+
}
44+
45+
public void unregister(String pluginName, List<String> urlPatterns) {
46+
for (String pattern : urlPatterns) {
47+
String pathPattern = MappingTree.convertToMatchedPathPattern(pattern);
48+
if (pathPattern.contains("**")) {
49+
wildcardResolvers.remove(pathPattern);
50+
} else if (pathPattern.contains("*")) {
51+
pathVariableResolvers.unregister(pathPattern);
52+
} else {
53+
noPathVariableResolvers.remove(pathPattern);
54+
}
55+
}
56+
}
57+
58+
public LocaleResolver dispatch(HttpClassicServerRequest request) {
59+
String path = UrlUtils.decodePath(request.path());
60+
return (LocaleResolver) OptionalUtils.get(() -> selectFromNoPathVariableResolvers(path))
61+
.orElse(() -> selectFromPathVariableResolvers(path))
62+
.orElse(() -> selectFromWildcardResolvers(path))
63+
.orElse(null);
64+
}
65+
66+
private Optional<LocaleResolver> selectFromNoPathVariableResolvers(String path) {
67+
LocaleResolver resolver = noPathVariableResolvers.get(path);
68+
return Optional.ofNullable(resolver);
69+
}
70+
71+
private Optional<LocaleResolver> selectFromPathVariableResolvers(String path) {
72+
return pathVariableResolvers.search(path);
73+
}
74+
75+
private Optional<LocaleResolver> selectFromWildcardResolvers(String path) {
76+
for (Map.Entry<String, LocaleResolver> entry : wildcardResolvers.entrySet()) {
77+
PathPattern pattern = Pattern.forPath(entry.getKey(), PATH_SEPARATOR);
78+
if (pattern.matches(path)) {
79+
return Optional.of(entry.getValue());
80+
}
81+
}
82+
return Optional.empty();
83+
}
84+
85+
@Override
86+
public void onPluginStarted(Plugin plugin) {
87+
BeanContainer container = plugin.container();
88+
List<LocaleResolver> localeResolvers = container.all(LocaleResolver.class)
89+
.stream()
90+
.map(BeanFactory::<LocaleResolver>get)
91+
.collect(Collectors.toList());
92+
for(LocaleResolver localeResolver : localeResolvers) {
93+
this.register(localeResolver.getName(),localeResolver,localeResolver.getUrlPattern(),localeResolver.getPriority());
94+
}
95+
}
96+
97+
@Override
98+
public void onPluginStopping(Plugin plugin) {
99+
BeanContainer container = plugin.container();
100+
List<LocaleResolver> localeResolvers = container.all(LocaleResolver.class)
101+
.stream()
102+
.map(BeanFactory::<LocaleResolver>get)
103+
.collect(Collectors.toList());
104+
for(LocaleResolver localeResolver : localeResolvers) {
105+
this.unregister(localeResolver.getName());
106+
}
107+
}
108+
}

framework/fit/java/fit-builtin/plugins/pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
<module>fit-value-fastjson</module>
4646
<module>fit-validation-hibernate-jakarta</module>
4747
<module>fit-validation-hibernate-javax</module>
48+
<module>fit-i18n-registry</module>
4849
</modules>
4950

5051
<dependencyManagement>

framework/fit/java/fit-builtin/services/fit-http-classic/definition/src/main/java/modelengine/fit/http/util/i18n/DefualtLocaleResolver.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@
1919
* @since 2025-08-01
2020
*/
2121
public class DefualtLocaleResolver implements LocaleResolver {
22-
public static final String DEFAULT_COOKIE_NAME = "locale";
23-
public static final int DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
24-
public static final String DEFAULT_COOKIE_DOMAIN = "/";
25-
public static final String DEFAULT_COOKIE_PATH = "/";
2622
private String cookieName = DEFAULT_COOKIE_NAME;
2723
private int cookieMaxAge = DEFAULT_COOKIE_MAX_AGE;
2824
private String cookieDomain = DEFAULT_COOKIE_DOMAIN;
@@ -65,6 +61,21 @@ public void setLocale(HttpClassicServerResponse response, Locale locale) {
6561
}
6662
}
6763

64+
@Override
65+
public String getName() {
66+
return "DefualtLocaleResolver";
67+
}
68+
69+
@Override
70+
public String getUrlPattern() {
71+
return "/**";
72+
}
73+
74+
@Override
75+
public int getPriority() {
76+
return 0;
77+
}
78+
6879
/**
6980
* 设置存储地区信息的 Cookie 名称。
7081
*

0 commit comments

Comments
 (0)