1use 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}