LINE Solver (C++)
Templated C++ port of the LINE queueing solver
Loading...
Searching...
No Matches
xml.h
Go to the documentation of this file.
1/*
2 * Copyright (c) 2012-2026, QORE Lab, Imperial College London
3 * All rights reserved.
4 */
5#ifndef LINE_UTIL_XML_H
6#define LINE_UTIL_XML_H
7
8/**
9 * @file
10 * @ingroup line_util
11 * A minimal XML DOM: read for the .lqnx interchange format, write for the JMT
12 * .jsimg and .jmva model files.
13 *
14 * cpp/third_party carries doctest and nlohmann/json, both MIT, and nothing
15 * else; adding a full XML library for one input format would be a heavier
16 * dependency than the format warrants. The .lqnx grammar LINE emits and
17 * consumes uses only elements, attributes, character data and comments, with
18 * no namespaces to resolve, no DTD, no entity declarations and no processing
19 * instructions other than the leading declaration. That is what this parses.
20 *
21 * The DOM mirrors the two org.w3c.dom operations MATLAB's parseXML uses:
22 * getAttribute (absent attribute reads as the empty string) and
23 * getElementsByTagName (a DESCENDANT search, not a child search -- this
24 * distinction matters: parseXML relies on it to reach `task` elements nested
25 * inside `processor`, and separately guards against it with an explicit
26 * parent-name test when collecting `activity` under `task-activities`).
27 *
28 * REFUSES rather than guesses: an unterminated tag, a mismatched close tag or
29 * an unquoted attribute value is an InputError. A silently mis-parsed model
30 * would produce a solvable network with the wrong topology, which is worse
31 * than no answer.
32 *
33 * The write side mirrors the four org.w3c.dom operations the JMT writers use --
34 * createElement, setAttribute, appendChild and createTextNode -- and serializes
35 * with `serialize`. TEXT-ONLY ELEMENTS ARE EMITTED INLINE, with no indentation
36 * inside the tags: JMT reads `<value>` bodies with Double.parseDouble and a
37 * newline plus leading spaces around the number is not what MATLAB's xmlwrite
38 * produces either. Mixed content (text alongside child elements) does not occur
39 * in either grammar and is not represented.
40 */
41
42#include <cctype>
43#include <fstream>
44#include <memory>
45#include <sstream>
46#include <string>
47#include <vector>
48
49#include "line/util/error.h"
50
51namespace line {
52namespace xml {
53
54struct Element {
55 std::string name;
56 std::vector<std::pair<std::string, std::string>> attrs;
57 std::vector<std::unique_ptr<Element>> children;
58 const Element* parent = nullptr;
59 /** Character data of a text-only element, as `createTextNode` supplies it. */
60 std::string text;
61
62 /** Attribute value, or the empty string when absent (org.w3c.dom semantics). */
63 std::string attr(const std::string& key) const {
64 for (const auto& kv : attrs)
65 if (kv.first == key) return kv.second;
66 return std::string();
67 }
68
69 bool has_attr(const std::string& key) const {
70 for (const auto& kv : attrs)
71 if (kv.first == key) return true;
72 return false;
73 }
74
75 /** Descendant-or-self search excluding self, in document order. */
76 std::vector<const Element*> by_tag(const std::string& tag) const {
77 std::vector<const Element*> out;
78 collect(tag, out);
79 return out;
80 }
81
82 /** Direct children with the given tag, in document order. */
83 std::vector<const Element*> child_tags(const std::string& tag) const {
84 std::vector<const Element*> out;
85 for (const auto& c : children)
86 if (c->name == tag) out.push_back(c.get());
87 return out;
88 }
89
90 /**
91 * `setAttribute`: replace the value in place when the key already exists,
92 * otherwise append. Replacing in place is what keeps attribute order stable
93 * across a writer that sets `className` twice -- the JSIM writer sets the
94 * LINE section name first and overwrites it with the JMT one.
95 */
96 Element& set_attr(const std::string& key, const std::string& value) {
97 for (auto& kv : attrs)
98 if (kv.first == key) {
99 kv.second = value;
100 return *this;
101 }
102 attrs.emplace_back(key, value);
103 return *this;
104 }
105
106 /** `createElement` + `appendChild` in one step; the child is owned here. */
107 Element& add_child(const std::string& tag) {
108 std::unique_ptr<Element> c(new Element());
109 c->name = tag;
110 c->parent = this;
111 children.push_back(std::move(c));
112 return *children.back();
113 }
114
115 /** `createTextNode` + `appendChild` on an element with no child elements. */
116 Element& add_text(const std::string& value) {
117 text += value;
118 return *this;
119 }
120
121 /** The common shape `<tag>value</tag>`. */
122 Element& add_text_child(const std::string& tag, const std::string& value) {
123 Element& c = add_child(tag);
124 c.add_text(value);
125 return c;
126 }
127
128private:
129 void collect(const std::string& tag, std::vector<const Element*>& out) const {
130 for (const auto& c : children) {
131 if (c->name == tag) out.push_back(c.get());
132 c->collect(tag, out);
133 }
134 }
135};
136
137namespace detail {
138
139inline bool is_space(char c) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }
140
141inline void skip_space(const std::string& s, std::size_t& i) {
142 while (i < s.size() && is_space(s[i])) ++i;
143}
144
145/** Expand the five predefined XML entities; anything else is left verbatim. */
146inline std::string unescape(const std::string& s) {
147 std::string out;
148 out.reserve(s.size());
149 for (std::size_t i = 0; i < s.size(); ++i) {
150 if (s[i] != '&') {
151 out.push_back(s[i]);
152 continue;
153 }
154 const std::size_t semi = s.find(';', i);
155 if (semi == std::string::npos) {
156 out.push_back(s[i]);
157 continue;
158 }
159 const std::string ent = s.substr(i + 1, semi - i - 1);
160 if (ent == "amp") out.push_back('&');
161 else if (ent == "lt") out.push_back('<');
162 else if (ent == "gt") out.push_back('>');
163 else if (ent == "quot") out.push_back('"');
164 else if (ent == "apos") out.push_back('\'');
165 else {
166 out.push_back('&');
167 continue;
168 }
169 i = semi;
170 }
171 return out;
172}
173
174inline std::string read_name(const std::string& s, std::size_t& i) {
175 const std::size_t start = i;
176 while (i < s.size() && !is_space(s[i]) && s[i] != '>' && s[i] != '/' && s[i] != '=') ++i;
177 if (i == start) throw InputError("xml: expected a name");
178 return s.substr(start, i - start);
179}
180
181} // namespace detail
182
183/** Parse a whole XML document and return its root element. */
184inline std::unique_ptr<Element> parse(const std::string& text) {
185 std::size_t i = 0;
186 std::unique_ptr<Element> root;
187 std::vector<Element*> stack;
188
189 while (i < text.size()) {
190 if (text[i] != '<') {
191 // Character data. WHITESPACE-ONLY RUNS ARE DROPPED: they are the
192 // pretty-printer's indentation, and keeping them would give every
193 // container element a text body and turn a re-serialization into
194 // mixed content. The .lqnx reader looks at no text at all; the JMT
195 // result readers need the bodies of the leaf elements, which is what
196 // survives the trim.
197 const std::size_t start = i;
198 while (i < text.size() && text[i] != '<') ++i;
199 const std::string raw = text.substr(start, i - start);
200 std::size_t b = 0, e = raw.size();
201 while (b < e && detail::is_space(raw[b])) ++b;
202 while (e > b && detail::is_space(raw[e - 1])) --e;
203 if (e > b && !stack.empty()) stack.back()->text += detail::unescape(raw.substr(b, e - b));
204 continue;
205 }
206 if (text.compare(i, 4, "<!--") == 0) {
207 const std::size_t end = text.find("-->", i + 4);
208 if (end == std::string::npos) throw InputError("xml: unterminated comment");
209 i = end + 3;
210 continue;
211 }
212 if (text.compare(i, 9, "<![CDATA[") == 0) {
213 const std::size_t end = text.find("]]>", i + 9);
214 if (end == std::string::npos) throw InputError("xml: unterminated CDATA section");
215 i = end + 3;
216 continue;
217 }
218 if (text.compare(i, 2, "<?") == 0) {
219 const std::size_t end = text.find("?>", i + 2);
220 if (end == std::string::npos) throw InputError("xml: unterminated processing instruction");
221 i = end + 2;
222 continue;
223 }
224 if (text.compare(i, 2, "<!") == 0) {
225 const std::size_t end = text.find('>', i + 2);
226 if (end == std::string::npos) throw InputError("xml: unterminated declaration");
227 i = end + 1;
228 continue;
229 }
230 if (text.compare(i, 2, "</") == 0) {
231 i += 2;
232 detail::skip_space(text, i);
233 const std::string name = detail::read_name(text, i);
234 detail::skip_space(text, i);
235 if (i >= text.size() || text[i] != '>') throw InputError("xml: malformed close tag");
236 ++i;
237 if (stack.empty() || stack.back()->name != name)
238 throw InputError("xml: close tag </" + name + "> does not match the open element");
239 stack.pop_back();
240 continue;
241 }
242
243 // open or self-closing element
244 ++i;
245 detail::skip_space(text, i);
246 std::unique_ptr<Element> owned(new Element());
247 Element* el = owned.get();
248 el->name = detail::read_name(text, i);
249
250 while (true) {
251 detail::skip_space(text, i);
252 if (i >= text.size()) throw InputError("xml: unterminated element <" + el->name + ">");
253 if (text[i] == '>' || (text[i] == '/' && i + 1 < text.size() && text[i + 1] == '>')) break;
254 const std::string key = detail::read_name(text, i);
255 detail::skip_space(text, i);
256 if (i >= text.size() || text[i] != '=')
257 throw InputError("xml: attribute '" + key + "' has no value");
258 ++i;
259 detail::skip_space(text, i);
260 if (i >= text.size() || (text[i] != '"' && text[i] != '\''))
261 throw InputError("xml: attribute '" + key + "' value is not quoted");
262 const char quote = text[i++];
263 const std::size_t vstart = i;
264 while (i < text.size() && text[i] != quote) ++i;
265 if (i >= text.size()) throw InputError("xml: unterminated attribute value");
266 el->attrs.emplace_back(key, detail::unescape(text.substr(vstart, i - vstart)));
267 ++i;
268 }
269
270 const bool self_closing = text[i] == '/';
271 i += self_closing ? 2 : 1;
272
273 el->parent = stack.empty() ? nullptr : stack.back();
274 Element* raw = el;
275 if (stack.empty()) {
276 if (root) throw InputError("xml: more than one root element");
277 root = std::move(owned);
278 } else {
279 stack.back()->children.push_back(std::move(owned));
280 }
281 if (!self_closing) stack.push_back(raw);
282 }
283
284 if (!stack.empty()) throw InputError("xml: unterminated element <" + stack.back()->name + ">");
285 if (!root) throw InputError("xml: the document has no root element");
286 return root;
287}
288
289/** Read and parse a file. */
290inline std::unique_ptr<Element> parse_file(const std::string& path) {
291 std::ifstream f(path, std::ios::binary);
292 if (!f) throw InputError("xml: cannot open " + path);
293 std::ostringstream ss;
294 ss << f.rdbuf();
295 return parse(ss.str());
296}
297
298/** A fresh detached element, the `createElement` of the write side. */
299inline std::unique_ptr<Element> element(const std::string& tag) {
300 std::unique_ptr<Element> e(new Element());
301 e->name = tag;
302 return e;
303}
304
305namespace detail {
306
307/**
308 * Escape for character data and for attribute values alike.
309 *
310 * `>` is escaped although only `]]>` requires it, and `"`/`'` although only the
311 * matching quote requires it: one escaper for both positions cannot be applied
312 * in the wrong place, and every XML reader expands all five entities.
313 */
314inline std::string escape(const std::string& s) {
315 std::string out;
316 out.reserve(s.size());
317 for (std::size_t i = 0; i < s.size(); ++i) {
318 switch (s[i]) {
319 case '&': out += "&amp;"; break;
320 case '<': out += "&lt;"; break;
321 case '>': out += "&gt;"; break;
322 case '"': out += "&quot;"; break;
323 case '\'': out += "&apos;"; break;
324 default: out.push_back(s[i]);
325 }
326 }
327 return out;
328}
329
330inline void serialize_element(const Element& e, int depth, std::string& out) {
331 const std::string pad(static_cast<std::size_t>(depth) * 2, ' ');
332 out += pad;
333 out += "<";
334 out += e.name;
335 for (std::size_t i = 0; i < e.attrs.size(); ++i) {
336 out += " ";
337 out += e.attrs[i].first;
338 out += "=\"";
339 out += escape(e.attrs[i].second);
340 out += "\"";
341 }
342 if (e.children.empty() && e.text.empty()) {
343 out += "/>\n";
344 return;
345 }
346 out += ">";
347 if (e.children.empty()) {
348 out += escape(e.text);
349 out += "</";
350 out += e.name;
351 out += ">\n";
352 return;
353 }
354 out += "\n";
355 for (std::size_t i = 0; i < e.children.size(); ++i)
356 serialize_element(*e.children[i], depth + 1, out);
357 out += pad;
358 out += "</";
359 out += e.name;
360 out += ">\n";
361}
362
363} // namespace detail
364
365/**
366 * Serialize a document: the XML declaration MATLAB's xmlwrite emits, then the
367 * root subtree.
368 */
369inline std::string serialize(const Element& root) {
370 std::string out = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
371 detail::serialize_element(root, 0, out);
372 return out;
373}
374
375/** Serialize to a file, creating or truncating it. */
376inline void write_file(const std::string& path, const Element& root) {
377 std::ofstream f(path, std::ios::binary);
378 if (!f) throw InputError("xml: cannot write " + path);
379 const std::string text = serialize(root);
380 f.write(text.data(), static_cast<std::streamsize>(text.size()));
381 if (!f) throw InputError("xml: write failed on " + path);
382}
383
384} // namespace xml
385} // namespace line
386
387#endif // LINE_UTIL_XML_H
InputError(const std::string &what)
Definition error.h:39
The exception types the port throws.
void write_file(const std::string &path, const Element &root)
Serialize to a file, creating or truncating it.
Definition xml.h:376
std::unique_ptr< Element > parse(const std::string &text)
Parse a whole XML document and return its root element.
Definition xml.h:184
std::unique_ptr< Element > parse_file(const std::string &path)
Read and parse a file.
Definition xml.h:290
std::unique_ptr< Element > element(const std::string &tag)
A fresh detached element, the createElement of the write side.
Definition xml.h:299
std::string serialize(const Element &root)
Serialize a document: the XML declaration MATLAB's xmlwrite emits, then the root subtree.
Definition xml.h:369
std::string text
Character data of a text-only element, as createTextNode supplies it.
Definition xml.h:60
Element & add_text(const std::string &value)
createTextNode + appendChild on an element with no child elements.
Definition xml.h:116
std::vector< std::pair< std::string, std::string > > attrs
Definition xml.h:56
std::string name
Definition xml.h:55
std::vector< std::unique_ptr< Element > > children
Definition xml.h:57
Element & add_child(const std::string &tag)
createElement + appendChild in one step; the child is owned here.
Definition xml.h:107
Element & set_attr(const std::string &key, const std::string &value)
setAttribute: replace the value in place when the key already exists, otherwise append.
Definition xml.h:96
bool has_attr(const std::string &key) const
Definition xml.h:69
const Element * parent
Definition xml.h:58
Element & add_text_child(const std::string &tag, const std::string &value)
The common shape <tag>value</tag>.
Definition xml.h:122
std::vector< const Element * > by_tag(const std::string &tag) const
Descendant-or-self search excluding self, in document order.
Definition xml.h:76
std::string attr(const std::string &key) const
Attribute value, or the empty string when absent (org.w3c.dom semantics).
Definition xml.h:63
std::vector< const Element * > child_tags(const std::string &tag) const
Direct children with the given tag, in document order.
Definition xml.h:83