diff --git a/src/App/CMakeLists.txt b/src/App/CMakeLists.txt index d97f8cd8d7..be1699a541 100644 --- a/src/App/CMakeLists.txt +++ b/src/App/CMakeLists.txt @@ -92,6 +92,7 @@ generate_from_xml(LinkBaseExtensionPy) generate_from_xml(DocumentObjectGroupPy) generate_from_xml(GeoFeaturePy) generate_from_xml(GeoFeatureGroupExtensionPy) +generate_from_xml(MetadataPy) generate_from_xml(OriginGroupExtensionPy) generate_from_xml(PartPy) @@ -108,6 +109,7 @@ SET(FreeCADApp_XML_SRCS DocumentObjectExtensionPy.xml GroupExtensionPy.xml LinkBaseExtensionPy.xml + MetadataPy.xml DocumentObjectGroupPy.xml DocumentObjectPy.xml GeoFeaturePy.xml @@ -261,6 +263,8 @@ SET(FreeCADApp_CPP_SRCS Enumeration.cpp Material.cpp MaterialPyImp.cpp + Metadata.cpp + MetadataPyImp.cpp ) SET(FreeCADApp_HPP_SRCS @@ -273,6 +277,7 @@ SET(FreeCADApp_HPP_SRCS ComplexGeoData.h Enumeration.h Material.h + Metadata.h ) SET(FreeCADApp_SRCS diff --git a/src/App/Metadata.cpp b/src/App/Metadata.cpp new file mode 100644 index 0000000000..31a8853c5a --- /dev/null +++ b/src/App/Metadata.cpp @@ -0,0 +1,750 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file LICENSE.html. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#ifndef _PreComp_ +# include +#endif + +#include "Metadata.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +#include "Base/XMLTools.h" +#include "App/Expression.h" +#include "App/Application.h" + +/* +*** From GCC: *** +In the GNU C Library, "major" and "minor" are defined +by . For historical compatibility, it is +currently defined by as well, but we plan to +remove this soon. To use "major", include +directly. If you did not intend to use a system-defined macro +"major", you should undefine it after including . +*/ +#ifdef major +#undef major +#endif +#ifdef minor +#undef minor +#endif + +using namespace App; +namespace fs = boost::filesystem; +XERCES_CPP_NAMESPACE_USE + + +Metadata::Metadata(const fs::path& metadataFile) +{ + // Any exception thrown by the XML code propagates out and prevents object creation + XMLPlatformUtils::Initialize(); + + _parser = std::make_shared(); + _parser->setValidationScheme(XercesDOMParser::Val_Never); + _parser->setDoNamespaces(true); + + auto errHandler = std::make_unique(); + _parser->setErrorHandler(errHandler.get()); + + _parser->parse(metadataFile.string().c_str()); + + auto doc = _parser->getDocument(); + _dom = doc->getDocumentElement(); + + auto rootTagName = StrXUTF8(_dom->getTagName()).str; + if (rootTagName != "package") + throw Base::XMLBaseException("Malformed package.xml document: Root group not found"); + + auto formatVersion = XMLString::parseInt(_dom->getAttribute(XUTF8Str("format").unicodeForm())); + switch (formatVersion) { + case 1: + parseVersion1(_dom); + break; + default: + throw Base::XMLBaseException("package.xml format version is not supported by this version of FreeCAD"); + } +} + +Metadata::Metadata() : _dom(nullptr) +{ +} + +Metadata::Metadata(const DOMNode* domNode, int format) : _dom(nullptr) +{ + auto element = dynamic_cast(domNode); + if (element) { + switch (format) { + case 1: + parseVersion1(element); + break; + default: + throw Base::XMLBaseException("package.xml format version is not supported by this version of FreeCAD"); + } + } +} + +Metadata::~Metadata() +{ +} + +std::string Metadata::name() const +{ + return _name; +} + +Meta::Version Metadata::version() const +{ + return _version; +} + +std::string Metadata::description() const +{ + return _description; +} + +std::vector Metadata::maintainer() const +{ + return _maintainer; +} + +std::vector Metadata::license() const +{ + return _license; +} + +std::vector Metadata::url() const +{ + return _url; +} + +std::vector Metadata::author() const +{ + return _author; +} + +std::vector Metadata::depend() const +{ + return _depend; +} + +std::vector Metadata::conflict() const +{ + return _conflict; +} + +std::vector Metadata::replace() const +{ + return _replace; +} + +std::vector Metadata::tag() const +{ + return _tag; +} + +fs::path Metadata::icon() const +{ + return _icon; +} + +std::string Metadata::classname() const +{ + return _classname; +} + +std::vector Metadata::file() const +{ + return _file; +} + +Meta::Version App::Metadata::freecadmin() const +{ + return _freecadmin; +} + +Meta::Version App::Metadata::freecadmax() const +{ + return _freecadmax; +} + +std::multimap Metadata::content() const +{ + return _content; +} + +std::vector Metadata::operator[](const std::string& tag) const +{ + std::vector returnValue; + auto range = _genericMetadata.equal_range(tag); + for (auto item = range.first; item != range.second; ++item) + returnValue.push_back(item->second); + return returnValue; +} + +XERCES_CPP_NAMESPACE::DOMElement* Metadata::dom() const +{ + return _dom; +} + +void Metadata::setName(const std::string& name) +{ + std::string invalidCharacters = "/\\?%*:|\"<>"; // Should cover all OSes + if (_name.find_first_of(invalidCharacters) != std::string::npos) + throw Base::RuntimeError("Name cannot contain any of: " + invalidCharacters); + + _name = name; +} + +void Metadata::setVersion(const Meta::Version& version) +{ + _version = version; +} + +void Metadata::setDescription(const std::string& description) +{ + _description = description; +} + +void Metadata::addMaintainer(const Meta::Contact& maintainer) +{ + _maintainer.push_back(maintainer); +} + +void Metadata::addLicense(const Meta::License& license) +{ + _license.push_back(license); +} + +void Metadata::addUrl(const Meta::Url& url) +{ + _url.push_back(url); +} + +void Metadata::addAuthor(const Meta::Contact& author) +{ + _author.push_back(author); +} + +void Metadata::addDepend(const Meta::Dependency& dep) +{ + _depend.push_back(dep); +} + +void Metadata::addConflict(const Meta::Dependency& dep) +{ + _conflict.push_back(dep); +} + +void Metadata::addReplace(const Meta::Dependency& dep) +{ + _replace.push_back(dep); +} + +void Metadata::addTag(const std::string& tag) +{ + _tag.push_back(tag); +} + +void Metadata::setIcon(const fs::path& path) +{ + _icon = path; +} + +void Metadata::setClassname(const std::string& name) +{ + _classname = name; +} + +void Metadata::addFile(const fs::path& path) +{ + _file.push_back(path); +} + +void Metadata::addContentItem(const std::string& tag, const Metadata& item) +{ + _content.insert(std::make_pair(tag, item)); +} + +void App::Metadata::setFreeCADMin(const Meta::Version& version) +{ + _freecadmin = version; +} + +void App::Metadata::setFreeCADMax(const Meta::Version& version) +{ + _freecadmax = version; +} + +void App::Metadata::addGenericMetadata(const std::string& tag, const Meta::GenericMetadata& genericMetadata) +{ + _genericMetadata.insert(std::make_pair(tag, genericMetadata)); +} + + +DOMElement* appendSimpleXMLNode(DOMElement* baseNode, const std::string& nodeName, const std::string& nodeContents) +{ + // For convenience (and brevity of final output) don't create nodes that don't have contents + if (nodeContents.empty()) + return nullptr; + + auto doc = baseNode->getOwnerDocument(); + DOMElement* namedElement = doc->createElement(XUTF8Str(nodeName.c_str()).unicodeForm()); + baseNode->appendChild(namedElement); + DOMText* namedNode = doc->createTextNode(XUTF8Str(nodeContents.c_str()).unicodeForm()); + namedElement->appendChild(namedNode); + return namedElement; +} + +void addAttribute(DOMElement* node, const std::string& key, const std::string& value) +{ + if (value.empty()) + return; + + node->setAttribute(XUTF8Str(key.c_str()).unicodeForm(), XUTF8Str(value.c_str()).unicodeForm()); +} + +void addDependencyNode(DOMElement* root, const std::string& name, const Meta::Dependency& depend) +{ + auto element = appendSimpleXMLNode(root, name, depend.package); + if (element) { + addAttribute(element, "version_lt", depend.version_lt); + addAttribute(element, "version_lte", depend.version_lte); + addAttribute(element, "version_eq", depend.version_eq); + addAttribute(element, "version_gte", depend.version_gte); + addAttribute(element, "version_gt", depend.version_gt); + addAttribute(element, "condition", depend.condition); + } +} + +void Metadata::write(const fs::path& file) const +{ + DOMImplementation* impl = DOMImplementationRegistry::getDOMImplementation(XUTF8Str("Core LS").unicodeForm()); + + DOMDocument* doc = impl->createDocument(nullptr, XUTF8Str("package").unicodeForm(), nullptr); + DOMElement* root = doc->getDocumentElement(); + root->setAttribute(XUTF8Str("format").unicodeForm(), XUTF8Str("1").unicodeForm()); + + appendToElement(root); + + DOMLSSerializer* theSerializer = ((DOMImplementationLS*)impl)->createLSSerializer(); + DOMConfiguration* config = theSerializer->getDomConfig(); + if (config->canSetParameter(XMLUni::fgDOMWRTFormatPrettyPrint, true)) + config->setParameter(XMLUni::fgDOMWRTFormatPrettyPrint, true); + + // set feature if the serializer supports the feature/mode + if (config->canSetParameter(XMLUni::fgDOMWRTSplitCdataSections, true)) + config->setParameter(XMLUni::fgDOMWRTSplitCdataSections, true); + + if (config->canSetParameter(XMLUni::fgDOMWRTDiscardDefaultContent, true)) + config->setParameter(XMLUni::fgDOMWRTDiscardDefaultContent, true); + + try { + XMLFormatTarget* myFormTarget = new LocalFileFormatTarget(file.string().c_str()); + DOMLSOutput* theOutput = ((DOMImplementationLS*)impl)->createLSOutput(); + + theOutput->setByteStream(myFormTarget); + theSerializer->write(doc, theOutput); + + theOutput->release(); + theSerializer->release(); + delete myFormTarget; + } + catch (const XMLException& toCatch) { + char* message = XMLString::transcode(toCatch.getMessage()); + std::string what = message; + XMLString::release(&message); + throw Base::XMLBaseException(what); + } + catch (const DOMException& toCatch) { + char* message = XMLString::transcode(toCatch.getMessage()); + std::string what = message; + XMLString::release(&message); + throw Base::XMLBaseException(what); + } + + doc->release(); +} + +bool Metadata::satisfies(const Meta::Dependency& dep) +{ + if (dep.package != _name) + return false; + + // The "condition" attribute allows an expression to enable or disable this dependency check: it must contain a valid + // FreeCAD Expression. If it evaluates to false, this dependency is bypassed (e.g. this function returns false). + if (!dep.condition.empty()) { + auto injectedString = dep.condition; + std::map replacements; + std::map& config = App::Application::Config(); + replacements.insert(std::make_pair("$BuildVersionMajor", config["BuildVersionMajor"])); + replacements.insert(std::make_pair("$BuildVersionMinor", config["BuildVersionMinor"])); + replacements.insert(std::make_pair("$BuildRevision", config["BuildRevision"])); + for (const auto& replacement : replacements) { + auto pos = injectedString.find(replacement.first); + while (pos != std::string::npos) { + injectedString.replace(pos, replacement.first.length(), replacement.second); + pos = injectedString.find(replacement.first); + } + } + auto parsedExpression = App::Expression::parse(nullptr, dep.condition); + auto result = parsedExpression->eval(); + if (boost::any_cast (result->getValueAsAny()) == false) + return false; + } + + if (!dep.version_eq.empty()) + return _version == Meta::Version(dep.version_eq); + + // Any of the others might be specified in pairs, so only return the "false" case + + if (!dep.version_lt.empty()) + if (!(_version < Meta::Version(dep.version_lt))) + return false; + + if (!dep.version_lte.empty()) + if (!(_version <= Meta::Version(dep.version_lt))) + return false; + + if (!dep.version_gt.empty()) + if (!(_version > Meta::Version(dep.version_lt))) + return false; + + if (!dep.version_gte.empty()) + if (!(_version >= Meta::Version(dep.version_lt))) + return false; + + return true; +} + +void Metadata::appendToElement(DOMElement* root) const +{ + appendSimpleXMLNode(root, "name", _name); + appendSimpleXMLNode(root, "description", _description); + appendSimpleXMLNode(root, "version", _version.str()); + + for (const auto& maintainer : _maintainer) { + auto element = appendSimpleXMLNode(root, "maintainer", maintainer.name); + if (element) + addAttribute(element, "email", maintainer.email); + } + + for (const auto& license : _license) { + auto element = appendSimpleXMLNode(root, "license", license.name); + if (element) + addAttribute(element, "file", license.file.string()); + } + + if (_freecadmin != Meta::Version()) + appendSimpleXMLNode(root, "freecadmin", _freecadmin.str()); + + if (_freecadmax != Meta::Version()) + appendSimpleXMLNode(root, "freecadmax", _freecadmin.str()); + + for (const auto& url : _url) { + auto element = appendSimpleXMLNode(root, "url", url.location); + if (element) { + std::string typeAsString("website"); + switch (url.type) { + case Meta::UrlType::website: typeAsString = "website"; break; + case Meta::UrlType::repository: typeAsString = "repository"; break; + case Meta::UrlType::bugtracker: typeAsString = "bugtracker"; break; + case Meta::UrlType::readme: typeAsString = "readme"; break; + case Meta::UrlType::documentation: typeAsString = "documentation"; break; + } + addAttribute(element, "type", typeAsString); + } + } + + for (const auto& author : _author) { + auto element = appendSimpleXMLNode(root, "author", author.name); + if (element) + addAttribute(element, "email", author.email); + } + + for (const auto& depend : _depend) + addDependencyNode(root, "depend", depend); + + for (const auto& conflict : _conflict) + addDependencyNode(root, "conflict", conflict); + + for (const auto& replace : _replace) + addDependencyNode(root, "replace", replace); + + for (const auto& tag : _tag) + appendSimpleXMLNode(root, "tag", tag); + + appendSimpleXMLNode(root, "icon", _icon.string()); + + appendSimpleXMLNode(root, "classname", _classname); + + for (const auto& file : _file) + appendSimpleXMLNode(root, "file", file.string()); + + for (const auto& md : _genericMetadata) { + auto element = appendSimpleXMLNode(root, md.first, md.second.contents); + for (const auto& attr : md.second.attributes) + addAttribute(element, attr.first, attr.second); + } + + if (!_content.empty()) { + auto doc = root->getOwnerDocument(); + DOMElement* contentRootElement = doc->createElement(XUTF8Str("content").unicodeForm()); + root->appendChild(contentRootElement); + for (const auto& content : _content) { + DOMElement* contentElement = doc->createElement(XUTF8Str(content.first.c_str()).unicodeForm()); + contentRootElement->appendChild(contentElement); + content.second.appendToElement(contentElement); + } + } +} + + +void Metadata::parseVersion1(const DOMNode* startNode) +{ + auto children = startNode->getChildNodes(); + + for (XMLSize_t i = 0; i < children->getLength(); ++i) { + auto child = children->item(i); + auto element = dynamic_cast(child); + if (!element) + continue; + + auto tag = element->getNodeName(); + auto tagString = StrXUTF8(tag).str; + + if (tagString == "name") + _name = StrXUTF8(element->getTextContent()).str; + else if (tagString == "version") + _version = Meta::Version(StrXUTF8(element->getTextContent()).str); + else if (tagString == "description") + _description = StrXUTF8(element->getTextContent()).str; + else if (tagString == "maintainer") + _maintainer.emplace_back(element); + else if (tagString == "license") + _license.emplace_back(element); + else if (tagString == "freecadmin") + _freecadmin = Meta::Version(StrXUTF8(element->getTextContent()).str); + else if (tagString == "freecadmax") + _freecadmax = Meta::Version(StrXUTF8(element->getTextContent()).str); + else if (tagString == "url") + _url.emplace_back(element); + else if (tagString == "author") + _author.emplace_back(element); + else if (tagString == "depend") + _depend.emplace_back(element); + else if (tagString == "conflict") + _conflict.emplace_back(element); + else if (tagString == "replace") + _replace.emplace_back(element); + else if (tagString == "tag") + _tag.emplace_back(StrXUTF8(element->getTextContent()).str); + else if (tagString == "file") + _file.emplace_back(StrXUTF8(element->getTextContent()).str); + else if (tagString == "classname") + _classname = StrXUTF8(element->getTextContent()).str; + else if (tagString == "icon") + _icon = fs::path(StrXUTF8(element->getTextContent()).str); + else if (tagString == "content") + parseContentNodeVersion1(element); // Recursive call + else { + // If none of this node's children have children of their own, it is a simple element and we + // can handle it as a GenericMetadata object + auto children = element->getChildNodes(); + bool hasGrandchildren = false; + for (XMLSize_t i = 0; i < children->getLength() && !hasGrandchildren; ++i) + if (children->item(i)->getChildNodes()->getLength() > 0) + hasGrandchildren = true; + if (!hasGrandchildren) + _genericMetadata.insert(std::make_pair(tagString, Meta::GenericMetadata(element))); + } + } +} + +void Metadata::parseContentNodeVersion1(const DOMElement* contentNode) +{ + auto children = contentNode->getChildNodes(); + for (XMLSize_t i = 0; i < children->getLength(); ++i) { + auto child = dynamic_cast(children->item(i)); + if (child) { + auto tag = StrXUTF8(child->getTagName()).str; + _content.insert(std::make_pair(tag, Metadata(child, 1))); + } + } +} + +Meta::Contact::Contact(const std::string& name, const std::string& email) : + name(name), + email(email) +{ + // This has to be provided manually since we have another constructor +} + +Meta::Contact::Contact(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + auto emailAttribute = e->getAttribute(XUTF8Str("email").unicodeForm()); + name = StrXUTF8(e->getTextContent()).str; + email = StrXUTF8(emailAttribute).str; +} + +Meta::License::License(const std::string& name, fs::path file) : + name(name), + file(file) +{ + // This has to be provided manually since we have another constructor +} + +Meta::License::License(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + auto fileAttribute = e->getAttribute(XUTF8Str("file").unicodeForm()); + if (XMLString::stringLen(fileAttribute) > 0) { + file = fs::path(StrXUTF8(fileAttribute).str); + } + name = StrXUTF8(e->getTextContent()).str; +} + +Meta::Url::Url(const std::string& location, UrlType type) : + location(location), + type(type) +{ + // This has to be provided manually since we have another constructor +} + +Meta::Url::Url(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + auto typeAttribute = StrXUTF8(e->getAttribute(XUTF8Str("type").unicodeForm())).str; + if (typeAttribute.empty() || typeAttribute == "website") + type = UrlType::website; + else if (typeAttribute == "bugtracker") + type = UrlType::bugtracker; + else if (typeAttribute == "repository") + type = UrlType::repository; + else if (typeAttribute == "readme") + type = UrlType::readme; + else if (typeAttribute == "documentation") + type = UrlType::documentation; + location = StrXUTF8(e->getTextContent()).str; +} + +Meta::Dependency::Dependency(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + version_lt = StrXUTF8(e->getAttribute(XUTF8Str("version_lt").unicodeForm())).str; + version_lte = StrXUTF8(e->getAttribute(XUTF8Str("version_lte").unicodeForm())).str; + version_eq = StrXUTF8(e->getAttribute(XUTF8Str("version_eq").unicodeForm())).str; + version_gte = StrXUTF8(e->getAttribute(XUTF8Str("version_gte").unicodeForm())).str; + version_gt = StrXUTF8(e->getAttribute(XUTF8Str("version_gt").unicodeForm())).str; + condition = StrXUTF8(e->getAttribute(XUTF8Str("condition").unicodeForm())).str; + + package = StrXUTF8(e->getTextContent()).str; +} + +Meta::Version::Version() : + major(0), + minor(0), + patch(0) +{ +} + +Meta::Version::Version(int major, int minor, int patch, const std::string& suffix) : + major(major), + minor(minor), + patch(patch), + suffix(suffix) +{ + +} + +Meta::Version::Version(const std::string& versionString) : + minor(0), + patch(0) +{ + std::istringstream stream(versionString); + char separator; + stream >> major; + if (stream) stream >> separator; + if (stream) stream >> minor; + if (stream) stream >> separator; + if (stream) stream >> patch; + if (stream) stream >> suffix; +} + +std::string Meta::Version::str() const +{ + std::ostringstream stream; + stream << major << "." << minor << "." << patch << suffix; + return stream.str(); +} + +bool Meta::Version::operator<(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) < std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator>(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) > std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator<=(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) <= std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator>=(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) >= std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator==(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) == std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +bool Meta::Version::operator!=(const Version& rhs) const +{ + return std::tie(major, minor, patch, suffix) != std::tie(rhs.major, rhs.minor, rhs.patch, rhs.suffix); +} + +Meta::GenericMetadata::GenericMetadata(const XERCES_CPP_NAMESPACE::DOMElement* e) +{ + contents = StrXUTF8(e->getTextContent()).str; + for (XMLSize_t i = 0; i < e->getAttributes()->getLength(); ++i) { + auto a = e->getAttributes()->item(i); + attributes.insert(std::make_pair(StrXUTF8(a->getNodeName()).str, + StrXUTF8(a->getTextContent()).str)); + } +} + +App::Meta::GenericMetadata::GenericMetadata(const std::string& contents) : + contents(contents) +{ +} diff --git a/src/App/Metadata.h b/src/App/Metadata.h new file mode 100644 index 0000000000..de8be3ea8e --- /dev/null +++ b/src/App/Metadata.h @@ -0,0 +1,299 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file LICENSE.html. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#ifndef BASE_METADATAREADER_H +#define BASE_METADATAREADER_H + +#include "FCConfig.h" + +#include + +#include +#include +#include + +#include +#include + + +namespace App { + + namespace Meta { + + /** + * \struct Contact + * \brief A person or company representing a point of contact for the package (either author or maintainer). + */ + struct AppExport Contact { + Contact() = default; + Contact(const std::string& name, const std::string& email); + explicit Contact(const XERCES_CPP_NAMESPACE::DOMElement* e); + std::string name; //< Contact name - required + std::string email; //< Contact email - may be optional + }; + + /** + * \struct License + * \brief A license that covers some or all of this package. + * + * Many licenses also require the inclusion of the complete license text, specified in this struct + * using the "file" member. + */ + struct AppExport License { + License() = default; + License(const std::string& name, boost::filesystem::path file); + explicit License(const XERCES_CPP_NAMESPACE::DOMElement* e); + std::string name; //< Short name of license, e.g. "LGPL2", "MIT", "Mozilla Public License", etc. + boost::filesystem::path file; //< Optional path to the license file, relative to the XML file's location + }; + + enum class UrlType { + website, + repository, + bugtracker, + readme, + documentation + }; + + /** + * \struct Url + * \brief A URL, including type information (e.g. website, repository, or bugtracker, in package.xml v3) + */ + struct AppExport Url { + Url() = default; + Url(const std::string& location, UrlType type); + explicit Url(const XERCES_CPP_NAMESPACE::DOMElement* e); + std::string location; //< The actual URL, including protocol + UrlType type; //< What kind of URL this is + }; + + /** + * \struct Version + * A semantic version structure providing comparison operators and conversion to and from std::string + */ + struct AppExport Version { + Version(); + Version(int major, int minor = 0, int patch = 0, const std::string& suffix = std::string()); + explicit Version(const std::string& semanticString); + + int major; + int minor; + int patch; + std::string suffix; + + std::string str() const; + + bool operator<(const Version&) const; + bool operator>(const Version&) const; + bool operator<=(const Version&) const; + bool operator>=(const Version&) const; + bool operator==(const Version&) const; + bool operator!=(const Version&) const; + }; + + /** + * \struct Dependency + * \brief Another package that this package depends on, conflicts with, or replaces + */ + struct AppExport Dependency { + Dependency() = default; + explicit Dependency(const XERCES_CPP_NAMESPACE::DOMElement* e); + std::string package; //< Required: must exactly match the contents of the "name" element in the referenced package's package.xml file. + std::string version_lt; //< Optional: The dependency to the package is restricted to versions less than the stated version number. + std::string version_lte; //< Optional: The dependency to the package is restricted to versions less or equal than the stated version number. + std::string version_eq; //< Optional: The dependency to the package is restricted to a version equal than the stated version number. + std::string version_gte; //< Optional: The dependency to the package is restricted to versions greater or equal than the stated version number. + std::string version_gt; //< Optional: The dependency to the package is restricted to versions greater than the stated version number. + std::string condition; //< Optional: Conditional expression as documented in REP149. + }; + + /** + * \struct GenericMetadata + * A structure to hold unrecognized single-level metadata. + * + * Most unrecognized metadata is simple: when parsing the XML, if the parser finds a tag it + * does not recognize, and that tag has no children, it is parsed into this data structure + * for convenient access by client code. + */ + struct AppExport GenericMetadata { + GenericMetadata() = default; + explicit GenericMetadata(const XERCES_CPP_NAMESPACE::DOMElement* e); + explicit GenericMetadata(const std::string &contents); + std::string contents; //< The contents of the tag + std::map attributes; //< The XML attributes of the tag + }; + + } + + /** + * \class Metadata + * \brief Reads data from a metadata file. + * + * The metadata format is based on https://ros.org/reps/rep-0149.html, modified for FreeCAD + * use. Full format documentation is available at the FreeCAD Wiki: + * https://wiki.freecadweb.org/Package_Metadata + */ + class AppExport Metadata { + public: + + Metadata(); + + /** + * Read the data from a file on disk + * + * This constructor takes a path to an XML file and loads the XML from that file as + * metadata. + */ + explicit Metadata(const boost::filesystem::path& metadataFile); + + /** + * Construct a Metadata object from a DOM node. + * + * This node may have any tag name: it is only accessed via its children, which are + * expected to follow the standard Metadata format for the contents of the element. + */ + Metadata(const XERCES_CPP_NAMESPACE::DOMNode* domNode, int format); + + ~Metadata(); + + + ////////////////////////////////////////////////////////////// + // Recognized Metadata + ////////////////////////////////////////////////////////////// + + std::string name() const; //< A short name for this package, often used as a menu entry. + Meta::Version version() const; //< Version string in symantic triplet format, e.g. "1.2.3". + std::string description() const; //< Text-only description of the package. No markup. + std::vector maintainer() const; //< Must be at least one, and must specify an email address. + std::vector license() const; //< Must be at least one, and most licenses require including a license file. + std::vector url() const; //< Any number of URLs may be specified, but at least one repository URL must be included at the package level. + std::vector author() const; //< Any number of authors may be specified, and email addresses are optional. + std::vector depend() const; //< Zero or more packages this package requires prior to use. + std::vector conflict() const; //< Zero of more packages this package conflicts with. + std::vector replace() const; //< Zero or more packages this package is intended to replace. + std::vector tag() const; //< Zero or more text tags related to this package. + boost::filesystem::path icon() const; //< Path to an icon file. + std::string classname() const; //< Recognized for convenience -- generally only used by Workbenches. + std::vector file() const; //< Arbitrary files associated with this package or content item. + Meta::Version freecadmin() const; //< The minimum FreeCAD version. + Meta::Version freecadmax() const; //< The maximum FreeCAD version. + + /** + * Access the metadata for the content elements of this package + * + * In addition to the overall package metadata, this class reads in metadata contained in a + * element. Each entry in the content element is an element representing some + * type of package content (e.g. add-on, macro, theme, etc.). This class places no restriction + * on the types, it is up to client code to place requirements on the metadata included + * here. + * + * For example, themes might be specified: + * + * + * High Contrast + * + * + */ + std::multimap content() const; + + /** + * Convenience accessor for unrecognized simple metadata. + * + * If the XML parser encounters tags that it does not recognize, and those tags have + * no children, a GenericMetadata object is created. Those objects can be accessed using + * operator[], which returns a (potentially empty) vector containing all instances of the + * given tag. It cannot be used to *create* a new tag, however. See addGenericMetadata(). + */ + std::vector operator[] (const std::string& tag) const; + + /** + * Directly access the DOM tree to support unrecognized multi-level metadata + */ + XERCES_CPP_NAMESPACE::DOMElement* dom() const; + + + // Setters + void setName(const std::string& name); + void setVersion(const Meta::Version& version); + void setDescription(const std::string& description); + void addMaintainer(const Meta::Contact& maintainer); + void addLicense(const Meta::License& license); + void addUrl(const Meta::Url& url); + void addAuthor(const Meta::Contact& author); + void addDepend(const Meta::Dependency& dep); + void addConflict(const Meta::Dependency& dep); + void addReplace(const Meta::Dependency& dep); + void addTag(const std::string& tag); + void setIcon(const boost::filesystem::path& path); + void setClassname(const std::string& name); + void addFile(const boost::filesystem::path& path); + void addContentItem(const std::string& tag, const Metadata& item); + void setFreeCADMin(const Meta::Version& version); + void setFreeCADMax(const Meta::Version& version); + void addGenericMetadata(const std::string& tag, const Meta::GenericMetadata& genericMetadata); + + /** + * Write the metadata to an XML file + */ + void write(const boost::filesystem::path& file) const; + + /** + * Determine whether this package satisfies the given dependency + */ + bool satisfies(const Meta::Dependency&); + + private: + + std::string _name; + Meta::Version _version; + std::string _description; + std::vector _maintainer; + std::vector _license; + std::vector _url; + std::vector _author; + std::vector _depend; + std::vector _conflict; + std::vector _replace; + std::vector _tag; + boost::filesystem::path _icon; + std::string _classname; + std::vector _file; + Meta::Version _freecadmin; + Meta::Version _freecadmax; + + std::multimap _content; + + std::multimap _genericMetadata; + + XERCES_CPP_NAMESPACE::DOMElement* _dom; + std::shared_ptr _parser; + + void parseVersion1(const XERCES_CPP_NAMESPACE::DOMNode* startNode); + void parseContentNodeVersion1(const XERCES_CPP_NAMESPACE::DOMElement* contentNode); + + void appendToElement(XERCES_CPP_NAMESPACE::DOMElement* root) const; + + }; + +} + +#endif \ No newline at end of file diff --git a/src/App/MetadataPy.xml b/src/App/MetadataPy.xml new file mode 100644 index 0000000000..8b211c59a0 --- /dev/null +++ b/src/App/MetadataPy.xml @@ -0,0 +1,156 @@ + + + + + + + + Metadata + A Metadata object reads an XML-formatted package metadata file and provides read-only access to its contents. + + A single constructor is supported: + Metadata(file) -- Reads the XML file and provides access to the metadata it specifies. + + Metadata + + + + + String: the name of this item + + + + + + String: the version of this item in semantic triplet format + + + + + + String: the description of this item (text only, no markup allowed) + + + + + + List of maintainer objects with 'name' and 'email' string attributes + + + + + + List of applicable licenses as objects with 'name' and 'file' string attributes + + + + + + + List of URLs as objects with 'location' and 'urltype' string attributes, where urltype is one of: + * website + * repository + * bugtracker + * readme + * documentation + + + + + + + List of author objects, each with a 'name' and a (potentially empty) 'email' string attribute + + + + + + + List of dependencies, as objects with the following attributes: + * package -- Required: must exactly match the contents of the 'name' element in the referenced package's package.xml file + * version_lt -- Optional: The dependency to the package is restricted to versions less than the stated version number + * version_lte -- Optional: The dependency to the package is restricted to versions less or equal than the stated version number + * version_eq -- Optional: The dependency to the package is restricted to a version equal than the stated version number + * version_gte -- Optional: The dependency to the package is restricted to versions greater or equal than the stated version number + * version_gt -- Optional: The dependency to the package is restricted to versions greater than the stated version number + * condition -- Optional: Conditional expression as documented in REP149 + + + + + + + List of conflicts, format identical to dependencies + + + + + + List of things this item is considered by its author to replace: format identical to dependencies + + + + + + List of strings + + + + + + Relative path to an icon file + + + + + + String: the name of the main Python class this item creates/represents + + + + + + A list of files associated with this item -- the meaning of each file is implementation-defined + + + + + + A dictionary of lists of content items: defined recursively, each item is itself a Metadata object -- see package.xml file format documentation for details + + + + + + + + + getGenericMetadata(name) + Get the list of GenericMetadata objects with key 'name'. Generic metadata objects are Python objects with + a string 'contents' and a dictionary of strings, 'attributes'. They represent unrecognized simple XML tags + in the metadata file. + + + + + + public: + MetadataPy(const Metadata & pla, PyTypeObject *T = &Type) + :PyObjectBase(new Metadata(pla),T){} + Metadata value() const + { return *(getMetadataPtr()); } + + + diff --git a/src/App/MetadataPyImp.cpp b/src/App/MetadataPyImp.cpp new file mode 100644 index 0000000000..32a83a26a7 --- /dev/null +++ b/src/App/MetadataPyImp.cpp @@ -0,0 +1,285 @@ +/*************************************************************************** + * Copyright (c) 2021 Chris Hennes * + * * + * This file is part of the FreeCAD CAx development system. * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Library General Public * + * License as published by the Free Software Foundation; either * + * version 2 of the License, or (at your option) any later version. * + * * + * This library is distributed in the hope that it will be useful, * + * but WITHOUT ANY WARRANTY; without even the implied warranty of * + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * + * GNU Library General Public License for more details. * + * * + * You should have received a copy of the GNU Library General Public * + * License along with this library; see the file LICENSE.html. If not, * + * write to the Free Software Foundation, Inc., 59 Temple Place, * + * Suite 330, Boston, MA 02111-1307, USA * + * * + ***************************************************************************/ + +#include "PreCompiled.h" + +#include "Metadata.h" + +// inclusion of the generated files (generated out of MetadataPy.xml) +#include "MetadataPy.h" +#include "MetadataPy.cpp" + +using namespace Base; + +// Returns a string which represents the object e.g. when printed in Python +std::string MetadataPy::representation(void) const +{ + MetadataPy::PointerType ptr = reinterpret_cast(_pcTwinPointer); + std::stringstream str; + str << "Metadata [Name=("; + str << ptr->name(); + str << "), Description=("; + str << ptr->description(); + str << "), Maintainer=("; + str << ptr->maintainer().front().name << ")]"; + + return str.str(); +} + +PyObject* MetadataPy::PyMake(struct _typeobject*, PyObject* args, PyObject*) // Python wrapper +{ + // create a new instance of MetadataPy and the Twin object + const char* filename; + if (!PyArg_ParseTuple(args, "s!", &filename)) + return nullptr; + + return new MetadataPy(new Metadata(filename)); +} + +// constructor method +int MetadataPy::PyInit(PyObject* args, PyObject* /*kwd*/) +{ + if (PyArg_ParseTuple(args, "")) { + return 0; + } + + // Main class constructor -- takes a file path, loads the metadata from it + PyErr_Clear(); + const char* file; + if (PyArg_ParseTuple(args, "s!", &file)) { + App::Metadata* a = new Metadata(file); + *(getMetadataPtr()) = *a; + return 0; + } + + // Copy constructor + PyErr_Clear(); + PyObject* o; + if (PyArg_ParseTuple(args, "O!", &(App::MetadataPy::Type), &o)) { + App::Metadata* a = static_cast(o)->getMetadataPtr(); + *(getMetadataPtr()) = *a; + return 0; + } + + PyErr_SetString(Base::BaseExceptionFreeCADError, "path to metadata file expected"); + return -1; +} + +Py::Object MetadataPy::getName(void) const +{ + return Py::String(getMetadataPtr()->name()); +} + +Py::Object MetadataPy::getVersion(void) const +{ + return Py::String(getMetadataPtr()->version().str()); +} + +Py::Object MetadataPy::getDescription(void) const +{ + return Py::String(getMetadataPtr()->description()); +} + +Py::Object MetadataPy::getMaintainer(void) const +{ + auto maintainers = getMetadataPtr()->maintainer(); + Py::List pyMaintainers; + for (const auto& m : maintainers) { + Py::Object pyMaintainer; + pyMaintainer.setAttr("name", Py::String(m.name)); + pyMaintainer.setAttr("email", Py::String(m.email)); + pyMaintainers.append(pyMaintainer); + } + return pyMaintainers; +} + +Py::Object MetadataPy::getAuthor(void) const +{ + auto authors = getMetadataPtr()->author(); + Py::List pyAuthors; + for (const auto& a : authors) { + Py::Object pyAuthor; + pyAuthor.setAttr("name", Py::String(a.name)); + pyAuthor.setAttr("email", Py::String(a.email)); + pyAuthors.append(pyAuthor); + } + return pyAuthors; +} + +Py::Object MetadataPy::getLicense(void) const +{ + auto licenses = getMetadataPtr()->license(); + Py::List pyLicenses; + for (const auto& lic : licenses) { + Py::Object pyLicense; + pyLicense.setAttr("name", Py::String(lic.name)); + pyLicense.setAttr("file", Py::String(lic.file.string())); + pyLicenses.append(pyLicense); + } + return pyLicenses; +} + +Py::Object MetadataPy::getUrl(void) const +{ + auto urls = getMetadataPtr()->url (); + Py::List pyUrls; + for (const auto& url : urls) { + Py::Object pyUrl; + pyUrl.setAttr("location", Py::String(url.location)); + switch (url.type) { + case Meta::UrlType::website: pyUrl.setAttr("type", Py::String("website")); break; + case Meta::UrlType::repository: pyUrl.setAttr("type", Py::String("repository")); break; + case Meta::UrlType::bugtracker: pyUrl.setAttr("type", Py::String("bugtracker")); break; + case Meta::UrlType::readme: pyUrl.setAttr("type", Py::String("readme")); break; + case Meta::UrlType::documentation: pyUrl.setAttr("type", Py::String("documentation")); break; + } + pyUrls.append(pyUrl); + } + return pyUrls; +} + +Py::Object dependencyToPyObject(const Meta::Dependency& d) +{ + Py::Object pyDependency; + pyDependency.setAttr("package",Py::String(d.package)); + pyDependency.setAttr("version_lt", Py::String(d.version_lt)); + pyDependency.setAttr("version_lte", Py::String(d.version_lte)); + pyDependency.setAttr("version_eq", Py::String(d.version_eq)); + pyDependency.setAttr("version_gt", Py::String(d.version_gt)); + pyDependency.setAttr("version_gte", Py::String(d.version_gte)); + pyDependency.setAttr("condition", Py::String(d.condition)); + return pyDependency; +} + +Py::Object MetadataPy::getDepend(void) const +{ + auto dependencies = getMetadataPtr()->depend(); + Py::List pyDependencies; + for (const auto& d : dependencies) { + pyDependencies.append(dependencyToPyObject(d)); + } + return pyDependencies; +} + +Py::Object MetadataPy::getConflict(void) const +{ + auto dependencies = getMetadataPtr()->conflict(); + Py::List pyDependencies; + for (const auto& d : dependencies) { + pyDependencies.append(dependencyToPyObject(d)); + } + return pyDependencies; +} + +Py::Object MetadataPy::getReplace(void) const +{ + auto dependencies = getMetadataPtr()->replace(); + Py::List pyDependencies; + for (const auto& d : dependencies) { + pyDependencies.append(dependencyToPyObject(d)); + } + return pyDependencies; +} + +// Tag, icon, classname, file + +Py::Object MetadataPy::getTag(void) const +{ + auto tags = getMetadataPtr()->tag(); + Py::List pyTags; + for (const auto& t : tags) { + pyTags.append(Py::String(t)); + } + return pyTags; +} + +Py::Object MetadataPy::getIcon(void) const +{ + return Py::String(getMetadataPtr()->icon().string()); +} + +Py::Object MetadataPy::getClassname(void) const +{ + return Py::String(getMetadataPtr()->classname()); +} + +Py::Object MetadataPy::getFile(void) const +{ + auto files = getMetadataPtr()->file(); + Py::List pyFiles; + for (const auto& f : files) { + pyFiles.append(Py::String(f.string())); + } + return pyFiles; +} + +Py::Object MetadataPy::getContent(void) const +{ + auto content = getMetadataPtr()->content(); + std::set keys; + for (const auto& item : content) { + keys.insert(item.first); + } + + // For the Python, we'll use a dictionary of lists to store the content components: + Py::Dict pyContent; + for (const auto& key : keys) { + Py::List pyContentForKey; + auto elements = content.equal_range(key); + for (auto element = elements.first; element != elements.second; ++element) { + auto contentMetadataItem = new MetadataPy(new Metadata(element->second)); + pyContentForKey.append(Py::asObject(contentMetadataItem)); + } + pyContent[key] = pyContentForKey; + } + return pyContent; +} + +PyObject* MetadataPy::getGenericMetadata(PyObject* args) +{ + const char* name; + if (!PyArg_ParseTuple(args, "s!", &name)) + return NULL; + auto gm = (*getMetadataPtr())[name]; + auto pyGenericMetadata = new Py::List; + for (const auto& item : gm) { + Py::Object pyItem; + pyItem.setAttr("contents", Py::String(item.contents)); + Py::Dict pyAttributes; + for (const auto& attribute : item.attributes) { + pyAttributes[attribute.first] = Py::String(attribute.second); + } + pyItem.setAttr("attributes", pyAttributes); + pyGenericMetadata->append(pyItem); + } + return pyGenericMetadata->ptr(); +} + +PyObject* MetadataPy::getCustomAttributes(const char* /*attr*/) const +{ + return 0; +} + +int MetadataPy::setCustomAttributes(const char* /*attr*/, PyObject* /*obj*/) +{ + return 0; +} \ No newline at end of file