MUSX Document Model
Loading...
Searching...
No Matches
FactoryBase.h
1/*
2 * Copyright (C) 2025, Robert Patterson
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining a copy
5 * of this software and associated documentation files (the "Software"), to deal
6 * in the Software without restriction, including without limitation the rights
7 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 * copies of the Software, and to permit persons to whom the Software is
9 * furnished to do so, subject to the following conditions:
10 *
11 * The above copyright notice and this permission notice shall be included in
12 * all copies or substantial portions of the Software.
13 *
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 * THE SOFTWARE.
21 */
22#pragma once
23
24#include <stdexcept>
25#include <string>
26#include <string_view>
27#include <optional>
28#include <functional>
29#include <unordered_set>
30#include <unordered_map>
31#include <map>
32#include <tuple>
33#include <sstream>
34#include <type_traits>
35#include <charconv>
36
37#include "musx/util/Logger.h"
38#include "musx/xml/XmlInterface.h"
39#include "musx/dom/BaseClasses.h"
40#include "musx/dom/Document.h"
41
42namespace musx {
43
48namespace factory {
49
50using namespace musx::xml;
51using namespace musx::dom;
52
53
57class unknown_xml_error : public std::runtime_error
58{
59public:
60 using std::runtime_error::runtime_error;
61};
62
63
73public:
81 using Resolver = std::function<void(const dom::DocumentPtr&)>;
82
92 void addResolver(Resolver resolver, const std::string_view& key)
93 {
94 assert(!key.empty());
95 if (resolvers.count(std::string(key)) > 0) {
96 return; // Skip if already registered
97 }
98
99 resolvers[std::string(key)] = std::move(resolver);
100 }
101
110 void resolveAll(const dom::DocumentPtr& document)
111 {
112 for (const auto& [key, resolver] : resolvers) {
113 resolver(document);
114 }
115
116 resolvers.clear();
117 }
118
119private:
126 std::map<std::string, Resolver> resolvers;
127};
128
129
134{
135protected:
146 template<typename DataType, typename ParserFunc>
147 static void getFieldFromXml(const XmlElementPtr& element, const std::string& nodeName, DataType& dataField, ParserFunc parserFunc, bool expected = false)
148 {
149 if (auto childElement = element->getFirstChildElement(nodeName)) {
150 dataField = parserFunc(childElement);
151 } else if (expected) {
152 std::stringstream msg;
153 msg << "Expected field <" << element->getTagName() << "><" << nodeName << "> not found.";
155 }
156 }
157
162 static XmlElementPtr getFirstChildElement(const XmlElementPtr& element, const std::string& childElementName)
163 {
164 auto childElement = element->getFirstChildElement(childElementName);
165 if (!childElement) {
166 throw std::invalid_argument("Missing <" + childElementName + "> element.");
167 }
168 return childElement;
169 }
170
172 static std::optional<std::string> getOptionalChildText(const XmlElementPtr& element, const std::string& childElementName)
173 {
174 auto childElement = element->getFirstChildElement(childElementName);
175 if (!childElement) {
176 return std::nullopt;
177 }
178 return childElement->getText();
179 }
180
182 template<typename T>
183 static std::optional<T> getOptionalChildTextAs(const XmlElementPtr& element, const std::string& childElementName, T defaultValue = {})
184 {
185 auto childElement = element->getFirstChildElement(childElementName);
186 if (!childElement) {
187 return std::nullopt;
188 }
189 return childElement->getTextAs<T>(defaultValue);
190 }
191
192public:
193 virtual ~FactoryBase() {}
194};
195
196#ifndef DOXYGEN_SHOULD_IGNORE_THIS
197
198template <typename EnumClass, typename FromClass = std::string_view>
199using XmlEnumMappingElement = std::unordered_map<FromClass, EnumClass>;
200template <typename EnumClass, typename FromClass = std::string_view>
201struct XmlEnumMapping
202{
203 static const XmlEnumMappingElement<EnumClass, FromClass> mapping;
204};
205
206#define MUSX_XML_ENUM_MAPPING(Type, ...) \
207template <> \
208const XmlEnumMappingElement<Type> XmlEnumMapping<Type>::mapping = __VA_ARGS__
209
210template <typename EnumClass, bool IgnoreUnknown, typename FromClass = std::string_view>
211class EnumMapper
212{
213 // If we ever need to, we can create a static lazy-initialize reverse mapping function here
214
215public:
216 static EnumClass xmlToEnum(const FromClass& value)
217 {
218 auto it = XmlEnumMapping<EnumClass>::mapping.find(value);
219 if (it != XmlEnumMapping<EnumClass>::mapping.end()) {
220 return it->second;
221 }
222 if constexpr (!IgnoreUnknown) {
223 std::string msg = [value]() {
224 if constexpr (std::is_arithmetic_v<FromClass>) {
225 return "Invalid enum value from xml: " + std::to_string(value);
226 }
227 else {
228 return "Invalid enum value from xml: " + std::string(value);
229 }
230 }();
231 MUSX_UNKNOWN_XML(msg);
232 }
233 return {};
234 }
235};
236
237template<typename EnumClass, typename FromClass, bool IgnoreUnknown = false>
238EnumClass toEnum(const FromClass& value)
239{
240 if constexpr (std::is_convertible_v<FromClass, std::string_view>) {
241 return EnumMapper<EnumClass, IgnoreUnknown, std::string_view>::xmlToEnum(value);
242 } else {
243 return EnumMapper<EnumClass, IgnoreUnknown, FromClass>::xmlToEnum(value);
244 }
245}
246
247template<typename EnumClass, bool IgnoreUnknown = false>
248EnumClass toEnum(const ::musx::xml::XmlElementPtr& e)
249{
250 return toEnum<EnumClass, std::string_view, IgnoreUnknown>(e->getTextTrimmed());
251}
252
253#define MUSX_XML_ELEMENT_ARRAY(Type, ...) \
254const ::musx::xml::XmlElementArray<Type>& Type::xmlMappingArray() { \
255 static const ::musx::xml::XmlElementArray<Type> instance = __VA_ARGS__; \
256 return instance; \
257} \
258static_assert(true, "") // require semicolon after macro
259
260using ResolverEntry = std::optional<ElementLinker::Resolver>;
261template <typename T>
262struct ResolverContainer
263{
264 inline static const ResolverEntry resolver = {};
265};
266
267#define MUSX_RESOLVER_ENTRY(Type, ...) \
268template <> \
269struct ResolverContainer<Type> { \
270 inline static const ResolverEntry resolver = ElementLinker::Resolver(__VA_ARGS__); \
271};
272
273template <typename T>
274struct FieldPopulator : public FactoryBase
275{
276 static void populateField(const std::shared_ptr<T>& instance, const XmlElementPtr& fieldElement)
277 {
278 auto it = elementXref().find(fieldElement->getTagName());
279 if (it != elementXref().end()) {
280 std::get<1>(*it)(fieldElement, instance);
281 } else {
282 const bool requireFields = [instance]() {
283 if constexpr (std::is_base_of_v<Base, T>) {
284 return instance->requireAllFields();
285 } else {
286 return true;
287 }
288 }();
289 if (requireFields) {
290 MUSX_UNKNOWN_XML("xml element <" + fieldElement->getParent()->getTagName() + "> has child <" + fieldElement->getTagName() + "> which is not in the element list.");
291 }
292 }
293 }
294
295 static void populate(const std::shared_ptr<T>& instance, const XmlElementPtr& element)
296 {
297 if constexpr (std::is_base_of_v<TextsBase, T>) {
298 instance->text = element->getText();
299 } else {
300 for (auto child = element->getFirstChildElement(); child; child = child->getNextSibling()) {
301 populateField(instance, child);
302 }
303 }
304 if constexpr (std::is_base_of_v<Base, T>) {
305 instance->integrityCheck(instance);
306 }
307 }
308
309 template <typename SubClass = T>
310 static void populate(const std::shared_ptr<T>& instance, const XmlElementPtr& element, ElementLinker& elementLinker)
311 {
312 populate(instance, element);
313 if (ResolverContainer<SubClass>::resolver.has_value()) {
314 elementLinker.addResolver(ResolverContainer<SubClass>::resolver.value(), element->getTagName());
315 }
316 }
317
318 template <typename... Args>
319 static std::shared_ptr<T> createAndPopulate(const XmlElementPtr& element, Args&&... args)
320 {
321 return FieldPopulator<T>::createAndPopulateImpl(element, std::forward<Args>(args)...);
322 }
323
324private:
325 static const std::unordered_map<std::string_view, XmlElementPopulator<T>>& elementXref()
326 {
327 static const std::unordered_map<std::string_view, XmlElementPopulator<T>> xref = []()
328 {
329 std::unordered_map<std::string_view, XmlElementPopulator<T>> retval;
330 auto mappingArray = T::xmlMappingArray();
331 for (std::size_t i = 0; i < mappingArray.size(); i++) {
332 const XmlElementDescriptor<T> descriptor = mappingArray[i];
333 retval[std::get<0>(descriptor)] = std::get<1>(descriptor);
334 }
335 return retval;
336 }();
337 return xref;
338 }
339
340 template <typename... Args>
341 static std::shared_ptr<T> createAndPopulateImpl(const XmlElementPtr& element, Args&&... args)
342 {
343 auto instance = std::make_shared<T>(std::forward<Args>(args)...);
344 FieldPopulator<T>::populate(instance, element);
345 return instance;
346 }
347};
348
355template <typename EnumClass, typename EmbeddedClass, typename... Args>
356inline void populateEmbeddedClass(const XmlElementPtr& e, std::unordered_map<EnumClass, std::shared_ptr<EmbeddedClass>>& listArray, Args&&... args)
357{
358 auto typeAttr = e->findAttribute("type");
359 if (!typeAttr) {
360 throw std::invalid_argument("<" + e->getTagName() + "> element has no type attribute");
361 }
362 listArray.emplace(toEnum<EnumClass>(typeAttr->getValueTrimmed()), FieldPopulator<EmbeddedClass>::createAndPopulate(e, std::forward<Args>(args)...));
363}
364
370template <typename T>
371inline std::vector<T> populateEmbeddedArray(const XmlElementPtr& e, const std::string_view& elementNodeName)
372{
373 std::vector<T> result;
374 for (auto child = e->getFirstChildElement(); child; child = child->getNextSibling()) {
375 if (child->getTagName() != elementNodeName) {
376 MUSX_UNKNOWN_XML("Unknown tag <" + child->getTagName() + "> while processing embedded xml array <" + e->getTagName() + ">");
377 continue;
378 }
379 if constexpr (std::is_fundamental_v<T>) {
380 result.push_back(child->getTextAs<T>());
381 } else if constexpr (std::is_same_v<T, std::string>) {
382 result.push_back(child->getText());
383 } else {
384 result.push_back(FieldPopulator<T>::createAndPopulate(child));
385 }
386 }
387 return result;
388}
389
390template <>
391template <typename... Args>
392inline std::shared_ptr<FontInfo> FieldPopulator<FontInfo>::createAndPopulate(const XmlElementPtr& element, Args&&... args)
393{
394 if (!element->getFirstChildElement()) return nullptr;
395 return FieldPopulator<FontInfo>::createAndPopulateImpl(element, std::forward<Args>(args)...);
396}
397
398void populateFontEfx(const XmlElementPtr& e, const std::shared_ptr<dom::FontInfo>& i);
399
400template <typename T>
401inline bool populateBoolean(const XmlElementPtr& element, const std::shared_ptr<T>& instance)
402{
403 MUSX_ASSERT_IF(!element) {
404 throw std::logic_error("Null element passed to populateBoolean function.");
405 }
406
407 if (!element->getFirstChildElement("offInPart")) {
408 return true;
409 }
410
411 if constexpr (std::is_base_of_v<Base, T>) {
412 const Base& instAsBase = *instance;
413 return instAsBase.getSourcePartId() == SCORE_PARTID; // return false if this is a part
414 } else {
415 return false; // I don't think we'll ever get an `offInPart` for the score, so assume it is for a part if we aren't a Base subclass
416 }
417}
418
419inline std::vector<std::uint8_t> hexToBytes(std::string_view hex)
420{
421 std::vector<std::uint8_t> out;
422
423 if (hex.size() % 2 == 0) {
424 out.reserve(hex.size() / 2);
425 for (std::size_t i = 0; i < hex.size(); i += 2) {
426 unsigned value = 0;
427 const char* first = hex.data() + i;
428 const char* last = first + 2; // OK even when last == data()+size()
429 auto [ptr, ec] = std::from_chars(first, last, value, 16);
430 if (ec != std::errc()) {
431 i = hex.size(); // break out of loop if MUSX_INTEGRITY_ERROR does not throw
432 MUSX_INTEGRITY_ERROR("Invalid hex digit in hex string.");
433 }
434 out.push_back(static_cast<std::uint8_t>(value));
435 }
436 } else {
437 MUSX_INTEGRITY_ERROR("Encountered odd-length hex string.");
438 }
439
440 return out;
441}
442
443#endif // DOXYGEN_SHOULD_IGNORE_THIS
444
445} // namespace factory
446} // namespace musx
Base class to enforce polymorphism across all DOM classes.
Definition BaseClasses.h:83
Cmper getSourcePartId() const
Gets the source partId for this instance. If an instance is fully shared with the score,...
Definition BaseClasses.h:124
A utility class for managing deferred relationships between elements during document construction.
Definition FactoryBase.h:72
std::function< void(const dom::DocumentPtr &)> Resolver
A callable type representing a deferred relationship resolver.
Definition FactoryBase.h:81
void resolveAll(const dom::DocumentPtr &document)
Resolves all deferred relationships.
Definition FactoryBase.h:110
void addResolver(Resolver resolver, const std::string_view &key)
Adds a resolver function to the linker.
Definition FactoryBase.h:92
Factory base class.
Definition FactoryBase.h:134
static void getFieldFromXml(const XmlElementPtr &element, const std::string &nodeName, DataType &dataField, ParserFunc parserFunc, bool expected=false)
Helper function to check if a child exists and populate it if so.
Definition FactoryBase.h:147
static XmlElementPtr getFirstChildElement(const XmlElementPtr &element, const std::string &childElementName)
Helper function to throw when child element does not exist.
Definition FactoryBase.h:162
static std::optional< T > getOptionalChildTextAs(const XmlElementPtr &element, const std::string &childElementName, T defaultValue={})
Helper function to return std::nullopt when child element does not exist.
Definition FactoryBase.h:183
static std::optional< std::string > getOptionalChildText(const XmlElementPtr &element, const std::string &childElementName)
Helper function to return std::nullopt when child element does not exist.
Definition FactoryBase.h:172
Exception for unknown xml node errors. (Used when MUSX_THROW_ON_UNKNOWN_XML is defined....
Definition FactoryBase.h:58
@ Warning
Warning messages indicating potential issues.
static void log(LogLevel level, const std::string &message)
Logs a message with a specific severity level.
Definition Logger.h:87
The DOM (document object model) for musx files.
constexpr Cmper SCORE_PARTID
The part id of the score.
Definition Fundamentals.h:79
std::shared_ptr< Document > DocumentPtr
Shared Document pointer.
Definition BaseClasses.h:55
Provides interfaces and optional implementations for traversing XML.
Definition PugiXmlImpl.h:33
std::tuple< const std::string_view, XmlElementPopulator< T > > XmlElementDescriptor
associates an xml node name with and XmlElementPopulator
Definition XmlInterface.h:125
std::shared_ptr< IXmlElement > XmlElementPtr
shared pointer to IXmlElement
Definition XmlInterface.h:121
object model for musx file (enigmaxml)
Definition BaseClasses.h:36