Add-ons: move parsing of the metadata file to a separate translation unit
This will be tidier, esp. since I am going to introduce another XML config file.
This commit is contained in:
parent
4ed99dd05c
commit
a137aed541
6 changed files with 510 additions and 367 deletions
|
@ -18,36 +18,26 @@
|
||||||
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <regex>
|
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <tuple>
|
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <cassert>
|
|
||||||
|
|
||||||
#include <simgear/debug/logstream.hxx>
|
|
||||||
#include <simgear/misc/sg_path.hxx>
|
#include <simgear/misc/sg_path.hxx>
|
||||||
#include <simgear/misc/strutils.hxx>
|
|
||||||
#include <simgear/nasal/cppbind/Ghost.hxx>
|
#include <simgear/nasal/cppbind/Ghost.hxx>
|
||||||
#include <simgear/nasal/cppbind/NasalHash.hxx>
|
#include <simgear/nasal/cppbind/NasalHash.hxx>
|
||||||
#include <simgear/nasal/naref.h>
|
#include <simgear/nasal/naref.h>
|
||||||
#include <simgear/props/props.hxx>
|
#include <simgear/props/props.hxx>
|
||||||
#include <simgear/props/props_io.hxx>
|
|
||||||
|
|
||||||
#include <Main/globals.hxx>
|
#include <Main/globals.hxx>
|
||||||
#include <Scripting/NasalSys.hxx>
|
#include <Scripting/NasalSys.hxx>
|
||||||
|
|
||||||
#include "addon_fwd.hxx"
|
#include "addon_fwd.hxx"
|
||||||
#include "Addon.hxx"
|
#include "Addon.hxx"
|
||||||
|
#include "AddonMetadataParser.hxx"
|
||||||
#include "AddonVersion.hxx"
|
#include "AddonVersion.hxx"
|
||||||
#include "contacts.hxx"
|
|
||||||
#include "exceptions.hxx"
|
|
||||||
#include "pointer_traits.hxx"
|
#include "pointer_traits.hxx"
|
||||||
|
|
||||||
namespace strutils = simgear::strutils;
|
|
||||||
|
|
||||||
using std::string;
|
using std::string;
|
||||||
using std::vector;
|
using std::vector;
|
||||||
|
|
||||||
|
@ -232,6 +222,12 @@ std::string Addon::getCodeRepositoryUrl() const
|
||||||
void Addon::setCodeRepositoryUrl(const std::string& addonCodeRepositoryUrl)
|
void Addon::setCodeRepositoryUrl(const std::string& addonCodeRepositoryUrl)
|
||||||
{ _codeRepositoryUrl = addonCodeRepositoryUrl; }
|
{ _codeRepositoryUrl = addonCodeRepositoryUrl; }
|
||||||
|
|
||||||
|
std::string Addon::getTriggerProperty() const
|
||||||
|
{ return _triggerProperty; }
|
||||||
|
|
||||||
|
void Addon::setTriggerProperty(const std::string& addonTriggerProperty)
|
||||||
|
{ _triggerProperty = addonTriggerProperty; }
|
||||||
|
|
||||||
SGPropertyNode_ptr Addon::getAddonNode() const
|
SGPropertyNode_ptr Addon::getAddonNode() const
|
||||||
{ return _addonNode; }
|
{ return _addonNode; }
|
||||||
|
|
||||||
|
@ -269,7 +265,7 @@ std::string Addon::str() const
|
||||||
// Static method
|
// Static method
|
||||||
SGPath Addon::getMetadataFile(const SGPath& addonPath)
|
SGPath Addon::getMetadataFile(const SGPath& addonPath)
|
||||||
{
|
{
|
||||||
return addonPath / "addon-metadata.xml";
|
return MetadataParser::getMetadataFile(addonPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
SGPath Addon::getMetadataFile() const
|
SGPath Addon::getMetadataFile() const
|
||||||
|
@ -280,353 +276,29 @@ SGPath Addon::getMetadataFile() const
|
||||||
// Static method
|
// Static method
|
||||||
Addon Addon::fromAddonDir(const SGPath& addonPath)
|
Addon Addon::fromAddonDir(const SGPath& addonPath)
|
||||||
{
|
{
|
||||||
SGPath metadataFile = getMetadataFile(addonPath);
|
Addon::Metadata metadata = MetadataParser::parseMetadataFile(addonPath);
|
||||||
SGPropertyNode addonRoot;
|
|
||||||
|
|
||||||
if (!metadataFile.exists()) {
|
|
||||||
throw errors::no_metadata_file_found(
|
|
||||||
"unable to find add-on metadata file '" + metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
readProperties(metadataFile, &addonRoot);
|
|
||||||
} catch (const sg_exception &e) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"unable to load add-on metadata file '" + metadataFile.utf8Str() + "': " +
|
|
||||||
e.getFormattedMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the 'meta' section
|
|
||||||
SGPropertyNode *metaNode = addonRoot.getChild("meta");
|
|
||||||
if (metaNode == nullptr) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"no /meta node found in add-on metadata file '" +
|
|
||||||
metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the file type
|
|
||||||
SGPropertyNode *fileTypeNode = metaNode->getChild("file-type");
|
|
||||||
if (fileTypeNode == nullptr) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"no /meta/file-type node found in add-on metadata file '" +
|
|
||||||
metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
string fileType = fileTypeNode->getStringValue();
|
|
||||||
if (fileType != "FlightGear add-on metadata") {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"Invalid /meta/file-type value for add-on metadata file '" +
|
|
||||||
metadataFile.utf8Str() + "': '" + fileType + "' "
|
|
||||||
"(expected 'FlightGear add-on metadata')");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check the format version
|
|
||||||
SGPropertyNode *fmtVersionNode = metaNode->getChild("format-version");
|
|
||||||
if (fmtVersionNode == nullptr) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"no /meta/format-version node found in add-on metadata file '" +
|
|
||||||
metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
int formatVersion = fmtVersionNode->getIntValue();
|
|
||||||
if (formatVersion != 1) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"unknown format version in add-on metadata file '" +
|
|
||||||
metadataFile.utf8Str() + "': " + std::to_string(formatVersion));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now the data we are really interested in
|
|
||||||
SGPropertyNode *addonNode = addonRoot.getChild("addon");
|
|
||||||
if (addonNode == nullptr) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"no /addon node found in add-on metadata file '" +
|
|
||||||
metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
SGPropertyNode *idNode = addonNode->getChild("identifier");
|
|
||||||
if (idNode == nullptr) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"no /addon/identifier node found in add-on metadata file '" +
|
|
||||||
metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
string addonId = strutils::strip(idNode->getStringValue());
|
|
||||||
|
|
||||||
// Require a non-empty identifier for the add-on
|
|
||||||
if (addonId.empty()) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"empty or whitespace-only value for the /addon/identifier node in "
|
|
||||||
"add-on metadata file '" + metadataFile.utf8Str() + "'");
|
|
||||||
} else if (addonId.find('.') == string::npos) {
|
|
||||||
SG_LOG(SG_GENERAL, SG_WARN,
|
|
||||||
"Add-on identifier '" << addonId << "' does not use reverse DNS "
|
|
||||||
"style (e.g., org.flightgear.addons.MyAddon) in add-on metadata "
|
|
||||||
"file '" << metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
SGPropertyNode *nameNode = addonNode->getChild("name");
|
|
||||||
if (nameNode == nullptr) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"no /addon/name node found in add-on metadata file '" +
|
|
||||||
metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
string addonName = strutils::strip(nameNode->getStringValue());
|
|
||||||
|
|
||||||
// Require a non-empty name for the add-on
|
|
||||||
if (addonName.empty()) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"empty or whitespace-only value for the /addon/name node in add-on "
|
|
||||||
"metadata file '" + metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
SGPropertyNode *versionNode = addonNode->getChild("version");
|
|
||||||
if (versionNode == nullptr) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"no /addon/version node found in add-on metadata file '" +
|
|
||||||
metadataFile.utf8Str() + "'");
|
|
||||||
}
|
|
||||||
AddonVersion addonVersion(strutils::strip(versionNode->getStringValue()));
|
|
||||||
|
|
||||||
auto addonAuthors = parseContactsNode<Author>(metadataFile,
|
|
||||||
addonNode->getChild("authors"));
|
|
||||||
auto addonMaintainers = parseContactsNode<Maintainer>(
|
|
||||||
metadataFile, addonNode->getChild("maintainers"));
|
|
||||||
|
|
||||||
string addonShortDescription;
|
|
||||||
SGPropertyNode *shortDescNode = addonNode->getChild("short-description");
|
|
||||||
if (shortDescNode != nullptr) {
|
|
||||||
addonShortDescription = strutils::strip(shortDescNode->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
string addonLongDescription;
|
|
||||||
SGPropertyNode *longDescNode = addonNode->getChild("long-description");
|
|
||||||
if (longDescNode != nullptr) {
|
|
||||||
addonLongDescription = strutils::strip(longDescNode->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
string addonLicenseDesignation, addonLicenseUrl;
|
|
||||||
SGPath addonLicenseFile;
|
|
||||||
std::tie(addonLicenseDesignation, addonLicenseFile, addonLicenseUrl) =
|
|
||||||
parseLicenseNode(addonPath, addonNode);
|
|
||||||
|
|
||||||
vector<string> addonTags;
|
|
||||||
SGPropertyNode *tagsNode = addonNode->getChild("tags");
|
|
||||||
if (tagsNode != nullptr) {
|
|
||||||
auto tagNodes = tagsNode->getChildren("tag");
|
|
||||||
for (const auto& node: tagNodes) {
|
|
||||||
addonTags.push_back(strutils::strip(node->getStringValue()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
string addonMinFGVersionRequired;
|
|
||||||
SGPropertyNode *minNode = addonNode->getChild("min-FG-version");
|
|
||||||
if (minNode != nullptr) {
|
|
||||||
addonMinFGVersionRequired = strutils::strip(minNode->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
string addonMaxFGVersionRequired;
|
|
||||||
SGPropertyNode *maxNode = addonNode->getChild("max-FG-version");
|
|
||||||
if (maxNode != nullptr) {
|
|
||||||
addonMaxFGVersionRequired = strutils::strip(maxNode->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
string addonHomePage, addonDownloadUrl, addonSupportUrl, addonCodeRepoUrl;
|
|
||||||
SGPropertyNode *urlsNode = addonNode->getChild("urls");
|
|
||||||
if (urlsNode != nullptr) {
|
|
||||||
SGPropertyNode *homePageNode = urlsNode->getChild("home-page");
|
|
||||||
if (homePageNode != nullptr) {
|
|
||||||
addonHomePage = strutils::strip(homePageNode->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
SGPropertyNode *downloadUrlNode = urlsNode->getChild("download");
|
|
||||||
if (downloadUrlNode != nullptr) {
|
|
||||||
addonDownloadUrl = strutils::strip(downloadUrlNode->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
SGPropertyNode *supportUrlNode = urlsNode->getChild("support");
|
|
||||||
if (supportUrlNode != nullptr) {
|
|
||||||
addonSupportUrl = strutils::strip(supportUrlNode->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
SGPropertyNode *codeRepoUrlNode = urlsNode->getChild("code-repository");
|
|
||||||
if (codeRepoUrlNode != nullptr) {
|
|
||||||
addonCodeRepoUrl = strutils::strip(codeRepoUrlNode->getStringValue());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object holding all the add-on metadata
|
// Object holding all the add-on metadata
|
||||||
Addon addon{addonId, std::move(addonVersion), addonPath,
|
Addon addon{std::move(metadata.id), std::move(metadata.version), addonPath,
|
||||||
addonMinFGVersionRequired, addonMaxFGVersionRequired};
|
std::move(metadata.minFGVersionRequired),
|
||||||
addon.setName(addonName);
|
std::move(metadata.maxFGVersionRequired)};
|
||||||
addon.setAuthors(addonAuthors);
|
addon.setName(std::move(metadata.name));
|
||||||
addon.setMaintainers(addonMaintainers);
|
addon.setAuthors(std::move(metadata.authors));
|
||||||
addon.setShortDescription(addonShortDescription);
|
addon.setMaintainers(std::move(metadata.maintainers));
|
||||||
addon.setLongDescription(addonLongDescription);
|
addon.setShortDescription(std::move(metadata.shortDescription));
|
||||||
addon.setLicenseDesignation(addonLicenseDesignation);
|
addon.setLongDescription(std::move(metadata.longDescription));
|
||||||
addon.setLicenseFile(addonLicenseFile);
|
addon.setLicenseDesignation(std::move(metadata.licenseDesignation));
|
||||||
addon.setLicenseUrl(addonLicenseUrl);
|
addon.setLicenseFile(std::move(metadata.licenseFile));
|
||||||
addon.setTags(addonTags);
|
addon.setLicenseUrl(std::move(metadata.licenseUrl));
|
||||||
addon.setHomePage(addonHomePage);
|
addon.setTags(std::move(metadata.tags));
|
||||||
addon.setDownloadUrl(addonDownloadUrl);
|
addon.setHomePage(std::move(metadata.homePage));
|
||||||
addon.setSupportUrl(addonSupportUrl);
|
addon.setDownloadUrl(std::move(metadata.downloadUrl));
|
||||||
addon.setCodeRepositoryUrl(addonCodeRepoUrl);
|
addon.setSupportUrl(std::move(metadata.supportUrl));
|
||||||
|
addon.setCodeRepositoryUrl(std::move(metadata.codeRepositoryUrl));
|
||||||
SG_LOG(SG_GENERAL, SG_DEBUG,
|
|
||||||
"Loaded add-on metadata file: '" << metadataFile.utf8Str() + "'");
|
|
||||||
|
|
||||||
return addon;
|
return addon;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility function for Addon::parseContactsNode<>()
|
|
||||||
//
|
|
||||||
// Read a node such as "name", "email" or "url", child of a contact node (e.g.,
|
|
||||||
// of an "author" or "maintainer" node).
|
|
||||||
static string
|
|
||||||
parseContactsNode_readNode(const SGPath& metadataFile,
|
|
||||||
SGPropertyNode* contactNode,
|
|
||||||
string subnodeName, bool allowEmpty)
|
|
||||||
{
|
|
||||||
SGPropertyNode *node = contactNode->getChild(subnodeName);
|
|
||||||
string contents;
|
|
||||||
|
|
||||||
if (node != nullptr) {
|
|
||||||
contents = simgear::strutils::strip(node->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!allowEmpty && contents.empty()) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"in add-on metadata file '" + metadataFile.utf8Str() + "': "
|
|
||||||
"when the node " + contactNode->getPath(true) + " exists, it must have "
|
|
||||||
"a non-empty '" + subnodeName + "' child node");
|
|
||||||
}
|
|
||||||
|
|
||||||
return contents;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Static method template (private and only used in this file)
|
|
||||||
template <class T>
|
|
||||||
vector<typename contact_traits<T>::strong_ref>
|
|
||||||
Addon::parseContactsNode(const SGPath& metadataFile, SGPropertyNode* mainNode)
|
|
||||||
{
|
|
||||||
using contactTraits = contact_traits<T>;
|
|
||||||
vector<typename contactTraits::strong_ref> res;
|
|
||||||
|
|
||||||
if (mainNode != nullptr) {
|
|
||||||
auto contactNodes = mainNode->getChildren(contactTraits::xmlNodeName());
|
|
||||||
res.reserve(contactNodes.size());
|
|
||||||
|
|
||||||
for (const auto& contactNode: contactNodes) {
|
|
||||||
string name, email, url;
|
|
||||||
|
|
||||||
name = parseContactsNode_readNode(metadataFile, contactNode.get(),
|
|
||||||
"name", false /* allowEmpty */);
|
|
||||||
email = parseContactsNode_readNode(metadataFile, contactNode.get(),
|
|
||||||
"email", true);
|
|
||||||
url = parseContactsNode_readNode(metadataFile, contactNode.get(),
|
|
||||||
"url", true);
|
|
||||||
|
|
||||||
using ptr_traits = shared_ptr_traits<typename contactTraits::strong_ref>;
|
|
||||||
res.push_back(ptr_traits::makeStrongRef(name, email, url));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Static method
|
|
||||||
std::tuple<string, SGPath, string>
|
|
||||||
Addon::parseLicenseNode(const SGPath& addonPath, SGPropertyNode* addonNode)
|
|
||||||
{
|
|
||||||
SGPath metadataFile = getMetadataFile(addonPath);
|
|
||||||
string licenseDesignation;
|
|
||||||
SGPath licenseFile;
|
|
||||||
string licenseUrl;
|
|
||||||
|
|
||||||
SGPropertyNode *licenseNode = addonNode->getChild("license");
|
|
||||||
if (licenseNode == nullptr) {
|
|
||||||
return std::tuple<string, SGPath, string>();
|
|
||||||
}
|
|
||||||
|
|
||||||
SGPropertyNode *licenseDesigNode = licenseNode->getChild("designation");
|
|
||||||
if (licenseDesigNode != nullptr) {
|
|
||||||
licenseDesignation = strutils::strip(licenseDesigNode->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
SGPropertyNode *licenseFileNode = licenseNode->getChild("file");
|
|
||||||
if (licenseFileNode != nullptr) {
|
|
||||||
// This effectively disallows filenames starting or ending with whitespace
|
|
||||||
string licenseFile_s = strutils::strip(licenseFileNode->getStringValue());
|
|
||||||
|
|
||||||
if (!licenseFile_s.empty()) {
|
|
||||||
if (licenseFile_s.find('\\') != string::npos) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"in add-on metadata file '" + metadataFile.utf8Str() + "': the "
|
|
||||||
"value of /addon/license/file contains '\\'; please use '/' "
|
|
||||||
"separators only");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (licenseFile_s.find_first_of("/\\") == 0) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"in add-on metadata file '" + metadataFile.utf8Str() + "': the "
|
|
||||||
"value of /addon/license/file must be relative to the add-on folder, "
|
|
||||||
"however it starts with '" + licenseFile_s[0] + "'");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::regex winDriveRegexp("([a-zA-Z]:).*");
|
|
||||||
std::smatch results;
|
|
||||||
|
|
||||||
if (std::regex_match(licenseFile_s, results, winDriveRegexp)) {
|
|
||||||
string winDrive = results.str(1);
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"in add-on metadata file '" + metadataFile.utf8Str() + "': the "
|
|
||||||
"value of /addon/license/file must be relative to the add-on folder, "
|
|
||||||
"however it starts with a Windows drive letter (" + winDrive + ")");
|
|
||||||
}
|
|
||||||
|
|
||||||
licenseFile = addonPath / licenseFile_s;
|
|
||||||
if ( !(licenseFile.exists() && licenseFile.isFile()) ) {
|
|
||||||
throw errors::error_loading_metadata_file(
|
|
||||||
"in add-on metadata file '" + metadataFile.utf8Str() + "': the "
|
|
||||||
"value of /addon/license/file (pointing to '" + licenseFile.utf8Str() +
|
|
||||||
"') doesn't correspond to an existing file");
|
|
||||||
}
|
|
||||||
} // of if (!licenseFile_s.empty())
|
|
||||||
} // of if (licenseFileNode != nullptr)
|
|
||||||
|
|
||||||
SGPropertyNode *licenseUrlNode = licenseNode->getChild("url");
|
|
||||||
if (licenseUrlNode != nullptr) {
|
|
||||||
licenseUrl = strutils::strip(licenseUrlNode->getStringValue());
|
|
||||||
}
|
|
||||||
|
|
||||||
return std::make_tuple(licenseDesignation, licenseFile, licenseUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::multimap<UrlType, QualifiedUrl> Addon::getUrls() const
|
|
||||||
{
|
|
||||||
std::multimap<UrlType, QualifiedUrl> res;
|
|
||||||
|
|
||||||
auto appendIfNonEmpty = [&res](UrlType type, string url, string detail = "") {
|
|
||||||
if (!url.empty()) {
|
|
||||||
res.emplace(type, QualifiedUrl(type, std::move(url), std::move(detail)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const auto& author: _authors) {
|
|
||||||
appendIfNonEmpty(UrlType::author, author->getUrl(), author->getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto& maint: _maintainers) {
|
|
||||||
appendIfNonEmpty(UrlType::maintainer, maint->getUrl(), maint->getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
appendIfNonEmpty(UrlType::homePage, getHomePage());
|
|
||||||
appendIfNonEmpty(UrlType::download, getDownloadUrl());
|
|
||||||
appendIfNonEmpty(UrlType::support, getSupportUrl());
|
|
||||||
appendIfNonEmpty(UrlType::codeRepository, getCodeRepositoryUrl());
|
|
||||||
appendIfNonEmpty(UrlType::license, getLicenseUrl());
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static method
|
// Static method
|
||||||
void Addon::setupGhost(nasal::Hash& addonsModule)
|
void Addon::setupGhost(nasal::Hash& addonsModule)
|
||||||
{
|
{
|
||||||
|
@ -649,6 +321,7 @@ void Addon::setupGhost(nasal::Hash& addonsModule)
|
||||||
.member("downloadUrl", &Addon::getDownloadUrl)
|
.member("downloadUrl", &Addon::getDownloadUrl)
|
||||||
.member("supportUrl", &Addon::getSupportUrl)
|
.member("supportUrl", &Addon::getSupportUrl)
|
||||||
.member("codeRepositoryUrl", &Addon::getCodeRepositoryUrl)
|
.member("codeRepositoryUrl", &Addon::getCodeRepositoryUrl)
|
||||||
|
.member("triggerProperty", &Addon::getTriggerProperty)
|
||||||
.member("node", &Addon::getAddonPropsNode)
|
.member("node", &Addon::getAddonPropsNode)
|
||||||
.member("loadSequenceNumber", &Addon::getLoadSequenceNumber);
|
.member("loadSequenceNumber", &Addon::getLoadSequenceNumber);
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,6 @@
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <tuple>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <simgear/misc/sg_path.hxx>
|
#include <simgear/misc/sg_path.hxx>
|
||||||
|
@ -33,8 +32,8 @@
|
||||||
#include <simgear/structure/SGReferenced.hxx>
|
#include <simgear/structure/SGReferenced.hxx>
|
||||||
|
|
||||||
#include "addon_fwd.hxx"
|
#include "addon_fwd.hxx"
|
||||||
#include "AddonVersion.hxx"
|
|
||||||
#include "contacts.hxx"
|
#include "contacts.hxx"
|
||||||
|
#include "AddonVersion.hxx"
|
||||||
|
|
||||||
namespace flightgear
|
namespace flightgear
|
||||||
{
|
{
|
||||||
|
@ -75,7 +74,8 @@ private:
|
||||||
std::string _detail;
|
std::string _detail;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Addon : public SGReferenced {
|
class Addon : public SGReferenced
|
||||||
|
{
|
||||||
public:
|
public:
|
||||||
// Default constructor. 'minFGVersionRequired' is initialized to "2017.4.0"
|
// Default constructor. 'minFGVersionRequired' is initialized to "2017.4.0"
|
||||||
// and 'maxFGVersionRequired' to "none".
|
// and 'maxFGVersionRequired' to "none".
|
||||||
|
@ -148,6 +148,9 @@ public:
|
||||||
std::string getCodeRepositoryUrl() const;
|
std::string getCodeRepositoryUrl() const;
|
||||||
void setCodeRepositoryUrl(const std::string& addonCodeRepositoryUrl);
|
void setCodeRepositoryUrl(const std::string& addonCodeRepositoryUrl);
|
||||||
|
|
||||||
|
std::string getTriggerProperty() const;
|
||||||
|
void setTriggerProperty(const std::string& addonTriggerProperty);
|
||||||
|
|
||||||
// Node pertaining to the add-on in the Global Property Tree
|
// Node pertaining to the add-on in the Global Property Tree
|
||||||
SGPropertyNode_ptr getAddonNode() const;
|
SGPropertyNode_ptr getAddonNode() const;
|
||||||
void setAddonNode(SGPropertyNode* addonNode);
|
void setAddonNode(SGPropertyNode* addonNode);
|
||||||
|
@ -171,20 +174,13 @@ public:
|
||||||
static void setupGhost(nasal::Hash& addonsModule);
|
static void setupGhost(nasal::Hash& addonsModule);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
class Metadata;
|
||||||
|
class MetadataParser;
|
||||||
|
|
||||||
// “Compute” a path to the metadata file from the add-on base path
|
// “Compute” a path to the metadata file from the add-on base path
|
||||||
static SGPath getMetadataFile(const SGPath& addonPath);
|
static SGPath getMetadataFile(const SGPath& addonPath);
|
||||||
SGPath getMetadataFile() const;
|
SGPath getMetadataFile() const;
|
||||||
|
|
||||||
static std::tuple<string, SGPath, string>
|
|
||||||
parseLicenseNode(const SGPath& addonPath, SGPropertyNode* addonNode);
|
|
||||||
|
|
||||||
// Parse an addon-metadata.xml node such as <authors> or <maintainers>.
|
|
||||||
// Return the corresponding vector<AuthorRef> or vector<MaintainerRef>. If
|
|
||||||
// the 'mainNode' argument is nullptr, return an empty vector.
|
|
||||||
template <class T>
|
|
||||||
static std::vector<typename contact_traits<T>::strong_ref>
|
|
||||||
parseContactsNode(const SGPath& metadataFile, SGPropertyNode* mainNode);
|
|
||||||
|
|
||||||
// The add-on identifier, in reverse DNS style. The AddonManager refuses to
|
// The add-on identifier, in reverse DNS style. The AddonManager refuses to
|
||||||
// register two add-ons with the same id in a given FlightGear session.
|
// register two add-ons with the same id in a given FlightGear session.
|
||||||
std::string _id;
|
std::string _id;
|
||||||
|
@ -220,11 +216,14 @@ private:
|
||||||
|
|
||||||
// Main node for the add-on in the Property Tree
|
// Main node for the add-on in the Property Tree
|
||||||
SGPropertyNode_ptr _addonNode;
|
SGPropertyNode_ptr _addonNode;
|
||||||
|
// The add-on will be loaded when the property referenced by
|
||||||
|
// _triggerProperty is written to.
|
||||||
|
std::string _triggerProperty;
|
||||||
// Semantics explained above
|
// Semantics explained above
|
||||||
int _loadSequenceNumber = -1;
|
int _loadSequenceNumber = -1;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::ostream& operator<<(std::ostream& os, const Addon& addonMetaData);
|
std::ostream& operator<<(std::ostream& os, const Addon& addon);
|
||||||
|
|
||||||
} // of namespace addons
|
} // of namespace addons
|
||||||
|
|
||||||
|
|
372
src/Add-ons/AddonMetadataParser.cxx
Normal file
372
src/Add-ons/AddonMetadataParser.cxx
Normal file
|
@ -0,0 +1,372 @@
|
||||||
|
// -*- coding: utf-8 -*-
|
||||||
|
//
|
||||||
|
// AddonMetadataParser.cxx --- Parser for FlightGear add-on metadata files
|
||||||
|
// Copyright (C) 2018 Florent Rougon
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#include <regex>
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <simgear/debug/logstream.hxx>
|
||||||
|
#include <simgear/misc/sg_path.hxx>
|
||||||
|
#include <simgear/misc/strutils.hxx>
|
||||||
|
#include <simgear/props/props.hxx>
|
||||||
|
#include <simgear/props/props_io.hxx>
|
||||||
|
|
||||||
|
#include "addon_fwd.hxx"
|
||||||
|
#include "AddonMetadataParser.hxx"
|
||||||
|
#include "AddonVersion.hxx"
|
||||||
|
#include "contacts.hxx"
|
||||||
|
#include "exceptions.hxx"
|
||||||
|
#include "pointer_traits.hxx"
|
||||||
|
|
||||||
|
namespace strutils = simgear::strutils;
|
||||||
|
|
||||||
|
using std::string;
|
||||||
|
using std::vector;
|
||||||
|
|
||||||
|
namespace flightgear
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace addons
|
||||||
|
{
|
||||||
|
|
||||||
|
// Static method
|
||||||
|
SGPath
|
||||||
|
Addon::MetadataParser::getMetadataFile(const SGPath& addonPath)
|
||||||
|
{
|
||||||
|
return addonPath / "addon-metadata.xml";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static method
|
||||||
|
Addon::Metadata
|
||||||
|
Addon::MetadataParser::parseMetadataFile(const SGPath& addonPath)
|
||||||
|
{
|
||||||
|
SGPath metadataFile = getMetadataFile(addonPath);
|
||||||
|
SGPropertyNode addonRoot;
|
||||||
|
Addon::Metadata metadata;
|
||||||
|
|
||||||
|
if (!metadataFile.exists()) {
|
||||||
|
throw errors::no_metadata_file_found(
|
||||||
|
"unable to find add-on metadata file '" + metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
readProperties(metadataFile, &addonRoot);
|
||||||
|
} catch (const sg_exception &e) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"unable to load add-on metadata file '" + metadataFile.utf8Str() + "': " +
|
||||||
|
e.getFormattedMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the 'meta' section
|
||||||
|
SGPropertyNode *metaNode = addonRoot.getChild("meta");
|
||||||
|
if (metaNode == nullptr) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"no /meta node found in add-on metadata file '" +
|
||||||
|
metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the file type
|
||||||
|
SGPropertyNode *fileTypeNode = metaNode->getChild("file-type");
|
||||||
|
if (fileTypeNode == nullptr) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"no /meta/file-type node found in add-on metadata file '" +
|
||||||
|
metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
string fileType = fileTypeNode->getStringValue();
|
||||||
|
if (fileType != "FlightGear add-on metadata") {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"Invalid /meta/file-type value for add-on metadata file '" +
|
||||||
|
metadataFile.utf8Str() + "': '" + fileType + "' "
|
||||||
|
"(expected 'FlightGear add-on metadata')");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the format version
|
||||||
|
SGPropertyNode *fmtVersionNode = metaNode->getChild("format-version");
|
||||||
|
if (fmtVersionNode == nullptr) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"no /meta/format-version node found in add-on metadata file '" +
|
||||||
|
metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
int formatVersion = fmtVersionNode->getIntValue();
|
||||||
|
if (formatVersion != 1) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"unknown format version in add-on metadata file '" +
|
||||||
|
metadataFile.utf8Str() + "': " + std::to_string(formatVersion));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now the data we are really interested in
|
||||||
|
SGPropertyNode *addonNode = addonRoot.getChild("addon");
|
||||||
|
if (addonNode == nullptr) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"no /addon node found in add-on metadata file '" +
|
||||||
|
metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *idNode = addonNode->getChild("identifier");
|
||||||
|
if (idNode == nullptr) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"no /addon/identifier node found in add-on metadata file '" +
|
||||||
|
metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
metadata.id = strutils::strip(idNode->getStringValue());
|
||||||
|
|
||||||
|
// Require a non-empty identifier for the add-on
|
||||||
|
if (metadata.id.empty()) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"empty or whitespace-only value for the /addon/identifier node in "
|
||||||
|
"add-on metadata file '" + metadataFile.utf8Str() + "'");
|
||||||
|
} else if (metadata.id.find('.') == string::npos) {
|
||||||
|
SG_LOG(SG_GENERAL, SG_WARN,
|
||||||
|
"Add-on identifier '" << metadata.id << "' does not use reverse DNS "
|
||||||
|
"style (e.g., org.flightgear.addons.MyAddon) in add-on metadata "
|
||||||
|
"file '" << metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *nameNode = addonNode->getChild("name");
|
||||||
|
if (nameNode == nullptr) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"no /addon/name node found in add-on metadata file '" +
|
||||||
|
metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
metadata.name = strutils::strip(nameNode->getStringValue());
|
||||||
|
|
||||||
|
// Require a non-empty name for the add-on
|
||||||
|
if (metadata.name.empty()) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"empty or whitespace-only value for the /addon/name node in add-on "
|
||||||
|
"metadata file '" + metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *versionNode = addonNode->getChild("version");
|
||||||
|
if (versionNode == nullptr) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"no /addon/version node found in add-on metadata file '" +
|
||||||
|
metadataFile.utf8Str() + "'");
|
||||||
|
}
|
||||||
|
metadata.version = AddonVersion{
|
||||||
|
strutils::strip(versionNode->getStringValue())};
|
||||||
|
|
||||||
|
metadata.authors = parseContactsNode<Author>(metadataFile,
|
||||||
|
addonNode->getChild("authors"));
|
||||||
|
metadata.maintainers = parseContactsNode<Maintainer>(
|
||||||
|
metadataFile, addonNode->getChild("maintainers"));
|
||||||
|
|
||||||
|
SGPropertyNode *shortDescNode = addonNode->getChild("short-description");
|
||||||
|
if (shortDescNode != nullptr) {
|
||||||
|
metadata.shortDescription = strutils::strip(shortDescNode->getStringValue());
|
||||||
|
} else {
|
||||||
|
metadata.shortDescription = string();
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *longDescNode = addonNode->getChild("long-description");
|
||||||
|
if (longDescNode != nullptr) {
|
||||||
|
metadata.longDescription = strutils::strip(longDescNode->getStringValue());
|
||||||
|
} else {
|
||||||
|
metadata.longDescription = string();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::tie(metadata.licenseDesignation, metadata.licenseFile,
|
||||||
|
metadata.licenseUrl) = parseLicenseNode(addonPath, addonNode);
|
||||||
|
|
||||||
|
SGPropertyNode *tagsNode = addonNode->getChild("tags");
|
||||||
|
if (tagsNode != nullptr) {
|
||||||
|
auto tagNodes = tagsNode->getChildren("tag");
|
||||||
|
for (const auto& node: tagNodes) {
|
||||||
|
metadata.tags.push_back(strutils::strip(node->getStringValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *minNode = addonNode->getChild("min-FG-version");
|
||||||
|
if (minNode != nullptr) {
|
||||||
|
metadata.minFGVersionRequired = strutils::strip(minNode->getStringValue());
|
||||||
|
} else {
|
||||||
|
metadata.minFGVersionRequired = string();
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *maxNode = addonNode->getChild("max-FG-version");
|
||||||
|
if (maxNode != nullptr) {
|
||||||
|
metadata.maxFGVersionRequired = strutils::strip(maxNode->getStringValue());
|
||||||
|
} else {
|
||||||
|
metadata.maxFGVersionRequired = string();
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.homePage = metadata.downloadUrl = metadata.supportUrl =
|
||||||
|
metadata.codeRepositoryUrl = string(); // defaults
|
||||||
|
SGPropertyNode *urlsNode = addonNode->getChild("urls");
|
||||||
|
if (urlsNode != nullptr) {
|
||||||
|
SGPropertyNode *homePageNode = urlsNode->getChild("home-page");
|
||||||
|
if (homePageNode != nullptr) {
|
||||||
|
metadata.homePage = strutils::strip(homePageNode->getStringValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *downloadUrlNode = urlsNode->getChild("download");
|
||||||
|
if (downloadUrlNode != nullptr) {
|
||||||
|
metadata.downloadUrl = strutils::strip(downloadUrlNode->getStringValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *supportUrlNode = urlsNode->getChild("support");
|
||||||
|
if (supportUrlNode != nullptr) {
|
||||||
|
metadata.supportUrl = strutils::strip(supportUrlNode->getStringValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *codeRepoUrlNode = urlsNode->getChild("code-repository");
|
||||||
|
if (codeRepoUrlNode != nullptr) {
|
||||||
|
metadata.codeRepositoryUrl =
|
||||||
|
strutils::strip(codeRepoUrlNode->getStringValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SG_LOG(SG_GENERAL, SG_DEBUG,
|
||||||
|
"Parsed add-on metadata file: '" << metadataFile.utf8Str() + "'");
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function for Addon::MetadataParser::parseContactsNode<>()
|
||||||
|
//
|
||||||
|
// Read a node such as "name", "email" or "url", child of a contact node (e.g.,
|
||||||
|
// of an "author" or "maintainer" node).
|
||||||
|
static string
|
||||||
|
parseContactsNode_readNode(const SGPath& metadataFile,
|
||||||
|
SGPropertyNode* contactNode,
|
||||||
|
string subnodeName, bool allowEmpty)
|
||||||
|
{
|
||||||
|
SGPropertyNode *node = contactNode->getChild(subnodeName);
|
||||||
|
string contents;
|
||||||
|
|
||||||
|
if (node != nullptr) {
|
||||||
|
contents = simgear::strutils::strip(node->getStringValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!allowEmpty && contents.empty()) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"in add-on metadata file '" + metadataFile.utf8Str() + "': "
|
||||||
|
"when the node " + contactNode->getPath(true) + " exists, it must have "
|
||||||
|
"a non-empty '" + subnodeName + "' child node");
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static method template (private and only used in this file)
|
||||||
|
template <class T>
|
||||||
|
vector<typename contact_traits<T>::strong_ref>
|
||||||
|
Addon::MetadataParser::parseContactsNode(const SGPath& metadataFile,
|
||||||
|
SGPropertyNode* mainNode)
|
||||||
|
{
|
||||||
|
using contactTraits = contact_traits<T>;
|
||||||
|
vector<typename contactTraits::strong_ref> res;
|
||||||
|
|
||||||
|
if (mainNode != nullptr) {
|
||||||
|
auto contactNodes = mainNode->getChildren(contactTraits::xmlNodeName());
|
||||||
|
res.reserve(contactNodes.size());
|
||||||
|
|
||||||
|
for (const auto& contactNode: contactNodes) {
|
||||||
|
string name, email, url;
|
||||||
|
|
||||||
|
name = parseContactsNode_readNode(metadataFile, contactNode.get(),
|
||||||
|
"name", false /* allowEmpty */);
|
||||||
|
email = parseContactsNode_readNode(metadataFile, contactNode.get(),
|
||||||
|
"email", true);
|
||||||
|
url = parseContactsNode_readNode(metadataFile, contactNode.get(),
|
||||||
|
"url", true);
|
||||||
|
|
||||||
|
using ptr_traits = shared_ptr_traits<typename contactTraits::strong_ref>;
|
||||||
|
res.push_back(ptr_traits::makeStrongRef(name, email, url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Static method
|
||||||
|
std::tuple<string, SGPath, string>
|
||||||
|
Addon::MetadataParser::parseLicenseNode(const SGPath& addonPath,
|
||||||
|
SGPropertyNode* addonNode)
|
||||||
|
{
|
||||||
|
SGPath metadataFile = getMetadataFile(addonPath);
|
||||||
|
string licenseDesignation;
|
||||||
|
SGPath licenseFile;
|
||||||
|
string licenseUrl;
|
||||||
|
|
||||||
|
SGPropertyNode *licenseNode = addonNode->getChild("license");
|
||||||
|
if (licenseNode == nullptr) {
|
||||||
|
return std::tuple<string, SGPath, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *licenseDesigNode = licenseNode->getChild("designation");
|
||||||
|
if (licenseDesigNode != nullptr) {
|
||||||
|
licenseDesignation = strutils::strip(licenseDesigNode->getStringValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
SGPropertyNode *licenseFileNode = licenseNode->getChild("file");
|
||||||
|
if (licenseFileNode != nullptr) {
|
||||||
|
// This effectively disallows filenames starting or ending with whitespace
|
||||||
|
string licenseFile_s = strutils::strip(licenseFileNode->getStringValue());
|
||||||
|
|
||||||
|
if (!licenseFile_s.empty()) {
|
||||||
|
if (licenseFile_s.find('\\') != string::npos) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"in add-on metadata file '" + metadataFile.utf8Str() + "': the "
|
||||||
|
"value of /addon/license/file contains '\\'; please use '/' "
|
||||||
|
"separators only");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (licenseFile_s.find_first_of("/\\") == 0) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"in add-on metadata file '" + metadataFile.utf8Str() + "': the "
|
||||||
|
"value of /addon/license/file must be relative to the add-on folder, "
|
||||||
|
"however it starts with '" + licenseFile_s[0] + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::regex winDriveRegexp("([a-zA-Z]:).*");
|
||||||
|
std::smatch results;
|
||||||
|
|
||||||
|
if (std::regex_match(licenseFile_s, results, winDriveRegexp)) {
|
||||||
|
string winDrive = results.str(1);
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"in add-on metadata file '" + metadataFile.utf8Str() + "': the "
|
||||||
|
"value of /addon/license/file must be relative to the add-on folder, "
|
||||||
|
"however it starts with a Windows drive letter (" + winDrive + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
licenseFile = addonPath / licenseFile_s;
|
||||||
|
if ( !(licenseFile.exists() && licenseFile.isFile()) ) {
|
||||||
|
throw errors::error_loading_metadata_file(
|
||||||
|
"in add-on metadata file '" + metadataFile.utf8Str() + "': the "
|
||||||
|
"value of /addon/license/file (pointing to '" + licenseFile.utf8Str() +
|
||||||
|
"') doesn't correspond to an existing file");
|
||||||
|
}
|
||||||
|
} // of if (!licenseFile_s.empty())
|
||||||
|
} // of if (licenseFileNode != nullptr)
|
||||||
|
|
||||||
|
SGPropertyNode *licenseUrlNode = licenseNode->getChild("url");
|
||||||
|
if (licenseUrlNode != nullptr) {
|
||||||
|
licenseUrl = strutils::strip(licenseUrlNode->getStringValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::make_tuple(licenseDesignation, licenseFile, licenseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // of namespace addons
|
||||||
|
|
||||||
|
} // of namespace flightgear
|
96
src/Add-ons/AddonMetadataParser.hxx
Normal file
96
src/Add-ons/AddonMetadataParser.hxx
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
// -*- coding: utf-8 -*-
|
||||||
|
//
|
||||||
|
// AddonMetadataParser.hxx --- Parser for FlightGear add-on metadata files
|
||||||
|
// Copyright (C) 2018 Florent Rougon
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program 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 General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#ifndef FG_ADDON_METADATA_PARSER_HXX
|
||||||
|
#define FG_ADDON_METADATA_PARSER_HXX
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <tuple>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <simgear/misc/sg_path.hxx>
|
||||||
|
|
||||||
|
#include "addon_fwd.hxx"
|
||||||
|
#include "Addon.hxx"
|
||||||
|
#include "AddonVersion.hxx"
|
||||||
|
#include "contacts.hxx"
|
||||||
|
|
||||||
|
class SGPropertyNode;
|
||||||
|
|
||||||
|
namespace flightgear
|
||||||
|
{
|
||||||
|
|
||||||
|
namespace addons
|
||||||
|
{
|
||||||
|
|
||||||
|
struct Addon::Metadata
|
||||||
|
{
|
||||||
|
// Comments about these fields can be found in Addon.hxx
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
AddonVersion version;
|
||||||
|
|
||||||
|
std::vector<AuthorRef> authors;
|
||||||
|
std::vector<MaintainerRef> maintainers;
|
||||||
|
|
||||||
|
std::string shortDescription;
|
||||||
|
std::string longDescription;
|
||||||
|
|
||||||
|
std::string licenseDesignation;
|
||||||
|
SGPath licenseFile;
|
||||||
|
std::string licenseUrl;
|
||||||
|
|
||||||
|
std::vector<std::string> tags;
|
||||||
|
|
||||||
|
std::string minFGVersionRequired;
|
||||||
|
std::string maxFGVersionRequired;
|
||||||
|
|
||||||
|
std::string homePage;
|
||||||
|
std::string downloadUrl;
|
||||||
|
std::string supportUrl;
|
||||||
|
std::string codeRepositoryUrl;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Addon::MetadataParser
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// “Compute” a path to the metadata file from the add-on base path
|
||||||
|
static SGPath getMetadataFile(const SGPath& addonPath);
|
||||||
|
|
||||||
|
// Parse the add-on metadata file inside 'addonPath' (as defined by
|
||||||
|
// getMetadataFile()) and return the corresponding Addon::Metadata instance.
|
||||||
|
static Addon::Metadata parseMetadataFile(const SGPath& addonPath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static std::tuple<string, SGPath, string>
|
||||||
|
parseLicenseNode(const SGPath& addonPath, SGPropertyNode* addonNode);
|
||||||
|
|
||||||
|
// Parse an addon-metadata.xml node such as <authors> or <maintainers>.
|
||||||
|
// Return the corresponding vector<AuthorRef> or vector<MaintainerRef>. If
|
||||||
|
// the 'mainNode' argument is nullptr, return an empty vector.
|
||||||
|
template <class T>
|
||||||
|
static std::vector<typename contact_traits<T>::strong_ref>
|
||||||
|
parseContactsNode(const SGPath& metadataFile, SGPropertyNode* mainNode);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // of namespace addons
|
||||||
|
|
||||||
|
} // of namespace flightgear
|
||||||
|
|
||||||
|
#endif // of FG_ADDON_METADATA_PARSER_HXX
|
|
@ -2,6 +2,7 @@ include(FlightGearComponent)
|
||||||
|
|
||||||
set(SOURCES Addon.cxx
|
set(SOURCES Addon.cxx
|
||||||
AddonManager.cxx
|
AddonManager.cxx
|
||||||
|
AddonMetadataParser.cxx
|
||||||
AddonVersion.cxx
|
AddonVersion.cxx
|
||||||
contacts.cxx
|
contacts.cxx
|
||||||
exceptions.cxx
|
exceptions.cxx
|
||||||
|
@ -10,6 +11,7 @@ set(SOURCES Addon.cxx
|
||||||
set(HEADERS addon_fwd.hxx
|
set(HEADERS addon_fwd.hxx
|
||||||
Addon.hxx
|
Addon.hxx
|
||||||
AddonManager.hxx
|
AddonManager.hxx
|
||||||
|
AddonMetadataParser.hxx
|
||||||
AddonVersion.hxx
|
AddonVersion.hxx
|
||||||
contacts.hxx
|
contacts.hxx
|
||||||
exceptions.hxx
|
exceptions.hxx
|
||||||
|
|
|
@ -9,6 +9,7 @@ set(sources
|
||||||
Main/positioninit.cxx
|
Main/positioninit.cxx
|
||||||
Add-ons/Addon.cxx
|
Add-ons/Addon.cxx
|
||||||
Add-ons/AddonManager.cxx
|
Add-ons/AddonManager.cxx
|
||||||
|
Add-ons/AddonMetadataParser.cxx
|
||||||
Add-ons/AddonVersion.cxx
|
Add-ons/AddonVersion.cxx
|
||||||
Add-ons/contacts.cxx
|
Add-ons/contacts.cxx
|
||||||
Add-ons/exceptions.cxx
|
Add-ons/exceptions.cxx
|
||||||
|
|
Loading…
Reference in a new issue