use std::collections::BTreeMap;
use crate::content::node::NodeState;
use crate::content::property::PropertyValue;
use crate::index::lucene::documents::aggregate::{self, Aggregate};
use crate::index::lucene::documents::name_pattern::{ALL_PROPERTIES, NamePattern};
use crate::index::value_pattern::ValuePattern;
use crate::index::{
IndexError, IndexResult, IndexWarning, children_in_tree_order, converting_boolean,
converting_long, converting_strings, strict_name, strict_names, strict_string, values_of,
};
pub const DEFAULT_BOOST: f32 = 1.0;
pub const DEFAULT_PROPERTY_WEIGHT: i64 = 5;
pub const OAK_CODEC_NAME: &str = "oakCodec";
pub const NODE_NAME_PROPERTY: &str = ":nodeName";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum CodecVerdict {
OakCodec,
Named(String),
Lucene46,
}
#[allow(
clippy::struct_excessive_bools,
reason = "one field per property of Oak's own definition, which are genuinely independent flags"
)]
#[derive(Clone, Debug, PartialEq)]
pub struct IndexingRule {
pub node_type_name: String,
pub inherited: bool,
registered_types: Vec<String>,
pub boost: f32,
pub include_property_types: Vec<String>,
pub node_type_index: bool,
pub node_name_indexed: bool,
pub properties: BTreeMap<String, PropertyDefinition>,
pub patterns: Vec<(NamePattern, PropertyDefinition)>,
pub aggregate: Aggregate,
pub fulltext_enabled: bool,
pub node_fulltext_indexed: bool,
pub indexes_all_nodes_of_matching_type: bool,
}
impl IndexingRule {
#[must_use]
pub fn config_of(&self, property_name: &str) -> Option<&PropertyDefinition> {
if let Some(definition) = self.properties.get(&fold_case(property_name)) {
return Some(definition);
}
self.patterns
.iter()
.find(|(pattern, _)| pattern.matches(property_name))
.map(|(_, definition)| definition)
}
#[must_use]
pub fn registers_under(&self, node_type_name: &str) -> bool {
self.registered_types
.iter()
.any(|registered| registered == node_type_name)
}
pub fn definitions(&self) -> impl Iterator<Item = &PropertyDefinition> {
self.properties
.values()
.chain(self.patterns.iter().map(|(_, definition)| definition))
}
#[must_use]
pub fn property_includes(&self) -> Vec<PropertyInclude<'_>> {
let exact = self.properties.values().map(|definition| PropertyInclude {
definition,
pattern: None,
});
let patterned = self
.patterns
.iter()
.map(|(pattern, definition)| PropertyInclude {
definition,
pattern: Some(pattern),
});
exact
.chain(patterned)
.filter(|include| include.definition.relative)
.collect()
}
}
#[derive(Clone, Copy, Debug)]
pub struct PropertyInclude<'rule> {
pub definition: &'rule PropertyDefinition,
pub pattern: Option<&'rule NamePattern>,
}
#[allow(
clippy::struct_excessive_bools,
reason = "one field per property of Oak's own definition, which are genuinely independent flags"
)]
#[derive(Clone, Debug, PartialEq)]
pub struct PropertyDefinition {
pub node_name: String,
pub name: String,
pub is_regexp: bool,
pub relative: bool,
pub ancestors: Vec<String>,
pub index: bool,
pub property_index: bool,
pub analyzed: bool,
pub node_scope_index: bool,
pub ordered: bool,
pub use_in_excerpt: bool,
pub use_in_suggest: bool,
pub use_in_spellcheck: bool,
pub facet: bool,
pub null_check_enabled: bool,
pub not_null_check_enabled: bool,
pub exclude_from_aggregation: bool,
pub boost: f32,
pub weight: i64,
pub included_property_types: Vec<String>,
pub declared_type: Option<String>,
pub unique: bool,
pub sync: bool,
pub value_pattern: ValuePattern,
}
impl PropertyDefinition {
#[must_use]
pub const fn fulltext_enabled(&self) -> bool {
self.index && (self.analyzed || self.node_scope_index)
}
}
#[allow(
clippy::struct_excessive_bools,
reason = "one field per property of Oak's own definition, which are genuinely independent flags"
)]
#[derive(Clone, Debug, PartialEq)]
pub struct IndexingRules {
pub rules: Vec<IndexingRule>,
pub aggregates: Vec<Aggregate>,
pub codec: CodecVerdict,
pub index_original_term: bool,
pub suggest_analyzed: bool,
pub evaluate_path_restrictions: bool,
pub maximum_field_length: Option<i64>,
pub has_tika_configuration: bool,
}
fn fold_case(name: &str) -> String {
name.to_lowercase()
}
fn optional_boolean(node: &NodeState<'_>, name: &str, default: bool) -> IndexResult<bool> {
let property = node.property(name)?;
Ok(match property {
None => default,
Some(property) => converting_boolean(Some(&property)),
})
}
fn optional_long(node: &NodeState<'_>, name: &str, default: i64) -> IndexResult<i64> {
Ok(converting_long(node.property(name)?.as_ref()).unwrap_or(default))
}
fn optional_float(node: &NodeState<'_>, name: &str, default: f32) -> IndexResult<f32> {
let Some(property) = node.property(name)? else {
return Ok(default);
};
let Some(text) = values_of(&property)
.first()
.and_then(PropertyValue::as_text)
else {
return Ok(default);
};
Ok(text.parse::<f32>().unwrap_or(default))
}
impl PropertyDefinition {
pub fn read(
definition_path: &str,
node_name: &str,
node: &NodeState<'_>,
warnings: &mut Vec<IndexWarning>,
) -> IndexResult<Self> {
for unsupported in [
"function",
"dynamicBoost",
"useInSimilarity",
"similarityTags",
] {
if node.property(unsupported)?.is_some() {
return Err(IndexError::UnsupportedDefinition {
definition_path: definition_path.to_owned(),
feature: format!("the property definition {node_name} sets {unsupported}"),
});
}
}
let is_regexp = optional_boolean(node, "isRegexp", false)?;
let name = strict_string(node.property("name")?.as_ref())
.map_or_else(|| node_name.to_owned(), str::to_owned);
let relative = is_relative_property(&name);
let boost = optional_float(node, "boost", DEFAULT_BOOST)?;
let weight = optional_long(node, "weight", DEFAULT_PROPERTY_WEIGHT)?;
let index = optional_boolean(node, "index", true)?;
let if_indexed = |name: &str, default: bool| -> IndexResult<bool> {
if index {
optional_boolean(node, name, default)
} else {
Ok(false)
}
};
let analyzed = if node.property("boost")?.is_some() {
true
} else {
if_indexed("analyzed", false)?
};
let unique = if_indexed("unique", false)?;
let sync = unique || if_indexed("sync", false)?;
let null_check_enabled = if_indexed("nullCheckEnabled", false)?;
if null_check_enabled && is_regexp {
return Err(IndexError::UnsupportedDefinition {
definition_path: definition_path.to_owned(),
feature: format!(
"the property definition {node_name} sets nullCheckEnabled on a regular \
expression, which Oak's own validation refuses"
),
});
}
Ok(Self {
node_name: node_name.to_owned(),
ancestors: ancestors_of(&name),
name,
is_regexp,
relative,
index,
property_index: sync || if_indexed("propertyIndex", false)?,
analyzed,
node_scope_index: if_indexed("nodeScopeIndex", false)?,
ordered: if_indexed("ordered", false)?,
use_in_excerpt: if_indexed("useInExcerpt", false)?,
use_in_suggest: if_indexed("useInSuggest", false)?,
use_in_spellcheck: if_indexed("useInSpellcheck", false)?,
facet: if_indexed("facets", false)?,
null_check_enabled,
not_null_check_enabled: if_indexed("notNullCheckEnabled", false)?,
exclude_from_aggregation: if_indexed("excludeFromAggregation", false)?,
boost,
weight,
declared_type: strict_string(node.property("type")?.as_ref()).map(str::to_owned),
included_property_types: converting_strings(
node.property("oak.experimental.includePropertyTypes")?
.as_ref(),
),
unique,
sync,
value_pattern: ValuePattern::from_definition(node, definition_path, warnings)?,
})
}
}
fn is_relative_property(name: &str) -> bool {
!name.starts_with('/') && name != ALL_PROPERTIES && name.contains('/')
}
fn ancestors_of(name: &str) -> Vec<String> {
if name == ALL_PROPERTIES {
return Vec::new();
}
let Some(at) = name.rfind('/') else {
return Vec::new();
};
name[..at]
.split('/')
.filter(|element| !element.is_empty())
.map(str::to_owned)
.collect()
}
impl IndexingRule {
pub fn read(
definition_path: &str,
node_type_name: &str,
node: &NodeState<'_>,
content_root: &NodeState<'_>,
aggregates: &[Aggregate],
warnings: &mut Vec<IndexWarning>,
) -> IndexResult<Self> {
let inherited = optional_boolean(node, "inherited", true)?;
let node_type_index = optional_boolean(node, "nodeTypeIndex", false)?;
let mut properties: BTreeMap<String, PropertyDefinition> = BTreeMap::new();
let mut patterns = Vec::new();
if node_type_index {
if optional_boolean(node, "sync", false)? {
return Err(IndexError::UnsupportedDefinition {
definition_path: definition_path.to_owned(),
feature: format!(
"the rule {node_type_name} is a synchronous nodeTypeIndex, which Oak \
keeps a :property-index for"
),
});
}
} else if let Some(property_node) = node.child_node("properties")? {
for (name, child) in children_in_tree_order(&property_node)? {
if properties.contains_key(&fold_case(&name)) {
continue;
}
let definition =
PropertyDefinition::read(definition_path, &name, &child, warnings)?;
if definition.sync || definition.unique {
return Err(IndexError::UnsupportedDefinition {
definition_path: definition_path.to_owned(),
feature: format!(
"the property definition {name} of rule {node_type_name} is {}, which \
Oak keeps a :property-index for",
if definition.unique { "unique" } else { "sync" }
),
});
}
if definition.is_regexp {
let pattern = NamePattern::parse(definition_path, &definition.name)?;
patterns.push((pattern, definition));
} else {
properties.insert(fold_case(&definition.name), definition);
}
}
}
let aggregate = aggregate::for_node_type(aggregates, node_type_name);
let fulltext_enabled = aggregate.has_node_aggregates()
|| properties
.values()
.chain(patterns.iter().map(|(_, definition)| definition))
.any(PropertyDefinition::fulltext_enabled);
let node_fulltext_indexed = aggregate.has_node_aggregates()
|| properties
.values()
.chain(patterns.iter().map(|(_, definition)| definition))
.any(|definition| definition.node_scope_index);
let indexes_all_nodes_of_matching_type = node_type_index
|| node_fulltext_indexed
|| properties
.values()
.any(|definition| definition.null_check_enabled && !definition.relative)
|| properties.contains_key(&fold_case("jcr:primaryType"));
let node_name_indexed = optional_boolean(node, "indexNodeName", false)?
|| properties
.values()
.any(|definition| definition.name == NODE_NAME_PROPERTY);
let rule = Self {
node_type_name: node_type_name.to_owned(),
inherited,
registered_types: registered_types(content_root, node_type_name, inherited)?,
boost: optional_float(node, "boost", DEFAULT_BOOST)?,
include_property_types: converting_strings(
node.property("includePropertyTypes")?.as_ref(),
),
node_type_index,
node_name_indexed,
properties,
patterns,
aggregate,
fulltext_enabled,
node_fulltext_indexed,
indexes_all_nodes_of_matching_type,
};
rule.validate(definition_path)?;
Ok(rule)
}
fn validate(&self, definition_path: &str) -> IndexResult<()> {
if self.node_type_name == "nt:base"
&& self
.definitions()
.any(|definition| definition.null_check_enabled)
{
return Err(IndexError::UnsupportedDefinition {
definition_path: definition_path.to_owned(),
feature: "an nt:base rule carries a nullCheckEnabled property definition, which \
Oak's own rule validation refuses"
.to_owned(),
});
}
Ok(())
}
}
fn registered_types(
content_root: &NodeState<'_>,
node_type_name: &str,
inherited: bool,
) -> IndexResult<Vec<String>> {
let mut names = vec![node_type_name.to_owned()];
if !inherited {
return Ok(names);
}
let node_type = match content_root.child_node("jcr:system")? {
None => None,
Some(system) => match system.child_node("jcr:nodeTypes")? {
None => None,
Some(types) => types.child_node(node_type_name)?,
},
};
if let Some(node_type) = node_type {
for property in ["rep:primarySubtypes", "rep:mixinSubtypes"] {
names.extend(strict_names(node_type.property(property)?.as_ref()).unwrap_or_default());
}
}
names.sort_unstable();
names.dedup();
Ok(names)
}
impl IndexingRules {
pub fn read(
definition: &NodeState<'_>,
definition_path: &str,
content_root: &NodeState<'_>,
warnings: &mut Vec<IndexWarning>,
) -> IndexResult<Self> {
let refuse = |feature: String| IndexError::UnsupportedDefinition {
definition_path: definition_path.to_owned(),
feature,
};
let compat_version = optional_long(definition, "compatVersion", 2)?;
if compat_version < 2 {
return Err(refuse(format!(
"compatVersion {compat_version} names its analyzed fields without the `full:` \
prefix, which is a second format rather than a variation on this one"
)));
}
let Some(rule_node) = definition.child_node("indexRules")? else {
return Err(refuse(
"it carries no indexRules, so Oak synthesizes version-1 rules from the flat \
includePropertyNames"
.to_owned(),
));
};
let aggregates = aggregate::read_aggregates(definition)?;
let mut rules = Vec::new();
for (node_type_name, node) in children_in_tree_order(&rule_node)? {
rules.push(IndexingRule::read(
definition_path,
&node_type_name,
&node,
content_root,
&aggregates,
warnings,
)?);
}
let fulltext_enabled = rules.iter().any(|rule| rule.fulltext_enabled);
let codec = match strict_string(definition.property("codec")?.as_ref()) {
Some(OAK_CODEC_NAME) => CodecVerdict::OakCodec,
Some(name) => CodecVerdict::Named(name.to_owned()),
None if fulltext_enabled => CodecVerdict::OakCodec,
None => CodecVerdict::Lucene46,
};
if codec != CodecVerdict::OakCodec {
return Err(refuse(format!(
"its codec resolves to {}, and froe writes the oakCodec composition alone",
match &codec {
CodecVerdict::Named(name) => name.clone(),
_ => "Lucene46".to_owned(),
}
)));
}
if let Some(pattern) = strict_string(definition.property("valueRegex")?.as_ref()) {
return Err(refuse(format!(
"it restricts values with the regular expression {pattern:?}, which froe does \
not evaluate"
)));
}
let maximum_field_length = converting_long(definition.property("maxFieldLength")?.as_ref());
if maximum_field_length == Some(0) {
return Err(refuse(
"maxFieldLength is zero, which leaves every analyzed field empty in 4.7.2 rather \
than being an error Oak reports"
.to_owned(),
));
}
if let Some(analyzers) = definition.child_node("analyzers")?
&& let Some((name, _)) = analyzers.child_node_entries()?.into_iter().next()
{
return Err(refuse(format!(
"its analyzers node declares the child {name}, and froe reproduces no \
consumer-registered analyzer"
)));
}
let index_original_term = match definition.child_node("analyzers")? {
None => false,
Some(analyzers) => optional_boolean(&analyzers, "indexOriginalTerm", false)?,
};
let suggest_analyzed = match definition.child_node("suggestion")? {
Some(suggestion) if suggestion.property("suggestAnalyzed")?.is_some() => {
optional_boolean(&suggestion, "suggestAnalyzed", false)?
}
_ => optional_boolean(definition, "suggestAnalyzed", false)?,
};
let rules = Self {
rules,
aggregates,
codec,
index_original_term,
suggest_analyzed,
evaluate_path_restrictions: optional_boolean(
definition,
"evaluatePathRestrictions",
false,
)?,
maximum_field_length,
has_tika_configuration: definition.child_node("tika")?.is_some(),
};
rules.refuse_conflicting_doc_value_types(definition_path)?;
Ok(rules)
}
pub fn aggregate_of(&self, node: &NodeState<'_>) -> IndexResult<Option<&Aggregate>> {
let primary = strict_name(node.property("jcr:primaryType")?.as_ref()).map(str::to_owned);
let mixins = strict_names(node.property("jcr:mixinTypes")?.as_ref()).unwrap_or_default();
for name in primary.into_iter().chain(mixins) {
if let Some(aggregate) = self
.aggregates
.iter()
.find(|aggregate| aggregate.node_type_name == name)
{
return Ok(Some(aggregate));
}
}
Ok(None)
}
pub fn applicable_rule(&self, node: &NodeState<'_>) -> IndexResult<Option<&IndexingRule>> {
let primary = strict_name(node.property("jcr:primaryType")?.as_ref()).map(str::to_owned);
let mixins = strict_names(node.property("jcr:mixinTypes")?.as_ref()).unwrap_or_default();
for name in primary.into_iter().chain(mixins) {
if let Some(rule) = self.rules.iter().find(|rule| rule.registers_under(&name)) {
return Ok(Some(rule));
}
}
Ok(None)
}
fn refuse_conflicting_doc_value_types(&self, definition_path: &str) -> IndexResult<()> {
let mut seen: BTreeMap<&str, (&str, &str)> = BTreeMap::new();
for rule in &self.rules {
for definition in rule.definitions() {
if !definition.ordered {
continue;
}
let declared = definition.declared_type.as_deref().unwrap_or("UNDEFINED");
if let Some((first_rule, first_type)) = seen.get(definition.name.as_str())
&& *first_type != declared
{
return Err(IndexError::UnsupportedDefinition {
definition_path: definition_path.to_owned(),
feature: format!(
"the ordered property {} is typed {first_type} by rule {first_rule} \
and {declared} by rule {}, which gives one doc-value field two types",
definition.name, rule.node_type_name
),
});
}
seen.insert(
definition.name.as_str(),
(rule.node_type_name.as_str(), declared),
);
}
}
Ok(())
}
}