From c3b1442546c83d366700ba4b08cce9357829bf0c Mon Sep 17 00:00:00 2001
From: Florent Rougon <f.rougon@free.fr>
Date: Tue, 12 Dec 2017 07:51:05 +0100
Subject: [PATCH] Add-ons: new supported fields: authors, maintainers,
 license/*, url/*, tags

- Parsing of the addon-metadata.xml file is now handled by a new static
  method of Addon:

    static Addon fromAddonDir(const SGPath& addonPath);

  This method will be reusable to gather all add-on metadata from a set
  of add-on directories (just call the method once per add-on). This
  change also simplifies AddonManager::registerAddonMetadata().

- New supported fields:

    authors
    maintainers
    license/{designation,file,url}
    url/{home-page,download,support,code-repository}
    tags

  See
  https://sourceforge.net/p/flightgear/fgdata/ci/next/tree/Docs/README.add-ons
  for documentation on these fields.

Mailing-list discussion:

  around https://sourceforge.net/p/flightgear/mailman/message/36155660/
---
 src/Add-ons/Addon.cxx        | 341 +++++++++++++++++++++++++++++++++++
 src/Add-ons/Addon.hxx        |  48 +++++
 src/Add-ons/AddonManager.cxx | 164 +----------------
 3 files changed, 394 insertions(+), 159 deletions(-)

diff --git a/src/Add-ons/Addon.cxx b/src/Add-ons/Addon.cxx
index be7e2d035..3bf5e42fe 100644
--- a/src/Add-ons/Addon.cxx
+++ b/src/Add-ons/Addon.cxx
@@ -18,14 +18,19 @@
 // 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 
 #include <ostream>
+#include <regex>
 #include <sstream>
 #include <string>
+#include <tuple>
 #include <utility>
+#include <vector>
 
 #include <simgear/misc/sg_path.hxx>
 #include <simgear/nasal/cppbind/Ghost.hxx>
 #include <simgear/nasal/cppbind/NasalHash.hxx>
 #include <simgear/nasal/naref.h>
+#include <simgear/props/props.hxx>
+#include <simgear/props/props_io.hxx>
 
 #include <Main/globals.hxx>
 #include <Scripting/NasalSys.hxx>
@@ -33,8 +38,12 @@
 #include "addon_fwd.hxx"
 #include "Addon.hxx"
 #include "AddonVersion.hxx"
+#include "exceptions.hxx"
+
+namespace strutils = simgear::strutils;
 
 using std::string;
+using std::vector;
 
 namespace flightgear
 {
@@ -82,6 +91,18 @@ AddonVersionRef Addon::getVersion() const
 void Addon::setVersion(const AddonVersion& addonVersion)
 { _version.reset(new AddonVersion(addonVersion)); }
 
+std::string Addon::getAuthors() const
+{ return _authors; }
+
+void Addon::setAuthors(const std::string& addonAuthors)
+{ _authors = addonAuthors; }
+
+std::string Addon::getMaintainers() const
+{ return _maintainers; }
+
+void Addon::setMaintainers(const std::string& addonMaintainers)
+{ _maintainers = addonMaintainers; }
+
 std::string Addon::getShortDescription() const
 { return _shortDescription; }
 
@@ -94,6 +115,30 @@ std::string Addon::getLongDescription() const
 void Addon::setLongDescription(const std::string& addonLongDescription)
 { _longDescription = addonLongDescription; }
 
+std::string Addon::getLicenseDesignation() const
+{ return _licenseDesignation; }
+
+void Addon::setLicenseDesignation(const std::string& addonLicenseDesignation)
+{ _licenseDesignation = addonLicenseDesignation; }
+
+SGPath Addon::getLicenseFile() const
+{ return _licenseFile; }
+
+void Addon::setLicenseFile(const SGPath& addonLicenseFile)
+{ _licenseFile = addonLicenseFile; }
+
+std::string Addon::getLicenseUrl() const
+{ return _licenseUrl; }
+
+void Addon::setLicenseUrl(const std::string& addonLicenseUrl)
+{ _licenseUrl = addonLicenseUrl; }
+
+std::vector<std::string> Addon::getTags() const
+{ return _tags; }
+
+void Addon::setTags(const std::vector<std::string>& addonTags)
+{ _tags = addonTags; }
+
 SGPath Addon::getBasePath() const
 { return _basePath; }
 
@@ -136,6 +181,12 @@ std::string Addon::getSupportUrl() const
 void Addon::setSupportUrl(const std::string& addonSupportUrl)
 { _supportUrl = addonSupportUrl; }
 
+std::string Addon::getCodeRepositoryUrl() const
+{ return _codeRepositoryUrl; }
+
+void Addon::setCodeRepositoryUrl(const std::string& addonCodeRepositoryUrl)
+{ _codeRepositoryUrl = addonCodeRepositoryUrl; }
+
 SGPropertyNode_ptr Addon::getAddonNode() const
 { return _addonNode; }
 
@@ -170,6 +221,289 @@ std::string Addon::str() const
   return oss.str();
 }
 
+// Static method
+SGPath Addon::getMetadataFile(const SGPath& addonPath)
+{
+  return addonPath / "addon-metadata.xml";
+}
+
+SGPath Addon::getMetadataFile() const
+{
+  return getMetadataFile(getBasePath());
+}
+
+// Static method
+Addon Addon::fromAddonDir(const SGPath& addonPath)
+{
+  SGPath metadataFile = getMetadataFile(addonPath);
+  SGPropertyNode addonRoot;
+
+  if (!metadataFile.exists()) {
+    throw addon_errors::no_metadata_file_found(
+      "unable to find add-on metadata file '" + metadataFile.utf8Str() + "'");
+  }
+
+  try {
+    readProperties(metadataFile, &addonRoot);
+  } catch (const sg_exception &e) {
+    throw addon_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 addon_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 addon_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 addon_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 addon_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 addon_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 addon_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 addon_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 addon_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 addon_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 addon_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 addon_errors::error_loading_metadata_file(
+      "no /addon/version node found in add-on metadata file '" +
+      metadataFile.utf8Str() + "'");
+  }
+  AddonVersion addonVersion(versionNode->getStringValue());
+
+  string addonAuthors;
+  SGPropertyNode *authorsNode = addonNode->getChild("authors");
+  if (authorsNode != nullptr) {
+    addonAuthors = strutils::strip(authorsNode->getStringValue());
+  }
+
+  string addonMaintainers;
+  SGPropertyNode *maintainersNode = addonNode->getChild("maintainers");
+  if (maintainersNode != nullptr) {
+    addonMaintainers = strutils::strip(maintainersNode->getStringValue());
+  }
+
+  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 = minNode->getStringValue();
+  }
+
+  string addonMaxFGVersionRequired;
+  SGPropertyNode *maxNode = addonNode->getChild("max-FG-version");
+  if (maxNode != nullptr) {
+    addonMaxFGVersionRequired = 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
+  Addon addon{addonId, std::move(addonVersion), addonPath,
+              addonMinFGVersionRequired, addonMaxFGVersionRequired};
+  addon.setName(addonName);
+  addon.setAuthors(addonAuthors);
+  addon.setMaintainers(addonMaintainers);
+  addon.setShortDescription(addonShortDescription);
+  addon.setLongDescription(addonLongDescription);
+  addon.setLicenseDesignation(addonLicenseDesignation);
+  addon.setLicenseFile(addonLicenseFile);
+  addon.setLicenseUrl(addonLicenseUrl);
+  addon.setTags(addonTags);
+  addon.setHomePage(addonHomePage);
+  addon.setDownloadUrl(addonDownloadUrl);
+  addon.setSupportUrl(addonSupportUrl);
+  addon.setCodeRepositoryUrl(addonCodeRepoUrl);
+
+  return addon;
+}
+
+// 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);
+}
+
+
 // Static method
 void Addon::setupGhost(nasal::Hash& addonsModule)
 {
@@ -177,14 +511,21 @@ void Addon::setupGhost(nasal::Hash& addonsModule)
     .member("id", &Addon::getId)
     .member("name", &Addon::getName)
     .member("version", &Addon::getVersion)
+    .member("authors", &Addon::getAuthors)
+    .member("maintainers", &Addon::getMaintainers)
     .member("shortDescription", &Addon::getShortDescription)
     .member("longDescription", &Addon::getLongDescription)
+    .member("licenseDesignation", &Addon::getLicenseDesignation)
+    .member("licenseFile", &Addon::getLicenseFile)
+    .member("licenseUrl", &Addon::getLicenseUrl)
+    .member("tags", &Addon::getTags)
     .member("basePath", &Addon::getBasePath)
     .member("minFGVersionRequired", &Addon::getMinFGVersionRequired)
     .member("maxFGVersionRequired", &Addon::getMaxFGVersionRequired)
     .member("homePage", &Addon::getHomePage)
     .member("downloadUrl", &Addon::getDownloadUrl)
     .member("supportUrl", &Addon::getSupportUrl)
+    .member("codeRepositoryUrl", &Addon::getCodeRepositoryUrl)
     .member("node", &Addon::getAddonPropsNode)
     .member("loadSequenceNumber", &Addon::getLoadSequenceNumber);
 }
diff --git a/src/Add-ons/Addon.hxx b/src/Add-ons/Addon.hxx
index 06679d49e..04d083bf8 100644
--- a/src/Add-ons/Addon.hxx
+++ b/src/Add-ons/Addon.hxx
@@ -22,6 +22,8 @@
 
 #include <ostream>
 #include <string>
+#include <tuple>
+#include <vector>
 
 #include <simgear/misc/sg_path.hxx>
 #include <simgear/nasal/cppbind/NasalHash.hxx>
@@ -47,6 +49,10 @@ public:
         std::string maxFGVersionRequired = "",
         SGPropertyNode* addonNode = nullptr);
 
+  // Parse the add-on metadata file inside 'addonPath' (as defined by
+  // getMetadataFile()) and return the corresponding Addon instance.
+  static Addon fromAddonDir(const SGPath& addonPath);
+
   std::string getId() const;
   void setId(const std::string& addonId);
 
@@ -56,15 +62,37 @@ public:
   AddonVersionRef getVersion() const;
   void setVersion(const AddonVersion& addonVersion);
 
+  std::string getAuthors() const;
+  void setAuthors(const std::string& addonAuthors);
+
+  std::string getMaintainers() const;
+  void setMaintainers(const std::string& addonMaintainers);
+
   std::string getShortDescription() const;
   void setShortDescription(const std::string& addonShortDescription);
 
   std::string getLongDescription() const;
   void setLongDescription(const std::string& addonLongDescription);
 
+  std::string getLicenseDesignation() const;
+  void setLicenseDesignation(const std::string& addonLicenseDesignation);
+
+  SGPath getLicenseFile() const;
+  void setLicenseFile(const SGPath& addonLicenseFile);
+
+  std::string getLicenseUrl() const;
+  void setLicenseUrl(const std::string& addonLicenseUrl);
+
+  std::vector<std::string> getTags() const;
+  void setTags(const std::vector<std::string>& addonTags);
+
   SGPath getBasePath() const;
   void setBasePath(const SGPath& addonBasePath);
 
+  // “Compute” a path to the metadata file from the add-on base path
+  static SGPath getMetadataFile(const SGPath& addonPath);
+  SGPath getMetadataFile() const;
+
   // Should be valid for use with simgear::strutils::compare_versions()
   std::string getMinFGVersionRequired() const;
   void setMinFGVersionRequired(const std::string& minFGVersionRequired);
@@ -83,6 +111,9 @@ public:
   std::string getSupportUrl() const;
   void setSupportUrl(const std::string& addonSupportUrl);
 
+  std::string getCodeRepositoryUrl() const;
+  void setCodeRepositoryUrl(const std::string& addonCodeRepositoryUrl);
+
   // Node pertaining to the add-on in the Global Property Tree
   SGPropertyNode_ptr getAddonNode() const;
   void setAddonNode(SGPropertyNode* addonNode);
@@ -103,6 +134,9 @@ public:
   static void setupGhost(nasal::Hash& addonsModule);
 
 private:
+  static std::tuple<string, SGPath, string>
+  parseLicenseNode(const SGPath& addonPath, SGPropertyNode* addonNode);
+
   // 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.
   std::string _id;
@@ -111,17 +145,31 @@ private:
   // Use a smart pointer to expose the AddonVersion instance to Nasal without
   // needing to copy the data every time.
   AddonVersionRef _version;
+
+  std::string _authors;
+  std::string _maintainers;
+
   // Strings describing what the add-on does
   std::string _shortDescription;
   std::string _longDescription;
+
+  std::string _licenseDesignation;
+  SGPath _licenseFile;
+  std::string _licenseUrl;
+
+  std::vector<std::string> _tags;
   SGPath _basePath;
+
   // To be used with simgear::strutils::compare_versions()
   std::string _minFGVersionRequired;
   // Ditto, but there is a special value: "none"
   std::string _maxFGVersionRequired;
+
   std::string _homePage;
   std::string _downloadUrl;
   std::string _supportUrl;
+  std::string _codeRepositoryUrl;
+
   // Main node for the add-on in the Property Tree
   SGPropertyNode_ptr _addonNode;
   // Semantics explained above
diff --git a/src/Add-ons/AddonManager.cxx b/src/Add-ons/AddonManager.cxx
index 2ca96d4d1..13710e5cf 100644
--- a/src/Add-ons/AddonManager.cxx
+++ b/src/Add-ons/AddonManager.cxx
@@ -19,8 +19,8 @@
 
 #include <algorithm>
 #include <memory>
-#include <utility>
 #include <string>
+#include <utility>
 #include <vector>
 
 #include <cstdlib>
@@ -107,168 +107,14 @@ AddonManager::loadConfigFileIfExists(const SGPath& configFile)
 string
 AddonManager::registerAddonMetadata(const SGPath& addonPath)
 {
-  SGPropertyNode addonRoot;
-  const SGPath metadataFile = addonPath / "addon-metadata.xml";
-
-  if (!metadataFile.exists()) {
-    throw addon_errors::no_metadata_file_found(
-      "unable to find add-on metadata file '" + metadataFile.utf8Str() + "'");
-  }
-
-  try {
-    readProperties(metadataFile, &addonRoot);
-  } catch (const sg_exception &e) {
-    throw addon_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 addon_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 addon_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 addon_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 addon_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 addon_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 addon_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 addon_errors::error_loading_metadata_file(
-      "no /addon/identifier node found in add-on metadata file '" +
-      metadataFile.utf8Str() + "'");
-  }
-  std::string addonId = strutils::strip(idNode->getStringValue());
-
-  // Require a non-empty identifier for the add-on
-  if (addonId.empty()) {
-    throw addon_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 addon_errors::error_loading_metadata_file(
-      "no /addon/name node found in add-on metadata file '" +
-      metadataFile.utf8Str() + "'");
-  }
-  std::string addonName = strutils::strip(nameNode->getStringValue());
-
-  // Require a non-empty name for the add-on
-  if (addonName.empty()) {
-    throw addon_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 addon_errors::error_loading_metadata_file(
-      "no /addon/version node found in add-on metadata file '" +
-      metadataFile.utf8Str() + "'");
-  }
-  AddonVersion addonVersion(versionNode->getStringValue());
-
-  std::string addonShortDescription;
-  SGPropertyNode *shortDescNode = addonNode->getChild("short-description");
-  if (shortDescNode != nullptr) {
-    addonShortDescription = strutils::strip(shortDescNode->getStringValue());
-  }
-
-  std::string addonLongDescription;
-  SGPropertyNode *longDescNode = addonNode->getChild("long-description");
-  if (longDescNode != nullptr) {
-    addonLongDescription = strutils::strip(longDescNode->getStringValue());
-  }
-
-  std::string addonMinFGVersionRequired;
-  SGPropertyNode *minNode = addonNode->getChild("min-FG-version");
-  if (minNode != nullptr) {
-    addonMinFGVersionRequired = minNode->getStringValue();
-  }
-
-  std::string addonMaxFGVersionRequired;
-  SGPropertyNode *maxNode = addonNode->getChild("max-FG-version");
-  if (maxNode != nullptr) {
-    addonMaxFGVersionRequired = maxNode->getStringValue();
-  }
-
-  std::string addonHomePage;
-  SGPropertyNode *homePageNode = addonNode->getChild("home-page");
-  if (homePageNode != nullptr) {
-    addonHomePage = strutils::strip(homePageNode->getStringValue());
-  }
-
-  std::string addonDownloadUrl;
-  SGPropertyNode *downloadUrlNode = addonNode->getChild("download-url");
-  if (downloadUrlNode != nullptr) {
-    addonDownloadUrl = strutils::strip(downloadUrlNode->getStringValue());
-  }
-
-  std::string addonSupportUrl;
-  SGPropertyNode *supportUrlNode = addonNode->getChild("support-url");
-  if (supportUrlNode != nullptr) {
-    addonSupportUrl = strutils::strip(supportUrlNode->getStringValue());
-  }
+  AddonRef addon(new Addon(Addon::fromAddonDir(addonPath)));
+  SGPath metadataFile = addon->getMetadataFile();
+  string addonId = addon->getId();
 
   SGPropertyNode* addonPropNode = fgGetNode("addons", true)
                                  ->getChild("by-id", 0, 1)
                                  ->getChild(addonId, 0, 1);
-  // Object holding all the add-on metadata
-  AddonRef addon(new Addon(
-                   addonId, std::move(addonVersion), addonPath,
-                   addonMinFGVersionRequired, addonMaxFGVersionRequired,
-                   addonPropNode));
-  addon->setName(addonName);
-  addon->setShortDescription(addonShortDescription);
-  addon->setLongDescription(addonLongDescription);
-  addon->setHomePage(addonHomePage);
-  addon->setDownloadUrl(addonDownloadUrl);
-  addon->setSupportUrl(addonSupportUrl);
+  addon->setAddonNode(addonPropNode);
   addon->setLoadSequenceNumber(_loadSequenceNumber++);
 
   // Check that the FlightGear version satisfies the add-on requirements