use super::{
DocumentMaker, DocumentState, FULLTEXT_FIELD, IndexResult, IndexingRule, Match, Matcher,
NodeState, PropertyInclude, PropertyState, PropertyType, RELATIVE_NODE_PREFIX, values_of,
};
impl DocumentMaker<'_> {
pub(super) fn index_aggregates(
&self,
node: &NodeState<'_>,
rule: &IndexingRule,
state: &mut DocumentState,
) -> IndexResult<()> {
let property_includes = rule.property_includes();
if !rule.aggregate.has_node_aggregates() && property_includes.is_empty() {
return Ok(());
}
let walk = AggregateWalk {
rule,
root_has_mime_type: node.property("jcr:mimeType")?.is_some(),
property_includes,
};
let level = AggregateLevel {
nodes: rule.aggregate.matcher(),
properties: PropertyIncludeMatcher::new(&walk.property_includes),
};
self.walk_aggregate(&walk, &level, node, "", state)
}
fn walk_aggregate(
&self,
walk: &AggregateWalk<'_>,
level: &AggregateLevel<'_>,
node: &NodeState<'_>,
matched: &str,
state: &mut DocumentState,
) -> IndexResult<()> {
for (name, child) in node.child_node_entries()? {
let (next_properties, ended_properties) = level.properties.step(&name);
let (next_nodes, outcome) = level.nodes.step(&name, &child)?;
for include in ended_properties {
self.index_property_include(&child, walk, include, state)?;
}
let child_path = if matched.is_empty() {
name.clone()
} else {
format!("{matched}/{name}")
};
match outcome {
Match::Stop if next_properties.is_exhausted() => continue,
Match::Stop | Match::Continue => {}
Match::Aggregate(includes) => {
for include in includes {
let relative = include.relative_node.then(|| {
format!("{RELATIVE_NODE_PREFIX}{}", include.elements.join("/"))
});
let names: Vec<&str> = std::iter::once(FULLTEXT_FIELD)
.chain(relative.as_deref())
.collect();
self.aggregate_node(&child, walk.writing_into(&names, &child_path), state)?;
}
}
}
let next = AggregateLevel {
nodes: next_nodes,
properties: next_properties,
};
self.walk_aggregate(walk, &next, &child, &child_path, state)?;
}
Ok(())
}
fn index_property_include(
&self,
node: &NodeState<'_>,
walk: &AggregateWalk<'_>,
include: PropertyInclude<'_>,
state: &mut DocumentState,
) -> IndexResult<()> {
let definition = include.definition;
let parent = definition.ancestors.join("/");
for mut property in included_properties(node, include)? {
property.name = format!("{parent}/{}", property.name);
if definition.ordered {
self.index_ordered(&property, definition, state)?;
}
self.index_property(
&property,
definition,
walk.rule,
walk.root_has_mime_type,
state,
)?;
}
Ok(())
}
fn aggregate_node(
&self,
node: &NodeState<'_>,
into: AggregatedInto<'_>,
state: &mut DocumentState,
) -> IndexResult<()> {
let names = into.names;
let covering = self.rules.applicable_rule(node)?;
let included_types = covering.map_or(&into.document_rule.include_property_types, |rule| {
&rule.include_property_types
});
let has_mime_type = node.property("jcr:mimeType")?.is_some();
for property in node.properties()? {
if property.name.starts_with(':') {
continue;
}
if !super::includes_property_type(included_types, &property) {
continue;
}
let relative_path = format!("{}/{}", into.matched, property.name);
let from_document = into.document_rule.config_of(&relative_path);
if from_document
.is_some_and(|definition| !definition.index || definition.exclude_from_aggregation)
{
continue;
}
if property.property_type == PropertyType::Binary {
self.index_binary(&property, has_mime_type, names, state);
continue;
}
let definition = covering.and_then(|rule| rule.config_of(&property.name));
if definition.is_some_and(|definition| !definition.node_scope_index) {
continue;
}
for value in values_of(&property) {
let Some(text) = value.as_text() else {
continue;
};
for name in names {
let position = state.fields.len();
self.index_fulltext(name, &text, None, state);
if let Some(definition) = definition
&& let Some(field) = state.fields.get_mut(position)
{
field.boost = definition.boost;
}
}
}
}
self.reaggregate(node, into, state)
}
fn reaggregate(
&self,
node: &NodeState<'_>,
into: AggregatedInto<'_>,
state: &mut DocumentState,
) -> IndexResult<()> {
let Some(aggregate) = self.rules.aggregate_of(node)? else {
return Ok(());
};
if !aggregate.has_node_aggregates() || into.depth >= into.limit {
return Ok(());
}
self.walk_reaggregate(node, &aggregate.matcher(), into, state)
}
fn walk_reaggregate(
&self,
node: &NodeState<'_>,
matcher: &Matcher<'_>,
into: AggregatedInto<'_>,
state: &mut DocumentState,
) -> IndexResult<()> {
for (name, child) in node.child_node_entries()? {
let (next, outcome) = matcher.step(&name, &child)?;
let child_path = format!("{}/{name}", into.matched);
match outcome {
Match::Stop => continue,
Match::Continue => {}
Match::Aggregate(includes) => {
for _ended in includes {
self.aggregate_node(&child, into.deeper(&child_path), state)?;
}
}
}
self.walk_reaggregate(&child, &next, into.at(&child_path), state)?;
}
Ok(())
}
}
#[derive(Clone, Copy)]
struct AggregatedInto<'walk> {
names: &'walk [&'walk str],
document_rule: &'walk IndexingRule,
matched: &'walk str,
limit: usize,
depth: usize,
}
impl<'walk> AggregatedInto<'walk> {
fn deeper(self, matched: &'walk str) -> Self {
Self {
matched,
depth: self.depth + 1,
..self
}
}
const fn at(self, matched: &'walk str) -> Self {
Self { matched, ..self }
}
}
struct AggregateWalk<'rule> {
rule: &'rule IndexingRule,
root_has_mime_type: bool,
property_includes: Vec<PropertyInclude<'rule>>,
}
impl AggregateWalk<'_> {
fn writing_into<'names>(
&'names self,
names: &'names [&'names str],
matched: &'names str,
) -> AggregatedInto<'names> {
AggregatedInto {
names,
document_rule: self.rule,
matched,
limit: usize::try_from(self.rule.aggregate.reaggregation_limit).unwrap_or(0),
depth: 0,
}
}
}
struct AggregateLevel<'rule> {
nodes: Matcher<'rule>,
properties: PropertyIncludeMatcher<'rule>,
}
struct PropertyIncludeMatcher<'rule> {
includes: &'rule [PropertyInclude<'rule>],
live: Vec<(usize, usize)>,
}
impl<'rule> PropertyIncludeMatcher<'rule> {
fn new(includes: &'rule [PropertyInclude<'rule>]) -> Self {
let live = includes
.iter()
.enumerate()
.filter(|(_, include)| !include.definition.ancestors.is_empty())
.map(|(at, _)| (at, 0usize))
.collect();
Self { includes, live }
}
fn step(&self, name: &str) -> (Self, Vec<PropertyInclude<'rule>>) {
let mut live = Vec::new();
let mut ended = Vec::new();
for (at, depth) in &self.live {
let ancestors = &self.includes[*at].definition.ancestors;
let Some(element) = ancestors.get(*depth) else {
continue;
};
if element != MATCH_ALL_STEP && element != name {
continue;
}
if depth + 1 == ancestors.len() {
ended.push(self.includes[*at]);
} else {
live.push((*at, depth + 1));
}
}
(
Self {
includes: self.includes,
live,
},
ended,
)
}
const fn is_exhausted(&self) -> bool {
self.live.is_empty()
}
}
const MATCH_ALL_STEP: &str = "*";
fn included_properties(
node: &NodeState<'_>,
include: PropertyInclude<'_>,
) -> IndexResult<Vec<PropertyState>> {
let definition = include.definition;
let name = definition
.name
.rsplit('/')
.next()
.unwrap_or(&definition.name);
let Some(pattern) = include.pattern else {
if name.starts_with(':') {
return Ok(Vec::new());
}
return Ok(node.property(name)?.into_iter().collect());
};
let parent = definition.ancestors.join("/");
Ok(node
.properties()?
.into_iter()
.filter(|property| !property.name.starts_with(':'))
.filter(|property| pattern.matches(&format!("{parent}/{}", property.name)))
.collect())
}