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 <unordered_set>
29#include <unordered_map>
30#include <tuple>
31#include <sstream>
32#include <type_traits>
33#include <charconv>
34
35#include "musx/util/Logger.h"
36#include "musx/factory/FactoryExceptions.h"
37#include "musx/xml/XmlInterface.h"
38#include "musx/dom/BaseClasses.h"
39#include "musx/dom/Document.h"
40#include "musx/factory/ConstructionContext.h"
41
42namespace musx {
43
48namespace factory {
49
50using namespace musx::xml;
51using namespace musx::dom;
52
57{
58protected:
69 template<typename DataType, typename ParserFunc>
70 static void getFieldFromXml(const XmlElementPtr& element, const std::string& nodeName, DataType& dataField, ParserFunc parserFunc, bool expected = false)
71 {
72 if (auto childElement = element->getFirstChildElement(nodeName)) {
73 dataField = parserFunc(childElement);
74 } else if (expected) {
75 std::stringstream msg;
76 msg << "Expected field <" << element->getTagName() << "><" << nodeName << "> not found.";
78 }
79 }
80
85 static XmlElementPtr getFirstChildElement(const XmlElementPtr& element, const std::string& childElementName)
86 {
87 auto childElement = element->getFirstChildElement(childElementName);
88 if (!childElement) {
89 throw std::invalid_argument("Missing <" + childElementName + "> element.");
90 }
91 return childElement;
92 }
93
95 static std::optional<std::string> getOptionalChildText(const XmlElementPtr& element, const std::string& childElementName)
96 {
97 auto childElement = element->getFirstChildElement(childElementName);
98 if (!childElement) {
99 return std::nullopt;
100 }
101 return childElement->getText();
102 }
103
105 template<typename T>
106 static std::optional<T> getOptionalChildTextAs(const XmlElementPtr& element, const std::string& childElementName, T defaultValue = {})
107 {
108 auto childElement = element->getFirstChildElement(childElementName);
109 if (!childElement) {
110 return std::nullopt;
111 }
112 return childElement->getTextAs<T>(defaultValue);
113 }
114
115public:
116 virtual ~FactoryBase() {}
117};
118
119#ifndef DOXYGEN_SHOULD_IGNORE_THIS
120
121template <typename EnumClass, typename FromClass = std::string_view>
122using XmlEnumMappingElement = std::unordered_map<FromClass, EnumClass>;
123template <typename EnumClass, typename FromClass = std::string_view>
124struct XmlEnumMapping
125{
126 static const XmlEnumMappingElement<EnumClass, FromClass> mapping;
127};
128
129#define MUSX_XML_ENUM_MAPPING(Type, ...) \
130template <> \
131const XmlEnumMappingElement<Type> XmlEnumMapping<Type>::mapping = __VA_ARGS__
132
133#if defined(_MSC_VER)
134#define MUSX_ENUM_CONVERSION_SIGNATURE __FUNCSIG__
135#else
136#define MUSX_ENUM_CONVERSION_SIGNATURE __PRETTY_FUNCTION__
137#endif
138
139template <typename EnumClass, bool IgnoreUnknown, typename FromClass = std::string_view>
140class EnumMapper
141{
142 // If we ever need to, we can create a static lazy-initialize reverse mapping function here
143
144public:
145 static EnumClass xmlToEnum(const FromClass& value)
146 {
147 auto it = XmlEnumMapping<EnumClass>::mapping.find(value);
148 if (it != XmlEnumMapping<EnumClass>::mapping.end()) {
149 return it->second;
150 }
151 if constexpr (!IgnoreUnknown) {
152 std::string msg = [value]() {
153 if constexpr (std::is_arithmetic_v<FromClass>) {
154 return "Invalid enum value from xml: `" + std::to_string(value) + "`";
155 }
156 else {
157 return "Invalid enum value from xml: `" + std::string(value) + "`";
158 }
159 }();
160 msg += " in " + std::string(MUSX_ENUM_CONVERSION_SIGNATURE);
161 MUSX_UNKNOWN_XML(msg);
162 }
163 return {};
164 }
165};
166
167#undef MUSX_ENUM_CONVERSION_SIGNATURE
168
169template<typename EnumClass, typename FromClass, bool IgnoreUnknown = false>
170EnumClass toEnum(const FromClass& value)
171{
172 if constexpr (std::is_convertible_v<FromClass, std::string_view>) {
173 return EnumMapper<EnumClass, IgnoreUnknown, std::string_view>::xmlToEnum(value);
174 } else {
175 return EnumMapper<EnumClass, IgnoreUnknown, FromClass>::xmlToEnum(value);
176 }
177}
178
179template<typename EnumClass, bool IgnoreUnknown = false>
180EnumClass toEnum(const ::musx::xml::XmlElementPtr& e)
181{
182 return toEnum<EnumClass, std::string_view, IgnoreUnknown>(e->getTextTrimmed());
183}
184
185#define MUSX_XML_ELEMENT_ARRAY(Type, ...) \
186const ::musx::xml::XmlElementArray<Type>& Type::xmlMappingArray() { \
187 static const ::musx::xml::XmlElementArray<Type> instance = __VA_ARGS__; \
188 return instance; \
189} \
190static_assert(true, "") // require semicolon after macro
191
192template <typename T>
193struct FieldPopulator : public FactoryBase
194{
195 static void populateField(ConstructionContext& context, const std::shared_ptr<T>& instance, const XmlElementPtr& fieldElement)
196 {
197 auto it = elementXref().find(fieldElement->getTagName());
198 if (it != elementXref().end()) {
199 std::get<1>(*it)(context, fieldElement, instance);
200 } else {
201 const bool requireFields = [instance]() {
202 if constexpr (std::is_base_of_v<EnigmaBase, T>) {
203 return instance->requireAllFields();
204 } else {
205 return true;
206 }
207 }();
208 if (requireFields) {
209 MUSX_UNKNOWN_XML("xml element <" + fieldElement->getParent()->getTagName() + "> has child <" + fieldElement->getTagName() + "> which is not in the element list.");
210 }
211 }
212 }
213
214 static void populate(ConstructionContext& context, const std::shared_ptr<T>& instance, const XmlElementPtr& element)
215 {
216 if constexpr (std::is_base_of_v<TextsBase, T>) {
217 instance->text = element->getText();
218 } else {
219 for (auto child = element->getFirstChildElement(); child; child = child->getNextSibling()) {
220 populateField(context, instance, child);
221 }
222 }
223 }
224
225 template <typename... Args>
226 static std::shared_ptr<T> createAndPopulate(ConstructionContext& context, const XmlElementPtr& element, Args&&... args)
227 {
228 return FieldPopulator<T>::createAndPopulateImpl(context, element, std::forward<Args>(args)...);
229 }
230
232 template <typename... Args>
233 static std::shared_ptr<T> populateExistingOrCreate(
234 ConstructionContext& context, const XmlElementPtr& element, std::shared_ptr<T> instance, Args&&... args)
235 {
236 if (!instance) {
237 instance = std::make_shared<T>(std::forward<Args>(args)...);
238 }
239 FieldPopulator<T>::populate(context, instance, element);
240 return instance;
241 }
242
243private:
244 static const std::unordered_map<std::string_view, XmlElementPopulator<T>>& elementXref()
245 {
246 static const std::unordered_map<std::string_view, XmlElementPopulator<T>> xref = []()
247 {
248 std::unordered_map<std::string_view, XmlElementPopulator<T>> retval;
249 auto mappingArray = T::xmlMappingArray();
250 for (std::size_t i = 0; i < mappingArray.size(); i++) {
251 const XmlElementDescriptor<T> descriptor = mappingArray[i];
252 retval[std::get<0>(descriptor)] = std::get<1>(descriptor);
253 }
254 return retval;
255 }();
256 return xref;
257 }
258
259 template <typename... Args>
260 static std::shared_ptr<T> createAndPopulateImpl(ConstructionContext& context, const XmlElementPtr& element, Args&&... args)
261 {
262 auto instance = std::make_shared<T>(std::forward<Args>(args)...);
263 FieldPopulator<T>::populate(context, instance, element);
264 return instance;
265 }
266};
267
274template <typename EnumClass, typename EmbeddedClass, typename... Args>
275inline void populateEmbeddedClass(ConstructionContext& context, const XmlElementPtr& e,
276 std::unordered_map<EnumClass, std::shared_ptr<EmbeddedClass>>& listArray, Args&&... args)
277{
278 auto typeAttr = e->findAttribute("type");
279 if (!typeAttr) {
280 throw std::invalid_argument("<" + e->getTagName() + "> element has no type attribute");
281 }
282 listArray.emplace(toEnum<EnumClass>(typeAttr->getValueTrimmed()), FieldPopulator<EmbeddedClass>::createAndPopulate(context, e, std::forward<Args>(args)...));
283}
284
290template <typename T>
291inline std::vector<T> populateEmbeddedArray(ConstructionContext& context, const XmlElementPtr& e, const std::string_view& elementNodeName)
292{
293 std::vector<T> result;
294 for (auto child = e->getFirstChildElement(); child; child = child->getNextSibling()) {
295 if (child->getTagName() != elementNodeName) {
296 MUSX_UNKNOWN_XML("Unknown tag <" + child->getTagName() + "> while processing embedded xml array <" + e->getTagName() + ">");
297 continue;
298 }
299 if constexpr (std::is_fundamental_v<T>) {
300 result.push_back(child->getTextAs<T>());
301 } else if constexpr (std::is_same_v<T, std::string>) {
302 result.push_back(child->getText());
303 } else {
304 result.push_back(FieldPopulator<T>::createAndPopulate(context, child));
305 }
306 }
307 return result;
308}
309
310void populateFontEfx(ConstructionContext& context, const XmlElementPtr& e, const std::shared_ptr<dom::FontInfo>& i);
311
315inline void populateFontId(ConstructionContext& context, const XmlElementPtr& e, dom::Cmper& fontId)
316{
317 fontId = e->getTextAs<dom::Cmper>();
318 context.registerFontId(fontId);
319}
320
321template <typename T>
322inline bool populateBoolean(ConstructionContext&, const XmlElementPtr& element, const std::shared_ptr<T>& instance)
323{
324 MUSX_ASSERT_IF(!element) {
325 throw std::logic_error("Null element passed to populateBoolean function.");
326 }
327
328 if (!element->getFirstChildElement("offInPart")) {
329 return true;
330 }
331
332 if constexpr (std::is_base_of_v<EnigmaBase, T>) {
333 const EnigmaBase& instAsBase = *instance;
334 return instAsBase.getSourcePartId() == SCORE_PARTID; // return false if this is a part
335 } else {
336 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
337 }
338}
339
340inline std::vector<std::uint8_t> hexToBytes(std::string_view hex)
341{
342 std::vector<std::uint8_t> out;
343
344 if (hex.size() % 2 == 0) {
345 out.reserve(hex.size() / 2);
346 for (std::size_t i = 0; i < hex.size(); i += 2) {
347 unsigned value = 0;
348 const char* first = hex.data() + i;
349 const char* last = first + 2; // OK even when last == data()+size()
350 auto [ptr, ec] = std::from_chars(first, last, value, 16);
351 if (ec != std::errc()) {
352 i = hex.size(); // break out of loop if MUSX_INTEGRITY_ERROR does not throw
353 MUSX_INTEGRITY_ERROR("Invalid hex digit in hex string.");
354 }
355 out.push_back(static_cast<std::uint8_t>(value));
356 }
357 } else {
358 MUSX_INTEGRITY_ERROR("Encountered odd-length hex string.");
359 }
360
361 return out;
362}
363
364#endif // DOXYGEN_SHOULD_IGNORE_THIS
365
366} // namespace factory
367} // namespace musx
Base for DOM classes that are represented in EnigmaData.
Definition BaseClasses.h:89
Cmper getSourcePartId() const
Gets the source partId for this instance. If an instance is fully shared with the score,...
Definition BaseClasses.h:112
Factory base class.
Definition FactoryBase.h:57
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:70
static XmlElementPtr getFirstChildElement(const XmlElementPtr &element, const std::string &childElementName)
Helper function to throw when child element does not exist.
Definition FactoryBase.h:85
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:106
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:95
@ 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:81
uint16_t Cmper
Enigma "comperator" key type.
Definition Fundamentals.h:57
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:131
std::shared_ptr< IXmlElement > XmlElementPtr
shared pointer to IXmlElement
Definition XmlInterface.h:127
object model for musx file (enigmaxml)
Definition BaseClasses.h:39