otsdaq-mu2e-stm  5.02.01
config.hh
1 #ifndef CONFIG_HH_
2 #define CONFIG_HH_
3 
4 #include <iostream>
5 #include <string>
6 #include <vector>
7 #include <stdexcept>
8 #include <sstream>
9 #include <fstream>
10 #include <regex>
11 #include <iomanip>
12 #include <unordered_map> // map full keys -> (file, local key) for include-aware setValue()
13 #include <utility> // std::pair
14 #include <filesystem> // path handling for include files relative to main XML
15 #include <functional>
16 #include "pugixml.hpp"
17 
18 // Configuation class
19 class Config {
20 
21 public:
22  // Access the singleton instance
23  static Config& getInstance(const std::string& xmlFile = "") {
24  static Config instance(xmlFile); // Lazy initialization
25  return instance;
26  }
27 
28  std::string getXMLpath() const {
29  return xml_path;
30  }
31 
32  // Re-read the XML file and reset all internal state
33  void reinit() {
34  rebuildExpandedDocument();
35  }
36 
37  // Access configuration values
38  // Template method definition must be in the header
39  template <typename T>
40  T getValue(const std::string& key) const {
41  pugi::xml_node node = resolveKeyPath(key);
42  if (!node) {
43  std::string parentKey = key.substr(0, key.find_last_of('.'));
44  std::string finalKey = key.substr(key.find_last_of('.') + 1);
45  pugi::xml_node parent = resolveKeyPath(parentKey);
46  if (parent) {
47  node = parent.child(finalKey.c_str());
48  }
49  }
50  if (!node) {
51  throw std::runtime_error("Key not found: " + key);
52  }
53  std::istringstream iss(node.text().get());
54  T value;
55  iss >> value;
56  if (iss.fail()) {
57  throw std::runtime_error("Invalid value or type for key: " + key);
58  }
59  return value;
60  }
61 
62  // Set value using a text-patching approach that preserves comments and formatting
63  template <typename T>
64  void setValue(const std::string& key, const T& value) {
65  std::ostringstream oss;
66  oss << value;
67  std::string strValue = oss.str();
68 
69  // Determine which physical XML file should be patched (main or an included fragment)
70  auto fileAndLocal = getFileAndLocalKeyForFullKey(key); // figure out target file and key within that file
71 
72  std::cerr << "Patching file: " << fileAndLocal.first << " with local key: " << fileAndLocal.second << "\n";
73 
74  // Load the target file (main or included fragment)
75  pugi::xml_document targetDoc;
76  if (!targetDoc.load_file(fileAndLocal.first.c_str()))
77  throw std::runtime_error("Failed to load: " + fileAndLocal.first);
78 
79  // Walk the local key path to find the exact node
80  pugi::xml_node targetNode = targetDoc.document_element();
81  std::istringstream ss(fileAndLocal.second);
82  std::string token;
83  while (std::getline(ss, token, '.')) {
84  if (token == targetNode.name()) continue; // skip root element name
85  targetNode = targetNode.child(token.c_str());
86  if (!targetNode)
87  throw std::runtime_error("Node not found in file: " + token);
88  }
89 
90  // Set the value on the exact node — no regex, no ambiguity
91  targetNode.text().set(strValue.c_str());
92 
93  // Save back to the correct file
94  if (!targetDoc.save_file(fileAndLocal.first.c_str()))
95  throw std::runtime_error("Failed to save: " + fileAndLocal.first);
96 
97  // Rebuild in-memory DOM to reflect the change
98  rebuildExpandedDocument();
99  }
100 
101  // Print all key paths and values
102  void printAllKeys() const {
103  printKeysRecursive(doc.document_element(), "");
104  }
105 
106  // Extract key-value pairs under a specific subtree
107  std::vector<std::pair<std::string, std::string>> extractKeysAndValues(const std::string& key) const {
108  std::vector<std::pair<std::string, std::string>> result;
109  pugi::xml_node node = resolveKeyPath(key);
110  if (!node) throw std::runtime_error("Key not found: " + key);
111 
112  for (pugi::xml_node child : node.children()) {
113  std::string name = child.name();
114  if (name.empty()) continue; // Skip text nodes
115  result.emplace_back(name, child.text().as_string());
116  }
117  return result;
118  }
119 
120  // Destructor
121  ~Config() {}
122 
123 private:
124 
125  // Private constructor
126  Config(const std::string& xmlFile) : xml_path(xmlFile) {
127  pugi::xml_parse_result result = doc.load_file(xml_path.c_str());
128  if (!result) throw std::runtime_error("Failed to load XML file: " + std::string(result.description()));
129 
130  // Build the expanded DOM by resolving <include file="..."/> nodes before any lookups happen
131  rebuildExpandedDocument(); // expand includes and build key->file mapping
132  }
133 
134  // Get the key path
135  pugi::xml_node resolveKeyPath(const std::string& key) const {
136  std::string adjustedKey = key;
137  // If key starts with root tag name (e.g., "stm."), remove it
138  std::string rootName = doc.document_element().name();
139  if (adjustedKey.find(rootName + ".") == 0) {
140  adjustedKey = adjustedKey.substr(rootName.length() + 1);
141  }
142  std::istringstream ss(adjustedKey);
143  std::string token;
144  pugi::xml_node node = doc.document_element();
145  while (std::getline(ss, token, '.')) {
146  node = node.child(token.c_str());
147  if (!node) break;
148  }
149  return node;
150  }
151 
152  // Helper function for recursive printing
153  void printKeysRecursive(pugi::xml_node node, const std::string& prefix) const {
154  for (pugi::xml_node child : node.children()) {
155  std::string fullKey = prefix.empty() ? child.name() : prefix + "." + child.name();
156  std::string value = child.text().as_string();
157  std::cout << fullKey << ": " << value << std::endl;
158  printKeysRecursive(child, fullKey);
159  }
160  }
161 
162  // Expand <include file="..."/> directives and rebuild the key->file mapping
163  void rebuildExpandedDocument() {
164  include_key_map.clear(); // clear old mapping (files may have changed after setValue)
165  base_dir = std::filesystem::path(xml_path).parent_path(); // remember base directory for relative include paths
166 
167  // Start by re-loading the main XML file into doc (so we always expand from the on-disk source)
168  pugi::xml_parse_result result = doc.load_file(xml_path.c_str()); // load main file again
169  if (!result) throw std::runtime_error("Failed to load XML file: " + std::string(result.description())); // maintain strong error reporting
170 
171  // Expand includes recursively into the in-memory document
172  expandIncludesRecursive(doc.document_element());
173 
174  // Build the mapping for main-file-owned nodes
175  registerKeysForSubtree(doc.document_element(), doc.document_element().name(), doc.document_element().name(), xml_path, false); // do NOT overwrite include-owned mappings
176  // registerKeysForSubtree(doc.document_element(), doc.document_element().name(), doc.document_element().name(), xml_path); // main doc keys map to main file
177  }
178 
179  // Recursively expand <include file="..."/> nodes by splicing the included file's root element into the DOM
180  void expandIncludesRecursive(pugi::xml_node node) {
181  for (pugi::xml_node child = node.first_child(); child; ) { // iterate manually because we may erase/insert while iterating
182  pugi::xml_node next = child.next_sibling(); // stash next sibling before modifying the tree
183 
184  // Recurse into normal elements first
185  if (child.type() == pugi::node_element && std::string(child.name()) != "include") { // only recurse into non-include elements
186  expandIncludesRecursive(child); // expand nested includes deeper in the tree
187  }
188 
189  // If this is an <include file="..."/> element, splice in the referenced XML
190  if (child.type() == pugi::node_element && std::string(child.name()) == "include") { // detect include directive
191  const char* fileAttr = child.attribute("file").value(); // read include filename
192  if (!fileAttr || std::string(fileAttr).empty()) { // validate attribute exists
193  throw std::runtime_error("Include tag missing 'file' attribute.");
194  }
195 
196  std::filesystem::path includePath = base_dir / std::filesystem::path(fileAttr); // resolve include file relative to main XML directory
197 
198  pugi::xml_document subdoc; // temporary document for included file
199  pugi::xml_parse_result subres = subdoc.load_file(includePath.string().c_str()); // parse included XML file
200  if (!subres) { // error handling for include parse failures
201  throw std::runtime_error("Failed to load included XML file: " + includePath.string() + " (" + std::string(subres.description()) + ")");
202  }
203 
204  pugi::xml_node subroot = subdoc.document_element(); // root element of included file
205  if (!subroot) { // sanity check
206  throw std::runtime_error("Included XML file has no document element: " + includePath.string());
207  }
208 
209  // Insert the included root node BEFORE the <include> element, preserving order in the parent
210  pugi::xml_node inserted = node.insert_copy_before(subroot, child); // splice included root into main DOM
211 
212  // Register keys under the inserted subtree so setValue knows which file to patch
213  std::string parentFull = computeNodeFullKey(node); // compute full key for the parent node in the expanded DOM
214  std::string insertedFull = parentFull.empty() ? inserted.name() : parentFull + "." + inserted.name(); // full key for inserted root under parent
215  std::string insertedLocal = inserted.name(); // local key within the included file starts at its root
216  //registerKeysForSubtree(inserted, insertedFull, insertedLocal, includePath.string()); // map inserted keys -> included file path
217  registerKeysForSubtree(inserted, insertedFull, insertedLocal, includePath.string(), true); // force include-owned mapping for inserted subtree
218 
219 
220  // Remove the <include> node itself
221  node.remove_child(child); // delete the directive from the expanded DOM
222  }
223 
224  child = next; // advance iteration safely
225  }
226  }
227 
228  // Compute the full dotted key for a node by walking up to the document element
229  std::string computeNodeFullKey(pugi::xml_node node) const {
230  if (!node) return ""; // defensive
231  std::vector<std::string> parts; // collect path segments bottom-up
232  pugi::xml_node cur = node; // iterator up the tree
233  while (cur && cur.type() == pugi::node_element) { // walk element nodes
234  parts.push_back(cur.name()); // store this element name
235  if (cur == doc.document_element()) break; // stop at the document root element
236  cur = cur.parent(); // move up
237  }
238  std::ostringstream oss; // build dotted key top-down
239  for (auto it = parts.rbegin(); it != parts.rend(); ++it) { // reverse to get root->leaf
240  if (it != parts.rbegin()) oss << "."; // dot separators
241  oss << *it; // segment
242  }
243  return oss.str(); // resulting full key
244  }
245 
246  // Register every element in a subtree as belonging to a particular source file
247  void registerKeysForSubtree(pugi::xml_node node, const std::string& fullPrefix, const std::string& localPrefix,
248  const std::string& sourceFile, const bool overwriteExisting = true) { // add overwrite flag (default keeps existing behaviour)
249  if (!node) return; // defensive
250 
251  // Store mapping for this node itself
252  if (overwriteExisting) { // allow caller to force ownership mapping
253  include_key_map[fullPrefix] = std::make_pair(sourceFile, localPrefix); // overwrite mapping (old behaviour)
254  } else { // preserve earlier mapping if it already exists (used to protect include-owned keys)
255  include_key_map.emplace(fullPrefix, std::make_pair(sourceFile, localPrefix)); // insert only if key not already present
256  }
257 
258  // Recurse through element children
259  for (pugi::xml_node child : node.children()) {
260  if (child.type() != pugi::node_element) continue; // skip text/comments/etc.
261  std::string childName = child.name(); // child tag name
262  std::string childFull = fullPrefix.empty() ? childName : fullPrefix + "." + childName; // full dotted key
263  std::string childLocal = localPrefix.empty() ? childName : localPrefix + "." + childName; // local dotted key (within its source file)
264  registerKeysForSubtree(child, childFull, childLocal, sourceFile, overwriteExisting); // propagate overwrite policy down the subtree
265  }
266  }
267 
268 
269  // Look up which file a given full key belongs to, and what the local key is inside that file
270  std::pair<std::string, std::string> getFileAndLocalKeyForFullKey(const std::string& fullKey) const {
271  // Normalize by ensuring the key is rooted at the main document element name
272  std::string rootName = doc.document_element().name();
273  std::string normalized = fullKey;
274  if (normalized.find(rootName + ".") != 0 && normalized != rootName) { // if user passes "channel" instead of "stm.channel", keep behaviour consistent
275  // If the caller provided a relative key like "channel", interpret it relative to the root
276  normalized = rootName + "." + normalized;
277  }
278 
279  // Prefer exact mapping hits
280  auto it = include_key_map.find(normalized);
281  if (it != include_key_map.end()) {
282  return it->second; // return (file, local key)
283  }
284 
285  // If not found, default to patching the main XML file using the provided key
286  // (This preserves prior behaviour for any keys that weren't in the mapping for some reason.)
287  return std::make_pair(xml_path, fullKey);
288  }
289 
290  // Delete copy constructor and assignment operator
291  Config(const Config&) = delete;
292  Config& operator=(const Config&) = delete;
293 
294  // The xml path
295  std::string xml_path;
296  // The xml document
297  pugi::xml_document doc;
298 
299  // Base directory of the main XML file for resolving relative include paths
300  std::filesystem::path base_dir;
301 
302  // Map from expanded full keys (e.g. "stm.channel") to (source file, local key in that file)
303  std::unordered_map<std::string, std::pair<std::string, std::string>> include_key_map;
304 };
305 
306 #endif
Definition: config.hh:19