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
36#include "musx/util/Logger.h"
37#include "musx/xml/XmlInterface.h"
38#include "musx/dom/BaseClasses.h"
39#include "musx/dom/Document.h"
40
41namespace musx {
42
47namespace factory {
48
49using namespace musx::xml;
50using namespace musx::dom;
51
52
56class unknown_xml_error : public std::runtime_error
57{
58public:
59 using std::runtime_error::runtime_error;
60};
61
62
72public:
80 using Resolver = std::function<void(const dom::DocumentPtr&)>;
81
91 void addResolver(Resolver resolver, const std::string_view& key)
92 {
93 assert(!key.empty());
94 if (resolvers.count(std::string(key)) > 0) {
95 return; // Skip if already registered
96 }
97
98 resolvers[std::string(key)] = std::move(resolver);
99 }
100
109 void resolveAll(const dom::DocumentPtr& document)
110 {
111 for (const auto& [key, resolver] : resolvers) {
112 resolver(document);
113 }
114
115 resolvers.clear();
116 }
117
118private:
125 std::map<std::string, Resolver> resolvers;
126};
127
128
133{
134protected:
145 template<typename DataType, typename ParserFunc>
146 static void getFieldFromXml(const XmlElementPtr& element, const std::string& nodeName, DataType& dataField, ParserFunc parserFunc, bool expected = false)
147 {
148 if (auto childElement = element->getFirstChildElement(nodeName)) {
149 dataField = parserFunc(childElement);
150 } else if (expected) {
151 std::stringstream msg;
152 msg << "Expected field <" << element->getTagName() << "><" << nodeName << "> not found.";
154 }
155 }
156
161 static XmlElementPtr getFirstChildElement(const XmlElementPtr& element, const std::string& childElementName)
162 {
163 auto childElement = element->getFirstChildElement(childElementName);
164 if (!childElement) {
165 throw std::invalid_argument("Missing <" + childElementName + "> element.");
166 }
167 return childElement;
168 }
169
171 static std::optional<std::string> getOptionalChildText(const XmlElementPtr& element, const std::string& childElementName)
172 {
173 auto childElement = element->getFirstChildElement(childElementName);
174 if (!childElement) {
175 return std::nullopt;
176 }
177 return childElement->getText();
178 }
179
181 template<typename T>
182 static std::optional<T> getOptionalChildTextAs(const XmlElementPtr& element, const std::string& childElementName, T defaultValue = {})
183 {
184 auto childElement = element->getFirstChildElement(childElementName);
185 if (!childElement) {
186 return std::nullopt;
187 }
188 return childElement->getTextAs<T>(defaultValue);
189 }
190
191public:
192 virtual ~FactoryBase() {}
193};
194
195#ifndef DOXYGEN_SHOULD_IGNORE_THIS
196
197template <typename EnumClass, typename FromClass = std::string_view>
198using XmlEnumMappingElement = std::unordered_map<FromClass, EnumClass>;
199template <typename EnumClass, typename FromClass = std::string_view>
200struct XmlEnumMapping
201{
202 static const XmlEnumMappingElement<EnumClass, FromClass> mapping;
203};
204
205#define MUSX_XML_ENUM_MAPPING(Type, ...) \
206template <> \
207const XmlEnumMappingElement<Type> XmlEnumMapping<Type>::mapping = __VA_ARGS__
208
209template <typename EnumClass, bool IgnoreUnknown, typename FromClass = std::string_view>
210class EnumMapper
211{
212 // If we ever need to, we can create a static lazy-initialize reverse mapping function here
213
214public:
215 static EnumClass xmlToEnum(const FromClass& value)
216 {
217 auto it = XmlEnumMapping<EnumClass>::mapping.find(value);
218 if (it != XmlEnumMapping<EnumClass>::mapping.end()) {
219 return it->second;
220 }
221 if constexpr (!IgnoreUnknown) {
222 std::string msg = [value]() {
223 if constexpr (std::is_arithmetic_v<FromClass>) {
224 return "Invalid enum value from xml: " + std::to_string(value);
225 }
226 else {
227 return "Invalid enum value from xml: " + std::string(value);
228 }
229 }();
230 MUSX_UNKNOWN_XML(msg);
231 }
232 return {};
233 }
234};
235
236template<typename EnumClass, typename FromClass, bool IgnoreUnknown = false>
237EnumClass toEnum(const FromClass& value)
238{
239 if constexpr (std::is_convertible_v<FromClass, std::string_view>) {
240 return EnumMapper<EnumClass, IgnoreUnknown, std::string_view>::xmlToEnum(value);
241 } else {
242 return EnumMapper<EnumClass, IgnoreUnknown, FromClass>::xmlToEnum(value);
243 }
244}
245
246template<typename EnumClass, bool IgnoreUnknown = false>
247EnumClass toEnum(const ::musx::xml::XmlElementPtr& e)
248{
249 return toEnum<EnumClass, std::string_view, IgnoreUnknown>(e->getTextTrimmed());
250}
251
252#define MUSX_XML_ELEMENT_ARRAY(Type, ...) \
253const ::musx::xml::XmlElementArray<Type>& Type::xmlMappingArray() { \
254 static const ::musx::xml::XmlElementArray<Type> instance = __VA_ARGS__; \
255 return instance; \
256} \
257static_assert(true, "") // require semicolon after macro
258
259using ResolverEntry = std::optional<ElementLinker::Resolver>;
260template <typename T>
261struct ResolverContainer
262{
263 inline static const ResolverEntry resolver = {};
264};
265
266#define MUSX_RESOLVER_ENTRY(Type, ...) \
267template <> \
268struct ResolverContainer<Type> { \
269 inline static const ResolverEntry resolver = ElementLinker::Resolver(__VA_ARGS__); \
270};
271
272template <typename T>
273struct FieldPopulator : public FactoryBase
274{
275 static void populateField(const std::shared_ptr<T>& instance, const XmlElementPtr& fieldElement)
276 {
277 auto it = elementXref().find(fieldElement->getTagName());
278 if (it != elementXref().end()) {
279 std::get<1>(*it)(fieldElement, instance);
280 } else {
281 const bool requireFields = [instance]() {
282 if constexpr (std::is_base_of_v<Base, T>) {
283 return instance->requireAllFields();
284 } else {
285 return true;
286 }
287 }();
288 if (requireFields) {
289 MUSX_UNKNOWN_XML("xml element <" + fieldElement->getParent()->getTagName() + "> has child <" + fieldElement->getTagName() + "> which is not in the element list.");
290 }
291 }
292 }
293
294 static void populate(const std::shared_ptr<T>& instance, const XmlElementPtr& element)
295 {
296 if constexpr (std::is_base_of_v<TextsBase, T>) {
297 instance->text = element->getText();
298 } else {
299 for (auto child = element->getFirstChildElement(); child; child = child->getNextSibling()) {
300 populateField(instance, child);
301 }
302 }
303 if constexpr (std::is_base_of_v<Base, T>) {
304 instance->integrityCheck();
305 }
306 }
307
308 template <typename SubClass = T>
309 static void populate(const std::shared_ptr<T>& instance, const XmlElementPtr& element, ElementLinker& elementLinker)
310 {
311 populate(instance, element);
312 if (ResolverContainer<SubClass>::resolver.has_value()) {
313 elementLinker.addResolver(ResolverContainer<SubClass>::resolver.value(), element->getTagName());
314 }
315 }
316
317 template <typename... Args>
318 static std::shared_ptr<T> createAndPopulate(const XmlElementPtr& element, Args&&... args)
319 {
320 return FieldPopulator<T>::createAndPopulateImpl(element, std::forward<Args>(args)...);
321 }
322
323private:
324 static const std::unordered_map<std::string_view, XmlElementPopulator<T>>& elementXref()
325 {
326 static const std::unordered_map<std::string_view, XmlElementPopulator<T>> xref = []()
327 {
328 std::unordered_map<std::string_view, XmlElementPopulator<T>> retval;
329 auto mappingArray = T::xmlMappingArray();
330 for (std::size_t i = 0; i < mappingArray.size(); i++) {
331 const XmlElementDescriptor<T> descriptor = mappingArray[i];
332 retval[std::get<0>(descriptor)] = std::get<1>(descriptor);
333 }
334 return retval;
335 }();
336 return xref;
337 }
338
339 template <typename... Args>
340 static std::shared_ptr<T> createAndPopulateImpl(const XmlElementPtr& element, Args&&... args)
341 {
342 auto instance = std::make_shared<T>(std::forward<Args>(args)...);
343 FieldPopulator<T>::populate(instance, element);
344 return instance;
345 }
346};
347
353template <typename EnumClass, typename EmbeddedClass>
354static void populateEmbeddedClass(const XmlElementPtr& e, std::unordered_map<EnumClass, std::shared_ptr<EmbeddedClass>>& listArray)
355{
356 auto typeAttr = e->findAttribute("type");
357 if (!typeAttr) {
358 throw std::invalid_argument("<" + e->getTagName() + "> element has no type attribute");
359 }
360 listArray.emplace(toEnum<EnumClass>(typeAttr->getValueTrimmed()), FieldPopulator<EmbeddedClass>::createAndPopulate(e));
361}
362
368template <typename T>
369static std::vector<T> populateEmbeddedArray(const XmlElementPtr& e, const std::string_view& elementNodeName)
370{
371 std::vector<T> result;
372 for (auto child = e->getFirstChildElement(); child; child = child->getNextSibling()) {
373 if (child->getTagName() != elementNodeName) {
374 MUSX_UNKNOWN_XML("Unknown tag <" + child->getTagName() + "> while processing embedded xml array <" + e->getTagName() + ">");
375 continue;
376 }
377 if constexpr (std::is_fundamental_v<T>) {
378 result.push_back(child->getTextAs<T>());
379 } else if constexpr (std::is_same_v<T, std::string>) {
380 result.push_back(child->getText());
381 } else {
382 result.push_back(FieldPopulator<T>::createAndPopulate(child));
383 }
384 }
385 return result;
386}
387
388template <>
389template <typename... Args>
390inline std::shared_ptr<FontInfo> FieldPopulator<FontInfo>::createAndPopulate(const XmlElementPtr& element, Args&&... args)
391{
392 if (!element->getFirstChildElement()) return nullptr;
393 return FieldPopulator<FontInfo>::createAndPopulateImpl(element, std::forward<Args>(args)...);
394}
395
396void populateFontEfx(const XmlElementPtr& e, const std::shared_ptr<dom::FontInfo>& i);
397
398template <typename T>
399static bool populateBoolean(const XmlElementPtr& element, const std::shared_ptr<T>& instance)
400{
401 MUSX_ASSERT_IF(!element) {
402 throw std::logic_error("Null element passed to populateBoolean function.");
403 }
404
405 if (!element->getFirstChildElement("offInPart")) {
406 return true;
407 }
408
409 if constexpr (std::is_base_of_v<Base, T>) {
410 const Base& instAsBase = *instance;
411 return instAsBase.getPartId() == SCORE_PARTID; // return false if this is a part
412 } else {
413 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
414 }
415}
416
417#endif // DOXYGEN_SHOULD_IGNORE_THIS
418
419} // namespace factory
420} // namespace musx
Base class to enforce polymorphism across all DOM classes.
Definition BaseClasses.h:60
virtual Cmper getPartId() const
Gets the partId for this instance (or SCORE_PARTID for score)
Definition BaseClasses.h:104
A utility class for managing deferred relationships between elements during document construction.
Definition FactoryBase.h:71
std::function< void(const dom::DocumentPtr &)> Resolver
A callable type representing a deferred relationship resolver.
Definition FactoryBase.h:80
void resolveAll(const dom::DocumentPtr &document)
Resolves all deferred relationships.
Definition FactoryBase.h:109
void addResolver(Resolver resolver, const std::string_view &key)
Adds a resolver function to the linker.
Definition FactoryBase.h:91
Factory base class.
Definition FactoryBase.h:133
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:146
static XmlElementPtr getFirstChildElement(const XmlElementPtr &element, const std::string &childElementName)
Helper function to throw when child element does not exist.
Definition FactoryBase.h:161
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:182
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:171
Exception for unknown xml node errors. (Used when MUSX_THROW_ON_UNKNOWN_XML is defined....
Definition FactoryBase.h:57
@ 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:80
std::shared_ptr< Document > DocumentPtr
Shared Document pointer.
Definition BaseClasses.h:51
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:32