use std::any::TypeId;
use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::sync::{Arc, RwLock};
use crate::context::ITemplateContext;
use crate::expression::TemplateValue;
use crate::templateresource::ITemplateResource;
use crate::util::{Locale, Utf16String};
use super::{
AbstractMessageResolver, IMessageResolver, MessageResolutionError, MessageResolutionResult,
StandardMessageResolutionUtils,
};
type Messages = HashMap<Utf16String, Utf16String>;
type LocalizedMessages = HashMap<Locale, Arc<Messages>>;
type TemplateMessagesHook = dyn Fn(&Utf16String, &dyn ITemplateResource, &Locale) -> MessageResolutionResult<Messages>
+ Send
+ Sync;
type OriginMessagesHook = dyn Fn(TypeId, &Locale) -> Messages + Send + Sync;
type MessageFormatterHook = dyn Fn(
&Locale,
&Utf16String,
Option<&[Option<Arc<TemplateValue>>]>,
) -> MessageResolutionResult<Option<Utf16String>>
+ Send
+ Sync;
type AbsentMessageHook = dyn Fn(
Option<&dyn ITemplateContext>,
Option<TypeId>,
Option<&Utf16String>,
Option<&[Option<Arc<TemplateValue>>]>,
) -> MessageResolutionResult<Option<Utf16String>>
+ Send
+ Sync;
pub struct StandardMessageResolver {
base: AbstractMessageResolver,
messages_by_locale_by_template: RwLock<HashMap<Utf16String, LocalizedMessages>>,
messages_by_locale_by_origin: RwLock<HashMap<TypeId, LocalizedMessages>>,
default_messages: RwLock<Messages>,
template_messages_hook: Option<Arc<TemplateMessagesHook>>,
origin_messages_hook: Option<Arc<OriginMessagesHook>>,
message_formatter_hook: Option<Arc<MessageFormatterHook>>,
absent_message_hook: Option<Arc<AbsentMessageHook>>,
}
impl StandardMessageResolver {
#[must_use]
pub fn new() -> Self {
Self {
base: AbstractMessageResolver::new(
"org.thymeleaf.messageresolver.StandardMessageResolver",
),
messages_by_locale_by_template: RwLock::new(HashMap::new()),
messages_by_locale_by_origin: RwLock::new(HashMap::new()),
default_messages: RwLock::new(HashMap::new()),
template_messages_hook: None,
origin_messages_hook: None,
message_formatter_hook: None,
absent_message_hook: None,
}
}
#[must_use]
pub fn with_template_messages_hook<F>(mut self, hook: F) -> Self
where
F: Fn(&Utf16String, &dyn ITemplateResource, &Locale) -> MessageResolutionResult<Messages>
+ Send
+ Sync
+ 'static,
{
self.template_messages_hook = Some(Arc::new(hook));
self
}
#[must_use]
pub fn with_origin_messages_hook<F>(mut self, hook: F) -> Self
where
F: Fn(TypeId, &Locale) -> Messages + Send + Sync + 'static,
{
self.origin_messages_hook = Some(Arc::new(hook));
self
}
#[must_use]
pub fn with_message_formatter_hook<F>(mut self, hook: F) -> Self
where
F: Fn(
&Locale,
&Utf16String,
Option<&[Option<Arc<TemplateValue>>]>,
) -> MessageResolutionResult<Option<Utf16String>>
+ Send
+ Sync
+ 'static,
{
self.message_formatter_hook = Some(Arc::new(hook));
self
}
#[must_use]
pub fn with_absent_message_hook<F>(mut self, hook: F) -> Self
where
F: Fn(
Option<&dyn ITemplateContext>,
Option<TypeId>,
Option<&Utf16String>,
Option<&[Option<Arc<TemplateValue>>]>,
) -> MessageResolutionResult<Option<Utf16String>>
+ Send
+ Sync
+ 'static,
{
self.absent_message_hook = Some(Arc::new(hook));
self
}
pub fn set_name(&mut self, name: Option<Utf16String>) {
self.base.set_name(name);
}
pub fn set_order(&mut self, order: Option<i32>) {
self.base.set_order(order);
}
#[must_use]
pub const fn get_default_messages(&self) -> &RwLock<HashMap<Utf16String, Utf16String>> {
&self.default_messages
}
pub fn set_default_messages(&self, default_messages: Option<&Messages>) {
if let Some(default_messages) = default_messages {
write_lock(&self.default_messages).extend(default_messages.clone());
}
}
pub fn add_default_message(
&self,
key: Utf16String,
value: Utf16String,
) -> MessageResolutionResult<()> {
self.add_default_message_nullable(Some(key), Some(value))
}
pub fn add_default_message_nullable(
&self,
key: Option<Utf16String>,
value: Option<Utf16String>,
) -> MessageResolutionResult<()> {
let key = key.ok_or_else(|| {
Box::new(MessageResolverArgumentError(
"Key for default message cannot be null",
)) as MessageResolutionError
})?;
let value = value.ok_or_else(|| {
Box::new(MessageResolverArgumentError(
"Value for default message cannot be null",
)) as MessageResolutionError
})?;
write_lock(&self.default_messages).insert(key, value);
Ok(())
}
pub fn clear_default_messages(&self) {
write_lock(&self.default_messages).clear();
}
pub fn register_origin_messages(
origin: TypeId,
locale: Locale,
messages: HashMap<Utf16String, Utf16String>,
) {
StandardMessageResolutionUtils::register_origin_messages(origin, locale, messages);
}
pub fn register_origin_parent(origin: TypeId, parent: TypeId) -> MessageResolutionResult<()> {
StandardMessageResolutionUtils::register_origin_parent(origin, parent)
}
pub fn format_message(
&self,
locale: &Locale,
message: &Utf16String,
message_parameters: Option<&[Option<Arc<TemplateValue>>]>,
) -> MessageResolutionResult<Option<Utf16String>> {
if let Some(hook) = &self.message_formatter_hook {
return hook(locale, message, message_parameters);
}
StandardMessageResolutionUtils::format_message(locale, message, message_parameters)
.map(Some)
}
pub fn resolve_messages_for_template(
&self,
template: &Utf16String,
template_resource: &dyn ITemplateResource,
locale: &Locale,
) -> MessageResolutionResult<HashMap<Utf16String, Utf16String>> {
if let Some(hook) = &self.template_messages_hook {
return hook(template, template_resource, locale);
}
StandardMessageResolutionUtils::resolve_messages_for_template(template_resource, locale)
}
pub fn resolve_messages_for_origin(
&self,
origin: TypeId,
locale: &Locale,
) -> HashMap<Utf16String, Utf16String> {
if let Some(hook) = &self.origin_messages_hook {
return hook(origin, locale);
}
StandardMessageResolutionUtils::resolve_messages_for_origin(origin, locale)
}
#[expect(
clippy::too_many_arguments,
reason = "三个阶段开关与 Java 消息解析方法参数保持一一对应"
)]
pub fn resolve_message_with_phases(
&self,
context: &dyn ITemplateContext,
origin: Option<TypeId>,
key: &Utf16String,
message_parameters: Option<&[Option<Arc<TemplateValue>>]>,
perform_template_based_resolution: bool,
perform_origin_based_resolution: bool,
perform_default_based_resolution: bool,
) -> MessageResolutionResult<Option<Utf16String>> {
let locale = context.get_locale();
if perform_template_based_resolution {
for template_data in context.get_template_stack() {
let Some(template) = template_data.get_template().cloned() else {
continue;
};
let cacheable = template_data
.get_validity()
.is_some_and(crate::cache::ICacheEntryValidity::is_cacheable);
let messages = if cacheable {
self.cached_template_messages(&template, &locale, || {
template_data.get_template_resource().map_or_else(
|| Ok(Messages::new()),
|resource| {
self.resolve_messages_for_template(&template, resource, &locale)
},
)
})?
} else {
Arc::new(template_data.get_template_resource().map_or_else(
|| Ok(Messages::new()),
|resource| self.resolve_messages_for_template(&template, resource, &locale),
)?)
};
if let Some(message) = messages.get(key) {
return self.format_message(&locale, message, message_parameters);
}
}
}
if perform_origin_based_resolution && let Some(origin) = origin {
let messages = self.cached_origin_messages(origin, &locale);
if let Some(message) = messages.get(key) {
return self.format_message(&locale, message, message_parameters);
}
}
if perform_default_based_resolution
&& let Some(message) = read_lock(&self.default_messages).get(key)
{
return self.format_message(&locale, message, message_parameters);
}
Ok(None)
}
fn cached_template_messages(
&self,
template: &Utf16String,
locale: &Locale,
load: impl FnOnce() -> MessageResolutionResult<Messages>,
) -> MessageResolutionResult<Arc<Messages>> {
if let Some(messages) = read_lock(&self.messages_by_locale_by_template)
.get(template)
.and_then(|localized| localized.get(locale))
{
return Ok(Arc::clone(messages));
}
let loaded = Arc::new(load()?);
let mut cache = write_lock(&self.messages_by_locale_by_template);
Ok(Arc::clone(
cache
.entry(template.clone())
.or_default()
.entry(locale.clone())
.or_insert(loaded),
))
}
fn cached_origin_messages(&self, origin: TypeId, locale: &Locale) -> Arc<Messages> {
if let Some(messages) = read_lock(&self.messages_by_locale_by_origin)
.get(&origin)
.and_then(|localized| localized.get(locale))
{
return Arc::clone(messages);
}
let loaded = Arc::new(self.resolve_messages_for_origin(origin, locale));
let mut cache = write_lock(&self.messages_by_locale_by_origin);
Arc::clone(
cache
.entry(origin)
.or_default()
.entry(locale.clone())
.or_insert(loaded),
)
}
}
impl Default for StandardMessageResolver {
fn default() -> Self {
Self::new()
}
}
impl IMessageResolver for StandardMessageResolver {
fn get_name(&self) -> Option<&Utf16String> {
self.base.get_name()
}
fn get_order(&self) -> Option<i32> {
self.base.get_order()
}
fn resolve_message_nullable(
&self,
context: Option<&dyn ITemplateContext>,
origin: Option<TypeId>,
key: Option<&Utf16String>,
message_parameters: Option<&[Option<Arc<TemplateValue>>]>,
) -> MessageResolutionResult<Option<Utf16String>> {
let context = context.ok_or_else(|| {
Box::new(MessageResolverArgumentError("Context cannot be null"))
as MessageResolutionError
})?;
let key = key.ok_or_else(|| {
Box::new(MessageResolverArgumentError("Message key cannot be null"))
as MessageResolutionError
})?;
self.resolve_message_with_phases(context, origin, key, message_parameters, true, true, true)
}
fn create_absent_message_representation_nullable(
&self,
context: Option<&dyn ITemplateContext>,
origin: Option<TypeId>,
key: Option<&Utf16String>,
message_parameters: Option<&[Option<Arc<TemplateValue>>]>,
) -> MessageResolutionResult<Option<Utf16String>> {
if let Some(hook) = &self.absent_message_hook {
return hook(context, origin, key, message_parameters);
}
let key = key.ok_or_else(|| {
Box::new(MessageResolverArgumentError("Message key cannot be null"))
as MessageResolutionError
})?;
let context = context
.ok_or_else(|| Box::new(MessageResolverNullContextError) as MessageResolutionError)?;
Ok(Some(Utf16String::from_rust_str(&format!(
"??{}_{}??",
key.to_string_lossy(),
context.get_locale()
))))
}
}
#[derive(Debug)]
struct MessageResolverArgumentError(&'static str);
impl Display for MessageResolverArgumentError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(self.0)
}
}
impl Error for MessageResolverArgumentError {}
#[derive(Debug)]
struct MessageResolverNullContextError;
impl Display for MessageResolverNullContextError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
formatter.write_str(
"Cannot invoke \"org.thymeleaf.context.ITemplateContext.getLocale()\" because \
\"context\" is null",
)
}
}
impl Error for MessageResolverNullContextError {}
fn read_lock<T>(lock: &RwLock<T>) -> std::sync::RwLockReadGuard<'_, T> {
lock.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write_lock<T>(lock: &RwLock<T>) -> std::sync::RwLockWriteGuard<'_, T> {
lock.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
#[cfg(test)]
mod tests {
use std::sync::RwLock;
use crate::util::Utf16String;
use super::StandardMessageResolver;
#[test]
fn default_messages_are_live_merged_validated_and_cleared() {
let resolver = StandardMessageResolver::new();
let identity = resolver.get_default_messages() as *const RwLock<_>;
assert_eq!(
identity,
resolver.get_default_messages() as *const RwLock<_>
);
resolver
.add_default_message(
Utf16String::from_rust_str("first"),
Utf16String::from_rust_str("one"),
)
.expect("valid default");
resolver.set_default_messages(Some(&std::collections::HashMap::from([
(
Utf16String::from_rust_str("second"),
Utf16String::from_rust_str("two"),
),
(
Utf16String::from_rust_str("first"),
Utf16String::from_rust_str("override"),
),
])));
{
let messages = resolver
.get_default_messages()
.read()
.expect("default messages read");
assert_eq!(messages.len(), 2);
assert_eq!(
messages.get(&Utf16String::from_rust_str("first")),
Some(&Utf16String::from_rust_str("override"))
);
}
let error = resolver
.add_default_message_nullable(None, Some(Utf16String::from_rust_str("value")))
.expect_err("null key");
assert_eq!(error.to_string(), "Key for default message cannot be null");
let error = resolver
.add_default_message_nullable(Some(Utf16String::from_rust_str("key")), None)
.expect_err("null value");
assert_eq!(
error.to_string(),
"Value for default message cannot be null"
);
resolver.clear_default_messages();
assert!(
resolver
.get_default_messages()
.read()
.expect("default messages read")
.is_empty()
);
}
}