use std::collections::HashMap;
use std::error::Error;
use std::fmt::{Display, Formatter};
use std::sync::{Arc, OnceLock, RwLock, RwLockReadGuard, RwLockWriteGuard};
use crate::templatemode::TemplateMode;
use crate::util::{Utf16String, case_fold_unit};
use super::{ElementName, ElementNameError, HTMLElementName, TextElementName, XMLElementName};
static HTML_REPOSITORY: OnceLock<RwLock<ElementNamesRepository>> = OnceLock::new();
static XML_REPOSITORY: OnceLock<RwLock<ElementNamesRepository>> = OnceLock::new();
static TEXT_REPOSITORY: OnceLock<RwLock<ElementNamesRepository>> = OnceLock::new();
#[derive(Clone)]
pub enum ElementNameValue {
Html(Arc<HTMLElementName>),
Xml(Arc<XMLElementName>),
Text(Arc<TextElementName>),
}
impl ElementNameValue {
#[must_use]
pub fn as_element_name(&self) -> &ElementName {
match self {
Self::Html(value) => value.as_element_name(),
Self::Xml(value) => value.as_element_name(),
Self::Text(value) => value.as_element_name(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ElementNamesError {
IllegalArgument(&'static str),
StringIndexOutOfBounds {
offset: i32,
length: i32,
buffer_length: usize,
},
UnknownTemplateMode(TemplateMode),
ElementName(ElementNameError),
RepositoryAliasCollision,
}
impl ElementNamesError {
#[must_use]
pub const fn class_name(&self) -> &'static str {
match self {
Self::IllegalArgument(_)
| Self::UnknownTemplateMode(_)
| Self::ElementName(ElementNameError::InvalidElementName) => {
"java.lang.IllegalArgumentException"
}
Self::StringIndexOutOfBounds { .. } => "java.lang.StringIndexOutOfBoundsException",
Self::ElementName(error) => error.class_name(),
Self::RepositoryAliasCollision => "java.lang.IndexOutOfBoundsException",
}
}
}
impl Display for ElementNamesError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::IllegalArgument(message) => formatter.write_str(message),
Self::StringIndexOutOfBounds {
offset,
length,
buffer_length,
} => write!(
formatter,
"offset {offset}, count {length}, length {buffer_length}"
),
Self::UnknownTemplateMode(mode) => {
write!(formatter, "Unknown template mode '{mode}'")
}
Self::ElementName(error) => Display::fmt(error, formatter),
Self::RepositoryAliasCollision => {
formatter.write_str("repository alias already exists")
}
}
}
}
impl Error for ElementNamesError {}
impl From<ElementNameError> for ElementNamesError {
fn from(value: ElementNameError) -> Self {
Self::ElementName(value)
}
}
pub struct ElementNames;
impl ElementNames {
pub fn for_name_buffer(
template_mode: Option<TemplateMode>,
buffer: Option<&[u16]>,
offset: i32,
length: i32,
) -> Result<ElementNameValue, ElementNamesError> {
let mode = require_mode(template_mode)?;
if mode == TemplateMode::RAW {
return Err(ElementNamesError::UnknownTemplateMode(mode));
}
let text = checked_buffer(buffer, offset, length, mode.is_text())?;
Self::for_name(Some(mode), Some(&Utf16String::from_utf16(text.to_vec())))
}
pub fn for_name(
template_mode: Option<TemplateMode>,
element_name: Option<&Utf16String>,
) -> Result<ElementNameValue, ElementNamesError> {
let mode = require_mode(template_mode)?;
match mode {
TemplateMode::HTML => Self::for_html_name(element_name).map(ElementNameValue::Html),
TemplateMode::XML => Self::for_xml_name(element_name).map(ElementNameValue::Xml),
mode if mode.is_text() => Self::for_text_name(element_name).map(ElementNameValue::Text),
mode => Err(ElementNamesError::UnknownTemplateMode(mode)),
}
}
pub fn for_name_with_prefix(
template_mode: Option<TemplateMode>,
prefix: Option<&Utf16String>,
element_name: Option<&Utf16String>,
) -> Result<ElementNameValue, ElementNamesError> {
let mode = require_mode(template_mode)?;
match mode {
TemplateMode::HTML => {
Self::for_html_name_with_prefix(prefix, element_name).map(ElementNameValue::Html)
}
TemplateMode::XML => {
Self::for_xml_name_with_prefix(prefix, element_name).map(ElementNameValue::Xml)
}
mode if mode.is_text() => {
Self::for_text_name_with_prefix(prefix, element_name).map(ElementNameValue::Text)
}
mode => Err(ElementNamesError::UnknownTemplateMode(mode)),
}
}
pub fn for_text_name(
element_name: Option<&Utf16String>,
) -> Result<Arc<TextElementName>, ElementNamesError> {
let element_name = element_name.ok_or(ElementNamesError::IllegalArgument(
"Name cannot be null or empty",
))?;
match repository_get_or_store(TemplateMode::TEXT, element_name, || {
build_text(element_name)
})? {
ElementNameValue::Text(value) => Ok(value),
_ => unreachable!("text repository contains only text names"),
}
}
pub fn for_xml_name(
element_name: Option<&Utf16String>,
) -> Result<Arc<XMLElementName>, ElementNamesError> {
let element_name = require_non_blank_name(element_name)?;
match repository_get_or_store(TemplateMode::XML, element_name, || build_xml(element_name))?
{
ElementNameValue::Xml(value) => Ok(value),
_ => unreachable!("xml repository contains only xml names"),
}
}
pub fn for_html_name(
element_name: Option<&Utf16String>,
) -> Result<Arc<HTMLElementName>, ElementNamesError> {
let element_name = require_non_blank_name(element_name)?;
match repository_get_or_store(TemplateMode::HTML, element_name, || {
build_html(element_name)
})? {
ElementNameValue::Html(value) => Ok(value),
_ => unreachable!("html repository contains only html names"),
}
}
pub fn for_text_name_with_prefix(
prefix: Option<&Utf16String>,
element_name: Option<&Utf16String>,
) -> Result<Arc<TextElementName>, ElementNamesError> {
let element_name = element_name.ok_or(ElementNamesError::IllegalArgument(
"Name cannot be null (nor empty if prefix is not empty)",
))?;
if trim_is_empty(element_name) && has_non_blank_prefix(prefix) {
return Err(ElementNamesError::IllegalArgument(
"Name cannot be null (nor empty if prefix is not empty)",
));
}
if !has_non_blank_prefix(prefix) {
return Self::for_text_name(Some(element_name));
}
let lookup = namespaced(prefix.expect("non-blank prefix"), element_name);
match repository_get_or_store(TemplateMode::TEXT, &lookup, || {
Ok(ElementNameValue::Text(Arc::new(TextElementName::for_name(
prefix.cloned(),
Some(element_name.clone()),
)?)))
})? {
ElementNameValue::Text(value) => Ok(value),
_ => unreachable!("text repository contains only text names"),
}
}
pub fn for_xml_name_with_prefix(
prefix: Option<&Utf16String>,
element_name: Option<&Utf16String>,
) -> Result<Arc<XMLElementName>, ElementNamesError> {
let element_name = require_non_blank_name(element_name)?;
if !has_non_blank_prefix(prefix) {
return Self::for_xml_name(Some(element_name));
}
let lookup = namespaced(prefix.expect("non-blank prefix"), element_name);
match repository_get_or_store(TemplateMode::XML, &lookup, || {
Ok(ElementNameValue::Xml(Arc::new(XMLElementName::for_name(
prefix.cloned(),
Some(element_name.clone()),
)?)))
})? {
ElementNameValue::Xml(value) => Ok(value),
_ => unreachable!("xml repository contains only xml names"),
}
}
pub fn for_html_name_with_prefix(
prefix: Option<&Utf16String>,
element_name: Option<&Utf16String>,
) -> Result<Arc<HTMLElementName>, ElementNamesError> {
let element_name = require_non_blank_name(element_name)?;
if !has_non_blank_prefix(prefix) {
return Self::for_html_name(Some(element_name));
}
let lookup = namespaced(prefix.expect("non-blank prefix"), element_name);
match repository_get_or_store(TemplateMode::HTML, &lookup, || {
Ok(ElementNameValue::Html(Arc::new(HTMLElementName::for_name(
prefix.cloned(),
Some(element_name.clone()),
)?)))
})? {
ElementNameValue::Html(value) => Ok(value),
_ => unreachable!("html repository contains only html names"),
}
}
}
struct ElementNamesRepository {
values: HashMap<Vec<u16>, ElementNameValue>,
}
fn repository_get_or_store(
mode: TemplateMode,
lookup: &Utf16String,
builder: impl FnOnce() -> Result<ElementNameValue, ElementNamesError>,
) -> Result<ElementNameValue, ElementNamesError> {
let repository = repository(mode);
let key = repository_key(mode, lookup);
if let Some(value) = read_recovering_poison(repository).values.get(&key) {
return Ok(value.clone());
}
let mut repository = write_recovering_poison(repository);
if let Some(value) = repository.values.get(&key) {
return Ok(value.clone());
}
let value = builder()?;
let names = value.as_element_name().get_complete_element_names();
let names = read_recovering_poison(&names).clone();
let mut keys = Vec::with_capacity(names.len());
for name in names.into_iter().flatten() {
let alias = repository_key(mode, &name);
if let Some(existing) = repository.values.get(&alias) {
return Ok(existing.clone());
}
keys.push(alias);
}
for alias in keys {
repository.values.insert(alias, value.clone());
}
Ok(value)
}
fn repository(mode: TemplateMode) -> &'static RwLock<ElementNamesRepository> {
let slot = match mode {
TemplateMode::HTML => &HTML_REPOSITORY,
TemplateMode::XML => &XML_REPOSITORY,
_ => &TEXT_REPOSITORY,
};
slot.get_or_init(|| {
RwLock::new(ElementNamesRepository {
values: HashMap::with_capacity(500),
})
})
}
fn repository_key(mode: TemplateMode, value: &Utf16String) -> Vec<u16> {
if mode.is_case_sensitive() {
value.as_utf16().to_vec()
} else {
value
.as_utf16()
.iter()
.map(|unit| case_fold_unit(*unit))
.collect()
}
}
fn build_text(name: &Utf16String) -> Result<ElementNameValue, ElementNamesError> {
let (prefix, local) = split_first(name, &[u16::from(b':')], false);
Ok(ElementNameValue::Text(Arc::new(TextElementName::for_name(
prefix,
Some(local),
)?)))
}
fn build_xml(name: &Utf16String) -> Result<ElementNameValue, ElementNamesError> {
let (prefix, local) = split_first(name, &[u16::from(b':')], false);
Ok(ElementNameValue::Xml(Arc::new(XMLElementName::for_name(
prefix,
Some(local),
)?)))
}
fn build_html(name: &Utf16String) -> Result<ElementNameValue, ElementNamesError> {
let units = name.as_utf16();
let split = units.iter().position(|unit| matches!(*unit, 0x3a | 0x2d));
let (prefix, local) = match split {
Some(0) | None => (None, name.clone()),
Some(index) if units[index] == u16::from(b':') => {
let candidate = &units[..=index];
if equals_ascii_ignore_case(candidate, "xml:")
|| equals_ascii_ignore_case(candidate, "xmlns:")
{
(None, name.clone())
} else {
(
Some(Utf16String::from_utf16(units[..index].to_vec())),
Utf16String::from_utf16(units[index + 1..].to_vec()),
)
}
}
Some(index) => (
Some(Utf16String::from_utf16(units[..index].to_vec())),
Utf16String::from_utf16(units[index + 1..].to_vec()),
),
};
Ok(ElementNameValue::Html(Arc::new(HTMLElementName::for_name(
prefix,
Some(local),
)?)))
}
fn split_first(
name: &Utf16String,
separators: &[u16],
_unused: bool,
) -> (Option<Utf16String>, Utf16String) {
let units = name.as_utf16();
match units.iter().position(|unit| separators.contains(unit)) {
Some(0) | None => (None, name.clone()),
Some(index) => (
Some(Utf16String::from_utf16(units[..index].to_vec())),
Utf16String::from_utf16(units[index + 1..].to_vec()),
),
}
}
fn require_mode(mode: Option<TemplateMode>) -> Result<TemplateMode, ElementNamesError> {
mode.ok_or(ElementNamesError::IllegalArgument(
"Template Mode cannot be null",
))
}
fn require_non_blank_name(name: Option<&Utf16String>) -> Result<&Utf16String, ElementNamesError> {
let name = name.ok_or(ElementNamesError::IllegalArgument(
"Name cannot be null or empty",
))?;
if trim_is_empty(name) {
return Err(ElementNamesError::IllegalArgument(
"Name cannot be null or empty",
));
}
Ok(name)
}
fn checked_buffer(
buffer: Option<&[u16]>,
offset: i32,
length: i32,
allow_empty: bool,
) -> Result<&[u16], ElementNamesError> {
let buffer = buffer.ok_or(ElementNamesError::IllegalArgument(
"Name cannot be null or empty",
))?;
if (!allow_empty && length == 0) || offset < 0 || length < 0 {
return Err(ElementNamesError::IllegalArgument(if length == 0 {
"Name cannot be null or empty"
} else {
"Both name offset and length must be equal to or greater than zero"
}));
}
let start = usize::try_from(offset).unwrap_or(usize::MAX);
let count = usize::try_from(length).unwrap_or(usize::MAX);
if start > buffer.len() || count > buffer.len().saturating_sub(start) {
return Err(ElementNamesError::StringIndexOutOfBounds {
offset,
length,
buffer_length: buffer.len(),
});
}
Ok(&buffer[start..start + count])
}
fn trim_is_empty(value: &Utf16String) -> bool {
value.as_utf16().iter().all(|unit| *unit <= 0x20)
}
fn has_non_blank_prefix(prefix: Option<&Utf16String>) -> bool {
prefix.is_some_and(|value| !trim_is_empty(value))
}
fn namespaced(prefix: &Utf16String, name: &Utf16String) -> Utf16String {
let mut result = prefix.as_utf16().to_vec();
result.push(u16::from(b':'));
result.extend_from_slice(name.as_utf16());
Utf16String::from_utf16(result)
}
fn equals_ascii_ignore_case(value: &[u16], expected: &str) -> bool {
value.len() == expected.len()
&& value
.iter()
.zip(expected.bytes())
.all(|(actual, expected)| {
case_fold_unit(*actual) == case_fold_unit(u16::from(expected))
})
}
fn read_recovering_poison<T>(lock: &RwLock<T>) -> RwLockReadGuard<'_, T> {
lock.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
fn write_recovering_poison<T>(lock: &RwLock<T>) -> RwLockWriteGuard<'_, T> {
lock.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}