#include "VariableMapper.h" #include "splitStringAtFirstSlash.h" #include <ChimeraTK/RegisterPath.h> #include <algorithm> #include <iostream> #include <libxml++/libxml++.h> #include <locale> #include <regex> namespace ChimeraTK { /********************************************************************************************************************/ VariableMapper& VariableMapper::getInstance() { static VariableMapper instance; return instance; } /********************************************************************************************************************/ bool VariableMapper::nodeIsWhitespace(const xmlpp::Node* node) { const xmlpp::TextNode* nodeAsText = dynamic_cast<const xmlpp::TextNode*>(node); if(nodeAsText) { return nodeAsText->is_white_space(); } return false; } /********************************************************************************************************************/ void VariableMapper::processLocationNode(xmlpp::Node const* locationNode) { const xmlpp::Element* location = dynamic_cast<const xmlpp::Element*>(locationNode); if(!location) { throw std::invalid_argument("Error parsing xml file in location node."); } std::string locationName = getAttributeValue(location, "name"); for(auto const& node : location->get_children()) { if(nodeIsWhitespace(node)) continue; if(dynamic_cast<xmlpp::CommentNode const*>(node)) continue; if(node->get_name() == "property") { processNode(node, locationName); } else if(node->get_name() == "import") { processImportNode(node, locationName); } else if(node->get_name() == "has_history") { auto& locationInfo = _locationDefaults[locationName]; locationInfo.useHasHistoryDefault = true; locationInfo.hasHistory = evaluateBool(getContentString(node)); } else if(node->get_name() == "is_writeable") { auto& locationInfo = _locationDefaults[locationName]; locationInfo.useIsWriteableDefault = true; locationInfo.isWriteable = evaluateBool(getContentString(node)); } else if(node->get_name() == "macro_pulse_number_source") { auto& locationInfo = _locationDefaults[locationName]; locationInfo.useMacroPulseNumberSourceDefault = true; locationInfo.macroPulseNumberSource = getContentString(node); } else if(node->get_name() == "D_spectrum") { processSpectrumNode(node, locationName); } else if(node->get_name() == "D_array") { processNode(node, locationName); } else if(node->get_name() == "D_xy") { processXyNode(node, locationName); } else { throw std::invalid_argument(std::string("Error parsing xml file in location ") + locationName + ": Unknown node '" + node->get_name() + "'"); } } } /********************************************************************************************************************/ std::string determineName(const xmlpp::Element* property, std::string source) { const xmlpp::Attribute* nameAttribute = property->get_attribute("name"); // if a name is given in xml take it if(nameAttribute) { return nameAttribute->get_value(); } else { // auto-determine the name from the source ///@todo FIXME: use register path to do the slash and replace fiddeling std::string name; if(source[0] == '/') { name = source.substr(1); } else { name = source; } // replace / with . in name return std::regex_replace(name, std::regex("/"), "."); } } /********************************************************************************************************************/ std::string getAbsoluteSource(std::string source, std::string locationName) { if(source[0] == '/') { return source; } else { return std::string("/") + locationName + "/" + source; } } /********************************************************************************************************************/ template<class PROPERTY_DESCRIPTION_TYPE> void VariableMapper::processHistoryAndWritableAttributes(PROPERTY_DESCRIPTION_TYPE propertyDescription, const xmlpp::Element* propertyXmlElement, std::string locationName) { auto hasHistoryNodes = propertyXmlElement->get_children("has_history"); if(!hasHistoryNodes.empty()) { propertyDescription->hasHistory = evaluateBool(getContentString(hasHistoryNodes.front())); } else { propertyDescription->hasHistory = getHasHistoryDefault(locationName); } auto isWriteableNodes = propertyXmlElement->get_children("is_writeable"); if(!isWriteableNodes.empty()) { propertyDescription->isWriteable = evaluateBool(getContentString(isWriteableNodes.front())); } else { propertyDescription->isWriteable = getIsWriteableDefault(locationName); } auto publishZeroMQ = propertyXmlElement->get_children("publish_ZMQ"); if(!publishZeroMQ.empty()) { propertyDescription->publishZMQ = evaluateBool(getContentString(publishZeroMQ.front())); } else { propertyDescription->publishZMQ = false; } auto macroPulseNumberSource = propertyXmlElement->get_children("macro_pulse_number_source"); if(!macroPulseNumberSource.empty()) { propertyDescription->macroPulseNumberSource = getContentString(macroPulseNumberSource.front()); } else { propertyDescription->macroPulseNumberSource = getMacroPusleNumberSourceDefault(locationName); } } /********************************************************************************************************************/ void VariableMapper::addDescription( std::shared_ptr<PropertyDescription> const& propertyDescription, std::list<std::string> const& absoluteSources) { _descriptions.push_back(propertyDescription); for(auto& source : absoluteSources) { _usedInputVariables.insert(source); } } /********************************************************************************************************************/ xmlpp::Element const* asXmlElement(xmlpp::Node const* node) { const xmlpp::Element* element = dynamic_cast<const xmlpp::Element*>(node); if(!element) { throw std::invalid_argument("Error parsing xml file: Node is not an element node: " + node->get_name()); } return element; } /********************************************************************************************************************/ void VariableMapper::processNode(xmlpp::Node const* propertyNode, std::string locationName) { auto property = asXmlElement(propertyNode); std::string source = getAttributeValue(property, "source"); std::string name = determineName(property, source); const xmlpp::Attribute* typeAttribute = property->get_attribute("type"); std::map<std::string, AutoPropertyDescription::DataType> dataTypeMap( {{"auto", AutoPropertyDescription::DataType::Auto}, {"byte", AutoPropertyDescription::DataType::Byte}, {"short", AutoPropertyDescription::DataType::Short}, {"int", AutoPropertyDescription::DataType::Int}, {"long", AutoPropertyDescription::DataType::Long}, {"float", AutoPropertyDescription::DataType::Float}, {"double", AutoPropertyDescription::DataType::Double}}); auto type = AutoPropertyDescription::DataType::Auto; if(typeAttribute) { try { type = dataTypeMap.at(typeAttribute->get_value()); } catch(std::out_of_range&) { throw ChimeraTK::logic_error("Unknown type attribute in line " + std::to_string(property->get_line()) + ": " + typeAttribute->get_value()); } } std::string absoluteSource = getAbsoluteSource(source, locationName); // prepare the property description auto autoPropertyDescription = std::make_shared<AutoPropertyDescription>(absoluteSource, locationName, name, type); processHistoryAndWritableAttributes(autoPropertyDescription, property, locationName); addDescription(autoPropertyDescription, {absoluteSource}); } /********************************************************************************************************************/ void VariableMapper::processSpectrumNode(xmlpp::Node const* node, std::string locationName) { auto spectrumXml = asXmlElement(node); // the "main source" of a spectum std::string source = getAttributeValue(spectrumXml, "source"); std::string name = determineName(spectrumXml, source); std::string absoluteSource = getAbsoluteSource(source, locationName); // prepare the property description auto spectrumDescription = std::make_shared<SpectrumDescription>(absoluteSource, locationName, name); processHistoryAndWritableAttributes(spectrumDescription, spectrumXml, locationName); auto startNodes = spectrumXml->get_children("start"); if(!startNodes.empty()) { spectrumDescription->start = std::stof(getContentString(startNodes.front())); } std::list<std::string> usedVariables({absoluteSource}); auto incrementNodes = spectrumXml->get_children("increment"); if(!incrementNodes.empty()) { spectrumDescription->increment = std::stof(getContentString(incrementNodes.front())); } auto startSourceNodes = spectrumXml->get_children("startSource"); if(!startSourceNodes.empty()) { auto startSource = getContentString(startSourceNodes.front()); spectrumDescription->startSource = startSource; usedVariables.push_back(startSource); } auto incrementSourceNodes = spectrumXml->get_children("incrementSource"); if(!incrementSourceNodes.empty()) { auto incrementSource = getContentString(incrementSourceNodes.front()); spectrumDescription->incrementSource = incrementSource; usedVariables.push_back(incrementSource); } auto numberOfBuffersNodes = spectrumXml->get_children("numberOfBuffers"); if(!numberOfBuffersNodes.empty()) { auto numberOfBuffers = getContentString(numberOfBuffersNodes.front()); spectrumDescription->numberOfBuffers = std::stoi(numberOfBuffers); usedVariables.push_back(numberOfBuffers); } addDescription(spectrumDescription, usedVariables); } /********************************************************************************************************************/ void VariableMapper::processXyNode(xmlpp::Node const* node, std::string& locationName) { auto xyXml = asXmlElement(node); auto xSource = getAttributeValue(xyXml, "x_source"); auto ySource = getAttributeValue(xyXml, "y_source"); auto name = determineName(xyXml, xSource + "_" + ySource); auto xAbsoluteSource = getAbsoluteSource(xSource, locationName); auto yAbsoluteSource = getAbsoluteSource(ySource, locationName); auto xyDescription = std::make_shared<XyDescription>(xAbsoluteSource, yAbsoluteSource, locationName, name, false); processHistoryAndWritableAttributes(xyDescription, xyXml, locationName); auto descriptionNode = xyXml->get_first_child("description"); xyDescription->description = getContentString(descriptionNode); addDescription(xyDescription, {xAbsoluteSource, yAbsoluteSource}); } /********************************************************************************************************************/ void VariableMapper::processImportNode(xmlpp::Node const* importNode, std::string importLocationName) { const xmlpp::Element* importElement = dynamic_cast<const xmlpp::Element*>(importNode); std::string directory; if(importElement) { // look for a directory attribute const xmlpp::Attribute* directoryAttribute = importElement->get_attribute("directory"); if(directoryAttribute) { directory = directoryAttribute->get_value(); } } for(auto const& node : importNode->get_children()) { const xmlpp::TextNode* nodeAsText = dynamic_cast<const xmlpp::TextNode*>(node); std::string importSource = nodeAsText->get_content(); import(importSource, importLocationName, directory); } } /********************************************************************************************************************/ void VariableMapper::import(std::string importSource, std::string importLocationName, std::string directory) { // a slash will be added after the source, so we make the source empty for an // import of everything if(importSource == "/") { importSource = ""; } // loop source tree, cut beginning, replace / with _ and add a property for(auto const& processVariable : _inputVariables) { if(_usedInputVariables.find(processVariable) != _usedInputVariables.end()) { continue; } if(processVariable.find(importSource + "/") == 0) { // processVariable starts with wanted source auto nameSource = processVariable.substr(importSource.size() + 1); // add the slash to be removed // we use the register path because it removes duplicate separators and // allows to use . as separater to replace all / with . ChimeraTK::RegisterPath propertyName; std::string locationName; if(importLocationName.empty()) { // a global import, try to get the location name from the source auto locationAndPropertyName = splitStringAtFirstSlash(nameSource); locationName = locationAndPropertyName.first; propertyName = locationAndPropertyName.second; if(locationName.empty()) { throw std::logic_error(std::string("Invalid XML content in global import of ") + (importSource.empty() ? "/" : importSource) + ": Cannot create location name from '" + nameSource + "', one hirarchy level is missing."); } // convenience for the user: You get an error message is you try a // global import with directory (in case you did not validate your xml // against the schema). if(!directory.empty()) { throw std::logic_error(std::string("Invalid XML content in global import of ") + (importSource.empty() ? "/" : importSource) + ": You cannot have a directory in a global import."); } } else { // import into a location, we know the location name. // add the directory first, then add the name source property propertyName /= directory; propertyName /= nameSource; locationName = importLocationName; } // get the property name with . instead of / propertyName.setAltSeparator("."); auto autoPropertyDescription = std::make_shared<AutoPropertyDescription>( processVariable, locationName, propertyName.getWithAltSeparator()); // we are importing, so all properties get the intended defaults (not // individual settings) autoPropertyDescription->hasHistory = getHasHistoryDefault(locationName); autoPropertyDescription->isWriteable = getIsWriteableDefault(locationName); autoPropertyDescription->macroPulseNumberSource = getMacroPusleNumberSourceDefault(locationName); addDescription(autoPropertyDescription, {processVariable}); } } } /********************************************************************************************************************/ void VariableMapper::prepareOutput(std::string xmlFile, std::set<std::string> inputVariables) { clear(); _inputVariables = inputVariables; xmlpp::DomParser parser; // parser.set_validate(); parser.set_substitute_entities(); // We just want the text to be // resolved/unescaped automatically. parser.parse_file(xmlFile); if(parser) { // Walk the tree: const xmlpp::Node* rootNode = parser.get_document()->get_root_node(); // deleted by DomParser. for(auto const& mainNode : rootNode->get_children()) { if(nodeIsWhitespace(mainNode)) continue; if(dynamic_cast<xmlpp::CommentNode const*>(mainNode)) continue; if(mainNode->get_name() == "location") { processLocationNode(mainNode); } else if(mainNode->get_name() == "import") { processImportNode(mainNode); } else if(mainNode->get_name() == "has_history") { _globalDefaults.hasHistory = evaluateBool(getContentString(mainNode)); } else if(mainNode->get_name() == "is_writeable") { _globalDefaults.isWriteable = evaluateBool(getContentString(mainNode)); } else if(mainNode->get_name() == "macro_pulse_number_source") { _globalDefaults.macroPulseNumberSource = getContentString(mainNode); } else { throw std::invalid_argument( std::string("Error parsing xml file ") + xmlFile + ": Unknown node '" + mainNode->get_name() + "'"); } } } else { throw std::invalid_argument(std::string("Error parsing xml file ") + xmlFile + ". No document produced."); } } /********************************************************************************************************************/ std::list<std::shared_ptr<PropertyDescription>> const& VariableMapper::getAllProperties() const { return _descriptions; } /********************************************************************************************************************/ std::list<std::shared_ptr<PropertyDescription>> VariableMapper::getPropertiesInLocation(std::string location) const { std::list<std::shared_ptr<PropertyDescription>> output; for(auto const& variable : _descriptions) { if(variable->location == location) { (void)output.push_back(variable); } } return output; } /********************************************************************************************************************/ std::unordered_set<std::string> VariableMapper::getAllLocations() const { std::unordered_set<std::string> result; for(auto const& variable : _descriptions) { result.insert(variable->location); } return result; } /********************************************************************************************************************/ void VariableMapper::directImport(std::set<std::string> inputVariables) { clear(); _inputVariables = inputVariables; import("/", ""); // import from /, create location names from first level of the tree } /********************************************************************************************************************/ void VariableMapper::clear() { _inputVariables.clear(); _usedInputVariables.clear(); _locationDefaults.clear(); _globalDefaults = PropertyAttributes(); _descriptions.clear(); } /********************************************************************************************************************/ /// printing the map is useful for debugging void VariableMapper::print(std::ostream& os) const { os << "====== VariableMapper =====" << std::endl; for(auto& description : _descriptions) { description->print(os); } os << "======= Mapping End =======" << std::endl; } /********************************************************************************************************************/ bool VariableMapper::evaluateBool(std::string txt) { transform(txt.begin(), txt.end(), txt.begin(), ::tolower); if(txt == "false" or txt == "0") { return false; } else if(txt == "true" or txt == "1") { return true; } else { throw std::invalid_argument(std::string("Error parsing xml file: could not convert to bool: ") + txt); } } /********************************************************************************************************************/ std::string VariableMapper::getContentString(xmlpp::Node const* node) { for(auto const& subNode : node->get_children()) { const xmlpp::TextNode* nodeAsText = dynamic_cast<const xmlpp::TextNode*>(subNode); if(nodeAsText) { if(nodeAsText->is_white_space()) { continue; } return nodeAsText->get_content(); } } throw std::invalid_argument( std::string("Error parsing xml file: node ") + node->get_name() + " does not have text nodes as children."); } /********************************************************************************************************************/ bool VariableMapper::getHasHistoryDefault(std::string const& locationName) { // if there is no default setting for the location, we will get the "default // default" which has useHasHistoryDefault disabled which is auto-generated by // the [] operator auto locationInfo = _locationDefaults[locationName]; if(locationInfo.useHasHistoryDefault) { return locationInfo.hasHistory; } else { return _globalDefaults.hasHistory; } } /********************************************************************************************************************/ bool VariableMapper::getIsWriteableDefault(std::string const& locationName) { // if there is no default setting for the location, we will get the "default // default" which has useIsWriteableDefault disabled which is auto-generated // by the [] operator auto locationInfo = _locationDefaults[locationName]; if(locationInfo.useIsWriteableDefault) { return locationInfo.isWriteable; } else { return _globalDefaults.isWriteable; } } /********************************************************************************************************************/ std::string VariableMapper::getMacroPusleNumberSourceDefault(std::string const& locationName) { // if there is no default setting for the location, we will get the "default // default" which has useMacroPulseNumberSourceDefault disabled which is // auto-generated by the [] operator auto locationInfo = _locationDefaults[locationName]; if(locationInfo.useMacroPulseNumberSourceDefault) { return locationInfo.macroPulseNumberSource; } else { return _globalDefaults.macroPulseNumberSource; } } /********************************************************************************************************************/ std::string VariableMapper::getAttributeValue(const xmlpp::Element* node, std::string const& attributeName) { auto attribute = node->get_attribute(attributeName); if(!attribute) { throw std::invalid_argument( "Error parsing xml file. Attribute '" + attributeName + "' not found in node '" + node->get_name() + "'."); } return attribute->get_value(); } } // namespace ChimeraTK