Skip to main content

document_svg/document/
ocel_xml.rs

1//! Bounded OCEL 2.0 XML event-log previews.
2//!
3//! OCEL XML contains object-centric events, object types and relationships.
4//! This adapter renders log structure only; event/object identifiers,
5//! timestamps, attributes and process-mining operations remain inert.
6
7use std::path::Path;
8
9use crate::convert::{ConvertOptions, PageConsumer, read_limited_file};
10use crate::document::html::{HtmlBlock, render_blocks_to_pages};
11use crate::error::{Error, Result};
12use crate::geospatial::xml_tree::{XmlElement, XmlLimits, parse_xml_tree};
13use crate::table::{TableAlign, TableData};
14
15const MAX_OCEL_XML_BYTES: u64 = 128 * 1024 * 1024;
16const MAX_OCEL_XML_EVENTS: usize = 1_000_000;
17const MAX_OCEL_XML_NODES: usize = 500_000;
18const MAX_OCEL_XML_DEPTH: usize = 128;
19const MAX_OCEL_XML_TEXT_BYTES: usize = 32 * 1024 * 1024;
20const MAX_OCEL_XML_ROWS: usize = 200_000;
21const MAX_OCEL_XML_DISPLAY_BYTES: usize = 512;
22
23#[derive(Default)]
24struct Summary {
25    logs: usize,
26    event_types: usize,
27    events: usize,
28    object_types: usize,
29    objects: usize,
30    attributes: usize,
31    relationships: usize,
32    value_types: usize,
33    rows: Vec<Vec<String>>,
34}
35
36struct OcelXmlPageSink<'a> {
37    inner: &'a mut dyn PageConsumer,
38    warnings: &'a [String],
39}
40
41impl PageConsumer for OcelXmlPageSink<'_> {
42    fn consume(&mut self, mut page: crate::ir::Page) -> Result<()> {
43        page.source_format = "ocel-xml".into();
44        if page.title.is_empty() {
45            page.title = "OCEL XML event log".into();
46        }
47        page.description =
48            "OCEL XML event/object structure is rendered as bounded inert metadata; identifiers, values and process operations are not exposed".into();
49        for warning in self.warnings {
50            page.warn(warning.clone());
51        }
52        self.inner.consume(page)
53    }
54}
55
56pub(crate) fn looks_like_prefix(prefix: &[u8]) -> bool {
57    crate::geospatial::xml_tree::looks_like_root(prefix, b"log", None)
58        && String::from_utf8_lossy(prefix)
59            .to_ascii_lowercase()
60            .contains("event-types")
61}
62
63pub(crate) fn convert(
64    path: &Path,
65    options: &ConvertOptions,
66    sink: &mut dyn PageConsumer,
67) -> Result<Vec<String>> {
68    let bytes = read_limited_file(
69        path,
70        options.max_input_bytes.min(MAX_OCEL_XML_BYTES),
71        "OCEL XML input",
72    )?;
73    let root = parse_xml_tree(
74        &bytes,
75        &XmlLimits {
76            max_events: options.max_xml_events.min(MAX_OCEL_XML_EVENTS),
77            max_nodes: MAX_OCEL_XML_NODES,
78            max_depth: MAX_OCEL_XML_DEPTH,
79            max_text_bytes: MAX_OCEL_XML_TEXT_BYTES,
80        },
81        "OCEL XML",
82    )?;
83    if !root.name.eq_ignore_ascii_case("log") {
84        return Err(Error::InvalidInput("OCEL XML root must be <log>".into()));
85    }
86    let mut summary = Summary {
87        logs: 1,
88        event_types: count_named(&root, "event-type"),
89        events: count_named(&root, "event"),
90        object_types: count_named(&root, "object-type"),
91        objects: count_named(&root, "object"),
92        attributes: count_named(&root, "attribute"),
93        relationships: count_named(&root, "relationship"),
94        value_types: count_named(&root, "string")
95            + count_named(&root, "integer")
96            + count_named(&root, "float")
97            + count_named(&root, "time")
98            + count_named(&root, "boolean"),
99        ..Summary::default()
100    };
101    push_row(
102        &mut summary.rows,
103        "Log",
104        &summary.logs.to_string(),
105        &format!(
106            "eventTypes={} objectTypes={}",
107            summary.event_types, summary.object_types
108        ),
109    )?;
110    push_row(
111        &mut summary.rows,
112        "Events",
113        &format!("events={}", summary.events),
114        &format!(
115            "attributes={} relationships={}",
116            summary.attributes, summary.relationships
117        ),
118    )?;
119    push_row(
120        &mut summary.rows,
121        "Objects",
122        &format!("objects={}", summary.objects),
123        &format!("attributeTypes={}", summary.value_types),
124    )?;
125    let blocks = vec![
126        HtmlBlock::Heading {
127            level: 1,
128            text: "OCEL XML event log".into(),
129        },
130        HtmlBlock::Paragraph {
131            text: "Object-Centric Event Log 2.0 XML structure is summarized without exposing identifiers, timestamps or attribute values.".into(),
132        },
133        HtmlBlock::Table(TableData {
134            headers: vec!["Kind".into(), "Value".into(), "Detail".into()],
135            rows: summary.rows,
136            alignments: vec![TableAlign::Left; 3],
137            raw_source: String::new(),
138        }),
139    ];
140    let warnings = vec![
141        "OCEL event/object IDs, types, timestamps, attribute names/values, relationships and private log data are omitted or redacted".into(),
142        "OCEL XML schema locations, external resources, process-mining discovery, filtering and event-log operations never run".into(),
143    ];
144    let mut page_sink = OcelXmlPageSink {
145        inner: sink,
146        warnings: &warnings,
147    };
148    render_blocks_to_pages(&blocks, &mut page_sink, options)?;
149    Ok(warnings)
150}
151
152fn count_named(element: &XmlElement, name: &str) -> usize {
153    element
154        .children
155        .iter()
156        .map(|child| usize::from(child.name.eq_ignore_ascii_case(name)) + count_named(child, name))
157        .sum()
158}
159
160fn push_row(rows: &mut Vec<Vec<String>>, kind: &str, value: &str, detail: &str) -> Result<()> {
161    if rows.len() >= MAX_OCEL_XML_ROWS {
162        return Err(Error::LimitExceeded(format!(
163            "OCEL XML rows exceed {MAX_OCEL_XML_ROWS}"
164        )));
165    }
166    rows.push(vec![truncate(kind), truncate(value), truncate(detail)]);
167    Ok(())
168}
169
170fn truncate(value: &str) -> String {
171    if value.len() <= MAX_OCEL_XML_DISPLAY_BYTES {
172        value.to_owned()
173    } else {
174        let mut end = MAX_OCEL_XML_DISPLAY_BYTES;
175        while end > 0 && !value.is_char_boundary(end) {
176            end -= 1;
177        }
178        format!("{}…", &value[..end])
179    }
180}