1 // Copyright (C) 2013 James Turner - zakalawe@mac.com
3 // This library is free software; you can redistribute it and/or
4 // modify it under the terms of the GNU Library General Public
5 // License as published by the Free Software Foundation; either
6 // version 2 of the License, or (at your option) any later version.
8 // This library is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11 // Library General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with this program; if not, write to the Free Software
15 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 #include <simgear/package/Catalog.hxx>
20 #include <boost/foreach.hpp>
25 #include <simgear/debug/logstream.hxx>
26 #include <simgear/props/props_io.hxx>
27 #include <simgear/io/HTTPRequest.hxx>
28 #include <simgear/io/HTTPClient.hxx>
29 #include <simgear/misc/sg_dir.hxx>
30 #include <simgear/structure/exception.hxx>
31 #include <simgear/package/Package.hxx>
32 #include <simgear/package/Root.hxx>
33 #include <simgear/package/Install.hxx>
34 #include <simgear/misc/strutils.hxx>
40 bool checkVersionString(const std::string& aVersion, const std::string& aCandidate)
42 if (aCandidate == aVersion) {
46 // examine each dot-seperated component in turn, supporting a wildcard
47 // in the versions from the catalog.
48 string_list appVersionParts = simgear::strutils::split(aVersion, ".");
49 string_list catVersionParts = simgear::strutils::split(aCandidate, ".");
51 size_t partCount = appVersionParts.size();
52 bool previousCandidatePartWasWildcard = false;
54 for (unsigned int p=0; p < partCount; ++p) {
55 // candidate string is too short, can match if it ended with wildcard
56 // part. This allows candidate '2016.*' to match '2016.1.2' and so on
57 if (catVersionParts.size() <= p) {
58 return previousCandidatePartWasWildcard;
61 if (catVersionParts[p] == "*") {
63 previousCandidatePartWasWildcard = true;
64 } else if (appVersionParts[p] != catVersionParts[p]) {
72 bool checkVersion(const std::string& aVersion, SGPropertyNode_ptr props)
74 BOOST_FOREACH(SGPropertyNode* v, props->getChildren("version")) {
75 std::string s(v->getStringValue());
76 if (checkVersionString(aVersion, s)) {
83 std::string redirectUrlForVersion(const std::string& aVersion, SGPropertyNode_ptr props)
85 BOOST_FOREACH(SGPropertyNode* v, props->getChildren("alternate-version")) {
86 std::string s(v->getStringValue("version"));
87 if (checkVersionString(aVersion, s)) {
88 return v->getStringValue("url");;
95 //////////////////////////////////////////////////////////////////////////////
97 class Catalog::Downloader : public HTTP::Request
100 Downloader(CatalogRef aOwner, const std::string& aUrl) :
105 m_owner->changeStatus(Delegate::STATUS_IN_PROGRESS);
109 virtual void gotBodyData(const char* s, int n)
111 m_buffer += std::string(s, n);
114 virtual void onDone()
116 if (responseCode() != 200) {
117 Delegate::StatusCode code = Delegate::FAIL_DOWNLOAD;
118 SG_LOG(SG_GENERAL, SG_ALERT, "catalog download failure:" << m_owner->url()
119 << "\n\t" << responseCode());
120 if (responseCode() == 404) {
121 code = Delegate::FAIL_NOT_FOUND;
123 m_owner->refreshComplete(code);
127 SGPropertyNode* props = new SGPropertyNode;
130 readProperties(m_buffer.data(), m_buffer.size(), props);
131 m_owner->parseProps(props);
132 } catch (sg_exception& ) {
133 SG_LOG(SG_GENERAL, SG_ALERT, "catalog parse failure:" << m_owner->url());
134 m_owner->refreshComplete(Delegate::FAIL_EXTRACT);
138 std::string ver(m_owner->root()->applicationVersion());
139 if (!checkVersion(ver, props)) {
140 SG_LOG(SG_GENERAL, SG_WARN, "downloaded catalog " << m_owner->url() << ", version required " << ver);
142 // check for a version redirect entry
143 std::string url = redirectUrlForVersion(ver, props);
145 SG_LOG(SG_GENERAL, SG_WARN, "redirecting from " << m_owner->url() <<
148 // update the URL and kick off a new request
149 m_owner->setUrl(url);
150 Downloader* dl = new Downloader(m_owner, url);
151 m_owner->root()->makeHTTPRequest(dl);
153 m_owner->refreshComplete(Delegate::FAIL_VERSION);
157 } // of version check failed
159 // cache the catalog data, now we have a valid install root
160 Dir d(m_owner->installRoot());
161 SGPath p = d.file("catalog.xml");
163 std::ofstream f(p.c_str(), std::ios::out | std::ios::trunc);
164 f.write(m_buffer.data(), m_buffer.size());
167 time(&m_owner->m_retrievedTime);
168 m_owner->writeTimestamp();
169 m_owner->refreshComplete(Delegate::STATUS_REFRESHED);
175 std::string m_buffer;
178 //////////////////////////////////////////////////////////////////////////////
180 Catalog::Catalog(Root *aRoot) :
182 m_status(Delegate::FAIL_UNKNOWN),
191 CatalogRef Catalog::createFromUrl(Root* aRoot, const std::string& aUrl)
193 CatalogRef c = new Catalog(aRoot);
199 CatalogRef Catalog::createFromPath(Root* aRoot, const SGPath& aPath)
202 xml.append("catalog.xml");
207 SGPropertyNode_ptr props;
209 props = new SGPropertyNode;
210 readProperties(xml.str(), props);
211 } catch (sg_exception& ) {
215 bool versionCheckOk = checkVersion(aRoot->applicationVersion(), props);
216 if (!versionCheckOk) {
217 SG_LOG(SG_GENERAL, SG_INFO, "catalog at:" << aPath << " failed version check: need" << aRoot->applicationVersion());
218 // keep the catalog but mark it as needing an update
220 SG_LOG(SG_GENERAL, SG_DEBUG, "creating catalog from:" << aPath);
223 CatalogRef c = new Catalog(aRoot);
224 c->m_installRoot = aPath;
225 c->parseProps(props);
228 if (versionCheckOk) {
229 // parsed XML ok, mark status as valid
230 c->changeStatus(Delegate::STATUS_SUCCESS);
232 c->changeStatus(Delegate::FAIL_VERSION);
238 bool Catalog::uninstall()
241 bool atLeastOneFailure = false;
243 BOOST_FOREACH(PackageRef p, installedPackages()) {
244 ok = p->existingInstall()->uninstall();
246 SG_LOG(SG_GENERAL, SG_WARN, "uninstall of package " <<
247 p->id() << " failed");
248 // continue trying other packages, bailing out here
250 atLeastOneFailure = true;
254 Dir d(m_installRoot);
255 ok = d.remove(true /* recursive */);
257 atLeastOneFailure = true;
260 changeStatus(atLeastOneFailure ? Delegate::FAIL_FILESYSTEM
261 : Delegate::STATUS_SUCCESS);
266 Catalog::packages() const
272 Catalog::packagesMatching(const SGPropertyNode* aFilter) const
275 BOOST_FOREACH(PackageRef p, m_packages) {
276 if (p->matches(aFilter)) {
284 Catalog::packagesNeedingUpdate() const
287 BOOST_FOREACH(PackageRef p, m_packages) {
288 if (!p->isInstalled()) {
292 if (p->install()->hasUpdate()) {
300 Catalog::installedPackages() const
303 BOOST_FOREACH(PackageRef p, m_packages) {
304 if (p->isInstalled()) {
311 void Catalog::refresh()
313 if (m_refreshRequest.valid()) {
314 // refresh in progress
318 Downloader* dl = new Downloader(this, url());
319 m_refreshRequest = dl;
320 // will update status to IN_PROGRESS
321 m_root->makeHTTPRequest(dl);
326 FindById(const std::string &id) : m_id(id) {}
328 bool operator()(const PackageRef& ref) const
330 return ref->id() == m_id;
336 void Catalog::parseProps(const SGPropertyNode* aProps)
338 // copy everything except package children?
339 m_props = new SGPropertyNode;
341 m_variantDict.clear(); // will rebuild during parse
342 std::set<PackageRef> orphans;
343 orphans.insert(m_packages.begin(), m_packages.end());
345 int nChildren = aProps->nChildren();
346 for (int i = 0; i < nChildren; i++) {
347 const SGPropertyNode* pkgProps = aProps->getChild(i);
348 if (strcmp(pkgProps->getName(), "package") == 0) {
349 // can't use getPackageById here becuase the variant dict isn't
350 // built yet. Instead we need to look at m_packages directly.
352 PackageList::iterator pit = std::find_if(m_packages.begin(), m_packages.end(),
353 FindById(pkgProps->getStringValue("id")));
355 if (pit != m_packages.end()) {
358 p->updateFromProps(pkgProps);
359 orphans.erase(p); // not an orphan
362 p = new Package(pkgProps, this);
363 m_packages.push_back(p);
366 string_list vars(p->variants());
367 for (string_list::iterator it = vars.begin(); it != vars.end(); ++it) {
368 m_variantDict[*it] = p.ptr();
371 SGPropertyNode* c = m_props->getChild(pkgProps->getName(), pkgProps->getIndex(), true);
372 copyProperties(pkgProps, c);
374 } // of children iteration
376 if (!orphans.empty()) {
377 SG_LOG(SG_GENERAL, SG_WARN, "have orphan packages: will become inaccesible");
378 std::set<PackageRef>::iterator it;
379 for (it = orphans.begin(); it != orphans.end(); ++it) {
380 SG_LOG(SG_GENERAL, SG_WARN, "\torphan package:" << (*it)->qualifiedId());
381 PackageList::iterator pit = std::find(m_packages.begin(), m_packages.end(), *it);
382 assert(pit != m_packages.end());
383 m_packages.erase(pit);
387 if (!m_url.empty()) {
388 if (m_url != m_props->getStringValue("url")) {
389 // this effectively allows packages to migrate to new locations,
390 // although if we're going to rely on that feature we should
391 // maybe formalise it!
392 SG_LOG(SG_GENERAL, SG_WARN, "package downloaded from:" << m_url
393 << " is now at: " << m_props->getStringValue("url"));
397 m_url = m_props->getStringValue("url");
399 if (m_installRoot.isNull()) {
400 m_installRoot = m_root->path();
401 m_installRoot.append(id());
403 Dir d(m_installRoot);
408 PackageRef Catalog::getPackageById(const std::string& aId) const
410 // search the variant dict here, so looking up aircraft variants
411 // works as expected.
412 PackageWeakMap::const_iterator it = m_variantDict.find(aId);
413 if (it == m_variantDict.end())
419 PackageRef Catalog::getPackageByPath(const std::string& aPath) const
421 PackageList::const_iterator it;
422 for (it = m_packages.begin(); it != m_packages.end(); ++it) {
423 if ((*it)->dirName() == aPath) {
431 std::string Catalog::id() const
433 return m_props->getStringValue("id");
436 std::string Catalog::url() const
441 void Catalog::setUrl(const std::string &url)
444 if (m_status == Delegate::FAIL_NOT_FOUND) {
445 m_status = Delegate::FAIL_UNKNOWN;
449 std::string Catalog::name() const
451 return getLocalisedString(m_props, "name");
454 std::string Catalog::description() const
456 return getLocalisedString(m_props, "description");
459 SGPropertyNode* Catalog::properties() const
461 return m_props.ptr();
464 void Catalog::parseTimestamp()
466 SGPath timestampFile = m_installRoot;
467 timestampFile.append(".timestamp");
468 std::ifstream f(timestampFile.c_str(), std::ios::in);
469 f >> m_retrievedTime;
472 void Catalog::writeTimestamp()
474 SGPath timestampFile = m_installRoot;
475 timestampFile.append(".timestamp");
476 std::ofstream f(timestampFile.c_str(), std::ios::out | std::ios::trunc);
477 f << m_retrievedTime << std::endl;
480 unsigned int Catalog::ageInSeconds() const
484 int diff = ::difftime(now, m_retrievedTime);
485 return (diff < 0) ? 0 : diff;
488 bool Catalog::needsRefresh() const
490 // always refresh in these cases
491 if ((m_status == Delegate::FAIL_VERSION) || (m_status == Delegate::FAIL_DOWNLOAD)) {
495 unsigned int maxAge = m_props->getIntValue("max-age-sec", m_root->maxAgeSeconds());
496 return (ageInSeconds() > maxAge);
499 std::string Catalog::getLocalisedString(const SGPropertyNode* aRoot, const char* aName) const
502 return std::string();
505 if (aRoot->hasChild(m_root->getLocale())) {
506 const SGPropertyNode* localeRoot = aRoot->getChild(m_root->getLocale().c_str());
507 if (localeRoot->hasChild(aName)) {
508 return localeRoot->getStringValue(aName);
512 return aRoot->getStringValue(aName);
515 void Catalog::refreshComplete(Delegate::StatusCode aReason)
517 m_refreshRequest.reset();
518 changeStatus(aReason);
521 void Catalog::changeStatus(Delegate::StatusCode newStatus)
523 if (m_status == newStatus) {
527 m_status = newStatus;
528 m_root->catalogRefreshStatus(this, newStatus);
529 m_statusCallbacks(this);
532 void Catalog::addStatusCallback(const Callback& cb)
534 m_statusCallbacks.push_back(cb);
537 Delegate::StatusCode Catalog::status() const
542 } // of namespace pkg
544 } // of namespace simgear