[App] Add metadata handling class

This commit is contained in:
Chris Hennes
2021-04-19 12:19:23 -05:00
parent 91b0409c48
commit 30fd2ce5b8
5 changed files with 1495 additions and 0 deletions

View File

@@ -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

750
src/App/Metadata.cpp Normal file
View File

@@ -0,0 +1,750 @@
/***************************************************************************
* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
* *
* 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 <memory>
#endif
#include "Metadata.h"
#include <xercesc/sax/HandlerBase.hpp>
#include <xercesc/util/XMLString.hpp>
#include <xercesc/util/PlatformUtils.hpp>
#include <xercesc/util/PlatformUtils.hpp>
#include <xercesc/dom/DOM.hpp>
#include <xercesc/parsers/XercesDOMParser.hpp>
#include <xercesc/dom/DOMImplementation.hpp>
#include <xercesc/dom/DOMImplementationLS.hpp>
#include <xercesc/framework/LocalFileFormatTarget.hpp>
#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 <sys/sysmacros.h>. For historical compatibility, it is
currently defined by <sys/types.h> as well, but we plan to
remove this soon. To use "major", include <sys/sysmacros.h>
directly. If you did not intend to use a system-defined macro
"major", you should undefine it after including <sys/types.h>.
*/
#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<XercesDOMParser>();
_parser->setValidationScheme(XercesDOMParser::Val_Never);
_parser->setDoNamespaces(true);
auto errHandler = std::make_unique<HandlerBase>();
_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 <package> 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<const DOMElement*>(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<Meta::Contact> Metadata::maintainer() const
{
return _maintainer;
}
std::vector<Meta::License> Metadata::license() const
{
return _license;
}
std::vector<Meta::Url> Metadata::url() const
{
return _url;
}
std::vector<Meta::Contact> Metadata::author() const
{
return _author;
}
std::vector<Meta::Dependency> Metadata::depend() const
{
return _depend;
}
std::vector<Meta::Dependency> Metadata::conflict() const
{
return _conflict;
}
std::vector<Meta::Dependency> Metadata::replace() const
{
return _replace;
}
std::vector<std::string> Metadata::tag() const
{
return _tag;
}
fs::path Metadata::icon() const
{
return _icon;
}
std::string Metadata::classname() const
{
return _classname;
}
std::vector<fs::path> Metadata::file() const
{
return _file;
}
Meta::Version App::Metadata::freecadmin() const
{
return _freecadmin;
}
Meta::Version App::Metadata::freecadmax() const
{
return _freecadmax;
}
std::multimap<std::string, Metadata> Metadata::content() const
{
return _content;
}
std::vector<Meta::GenericMetadata> Metadata::operator[](const std::string& tag) const
{
std::vector<Meta::GenericMetadata> 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<std::string, std::string> replacements;
std::map<std::string, std::string>& 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<bool> (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<const DOMElement*>(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<const DOMElement*>(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)
{
}

299
src/App/Metadata.h Normal file
View File

@@ -0,0 +1,299 @@
/***************************************************************************
* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
* *
* 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 <boost/filesystem.hpp>
#include <string>
#include <vector>
#include <map>
#include <xercesc/dom/DOM.hpp>
#include <xercesc/parsers/XercesDOMParser.hpp>
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<std::string, std::string> 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 <package> 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<Meta::Contact> maintainer() const; //< Must be at least one, and must specify an email address.
std::vector<Meta::License> license() const; //< Must be at least one, and most licenses require including a license file.
std::vector<Meta::Url> url() const; //< Any number of URLs may be specified, but at least one repository URL must be included at the package level.
std::vector<Meta::Contact> author() const; //< Any number of authors may be specified, and email addresses are optional.
std::vector<Meta::Dependency> depend() const; //< Zero or more packages this package requires prior to use.
std::vector<Meta::Dependency> conflict() const; //< Zero of more packages this package conflicts with.
std::vector<Meta::Dependency> replace() const; //< Zero or more packages this package is intended to replace.
std::vector<std::string> 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<boost::filesystem::path> 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
* <content> 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:
* <content>
* <theme>
* <name>High Contrast</name>
* </theme>
* </content>
*/
std::multimap<std::string, Metadata> 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<Meta::GenericMetadata> 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<Meta::Contact> _maintainer;
std::vector<Meta::License> _license;
std::vector<Meta::Url> _url;
std::vector<Meta::Contact> _author;
std::vector<Meta::Dependency> _depend;
std::vector<Meta::Dependency> _conflict;
std::vector<Meta::Dependency> _replace;
std::vector<std::string> _tag;
boost::filesystem::path _icon;
std::string _classname;
std::vector<boost::filesystem::path> _file;
Meta::Version _freecadmin;
Meta::Version _freecadmax;
std::multimap<std::string, Metadata> _content;
std::multimap<std::string, Meta::GenericMetadata> _genericMetadata;
XERCES_CPP_NAMESPACE::DOMElement* _dom;
std::shared_ptr<XERCES_CPP_NAMESPACE::XercesDOMParser> _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

156
src/App/MetadataPy.xml Normal file
View File

@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="UTF-8"?>
<GenerateModel xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="generateMetaModel_Module.xsd">
<PythonExport
Father="PyObjectBase"
Name="MetadataPy"
Twin="Metadata"
TwinPointer="Metadata"
Include="App/Metadata.h"
FatherInclude="Base/PyObjectBase.h"
Namespace="App"
Constructor="true"
Delete="true"
NumberProtocol="false"
RichCompare="false"
FatherNamespace="Base">
<Documentation>
<Author Licence="LGPL" Name="Chris Hennes" EMail="chennes@pioneerlibrarysystem.org" />
<UserDocu>
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.
</UserDocu>
<DeveloperDocu>Metadata</DeveloperDocu>
</Documentation>
<Attribute Name="Name" ReadOnly="true">
<Documentation>
<UserDocu>String: the name of this item</UserDocu>
</Documentation>
<Parameter Name="Name" Type="Object" />
</Attribute>
<Attribute Name="Version" ReadOnly="true">
<Documentation>
<UserDocu>String: the version of this item in semantic triplet format</UserDocu>
</Documentation>
<Parameter Name="Version" Type="Object" />
</Attribute>
<Attribute Name="Description" ReadOnly="true">
<Documentation>
<UserDocu>String: the description of this item (text only, no markup allowed)</UserDocu>
</Documentation>
<Parameter Name="Description" Type="Object" />
</Attribute>
<Attribute Name="Maintainer" ReadOnly="true">
<Documentation>
<UserDocu>List of maintainer objects with 'name' and 'email' string attributes</UserDocu>
</Documentation>
<Parameter Name="Maintainer" Type="Object" />
</Attribute>
<Attribute Name="License" ReadOnly="true">
<Documentation>
<UserDocu>List of applicable licenses as objects with 'name' and 'file' string attributes</UserDocu>
</Documentation>
<Parameter Name="License" Type="Object" />
</Attribute>
<Attribute Name="Url" ReadOnly="true">
<Documentation>
<UserDocu>
List of URLs as objects with 'location' and 'urltype' string attributes, where urltype is one of:
* website
* repository
* bugtracker
* readme
* documentation
</UserDocu>
</Documentation>
<Parameter Name="Url" Type="Object" />
</Attribute>
<Attribute Name="Author" ReadOnly="true">
<Documentation>
<UserDocu>List of author objects, each with a 'name' and a (potentially empty) 'email' string attribute</UserDocu>
</Documentation>
<Parameter Name="Author" Type="Object" />
</Attribute>
<Attribute Name="Depend" ReadOnly="true">
<Documentation>
<UserDocu>
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
</UserDocu>
</Documentation>
<Parameter Name="Depend" Type="Object" />
</Attribute>
<Attribute Name="Conflict" ReadOnly="true">
<Documentation>
<UserDocu>List of conflicts, format identical to dependencies</UserDocu>
</Documentation>
<Parameter Name="Conflict" Type="Object" />
</Attribute>
<Attribute Name="Replace" ReadOnly="true">
<Documentation>
<UserDocu>List of things this item is considered by its author to replace: format identical to dependencies</UserDocu>
</Documentation>
<Parameter Name="Replace" Type="Object" />
</Attribute>
<Attribute Name="Tag" ReadOnly="true">
<Documentation>
<UserDocu>List of strings</UserDocu>
</Documentation>
<Parameter Name="Tag" Type="Object" />
</Attribute>
<Attribute Name="Icon" ReadOnly="true">
<Documentation>
<UserDocu>Relative path to an icon file</UserDocu>
</Documentation>
<Parameter Name="Icon" Type="Object" />
</Attribute>
<Attribute Name="Classname" ReadOnly="true">
<Documentation>
<UserDocu>String: the name of the main Python class this item creates/represents</UserDocu>
</Documentation>
<Parameter Name="Classname" Type="Object" />
</Attribute>
<Attribute Name="File" ReadOnly="true">
<Documentation>
<UserDocu>A list of files associated with this item -- the meaning of each file is implementation-defined</UserDocu>
</Documentation>
<Parameter Name="File" Type="Object" />
</Attribute>
<Attribute Name="Content" ReadOnly="true">
<Documentation>
<UserDocu>A dictionary of lists of content items: defined recursively, each item is itself a Metadata object -- see package.xml file format documentation for details</UserDocu>
</Documentation>
<Parameter Name="Content" Type="Object" />
</Attribute>
<Methode Name="getGenericMetadata">
<Documentation>
<UserDocu>
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.
</UserDocu>
</Documentation>
</Methode>
<ClassDeclarations>
public:
MetadataPy(const Metadata &amp; pla, PyTypeObject *T = &amp;Type)
:PyObjectBase(new Metadata(pla),T){}
Metadata value() const
{ return *(getMetadataPtr()); }
</ClassDeclarations>
</PythonExport>
</GenerateModel>

285
src/App/MetadataPyImp.cpp Normal file
View File

@@ -0,0 +1,285 @@
/***************************************************************************
* Copyright (c) 2021 Chris Hennes <chennes@pioneerlibrarysystem.org> *
* *
* 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<MetadataPy::PointerType>(_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<App::MetadataPy*>(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<std::string> 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;
}