1 // Copyright (C) 2013  James Turner - zakalawe@mac.com
2 //
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.
7 //
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.
12 //
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.
16 //
17 
18 #include <simgear_config.h>
19 
20 #include <simgear/package/Package.hxx>
21 
22 #include <algorithm>
23 #include <cassert>
24 
25 #include <simgear/debug/logstream.hxx>
26 #include <simgear/structure/exception.hxx>
27 
28 #include <simgear/package/Catalog.hxx>
29 #include <simgear/package/Install.hxx>
30 #include <simgear/package/Root.hxx>
31 
32 namespace simgear {
33 
34 namespace pkg {
35 
Package(const SGPropertyNode * aProps,CatalogRef aCatalog)36 Package::Package(const SGPropertyNode* aProps, CatalogRef aCatalog) :
37     m_catalog(aCatalog.get())
38 {
39     initWithProps(aProps);
40 }
41 
initWithProps(const SGPropertyNode * aProps)42 void Package::initWithProps(const SGPropertyNode* aProps)
43 {
44     m_props = const_cast<SGPropertyNode*>(aProps);
45 // cache tag values
46     for (auto c : aProps->getChildren("tag")) {
47       m_tags.insert (strutils::lowercase (c->getStringValue()));
48     }
49 
50     m_id = m_props->getStringValue("id");
51 
52     m_variants.push_back(m_id);
53     for (auto var : m_props->getChildren("variant")) {
54         m_variants.push_back(var->getStringValue("id"));
55     }
56 }
57 
updateFromProps(const SGPropertyNode * aProps)58 void Package::updateFromProps(const SGPropertyNode* aProps)
59 {
60     m_tags.clear();
61     m_variants.clear();
62     initWithProps(aProps);
63 }
64 
matches(const SGPropertyNode * aFilter) const65 bool Package::matches(const SGPropertyNode* aFilter) const
66 {
67     const std::string& filter_name = aFilter->getNameString();
68 
69     if (filter_name == "any-of") {
70         const int anyChildren = aFilter->nChildren();
71         for (int j = 0; j < anyChildren; j++) {
72             const SGPropertyNode* anyChild = aFilter->getChild(j);
73             if (matches(anyChild)) {
74                 return true;
75             }
76         }
77 
78         return false; // none of our children matched
79     } else if (filter_name.empty() || (filter_name == "all-of")) {
80         const int allChildren = aFilter->nChildren();
81         for (int j = 0; j < allChildren; j++) {
82             const SGPropertyNode* allChild = aFilter->getChild(j);
83             if (!matches(allChild)) {
84                 return false;
85             }
86         }
87 
88         return true; // all of our children matched
89     }
90 
91     if (strutils::starts_with(filter_name, "rating-")) {
92         int minRating = aFilter->getIntValue();
93         std::string rname = aFilter->getName() + 7;
94         int ourRating = m_props->getChild("rating")->getIntValue(rname, 0);
95         return (ourRating >= minRating);
96     }
97 
98     if (filter_name == "tag") {
99         const std::string tag = strutils::lowercase (aFilter->getStringValue());
100         return (m_tags.find(tag) != m_tags.end());
101     }
102 
103     if (filter_name == "installed") {
104         return (isInstalled() == aFilter->getBoolValue());
105     }
106 
107     bool handled = false;
108     // substring search of name, description, across variants too
109     if ((filter_name == "text") || (filter_name == "name")) {
110         handled = true;
111       const std::string n = strutils::lowercase (aFilter->getStringValue());
112 
113       const size_t pos = strutils::lowercase (name()).find(n);
114       if (pos != std::string::npos) {
115         return true;
116       }
117 
118       for (auto var : m_props->getChildren("variant")) {
119           if (var->hasChild("name")) {
120               const std::string variantName = strutils::lowercase (var->getStringValue("name"));
121               size_t pos = variantName.find(n);
122               if (pos != std::string::npos) {
123                   return true;
124               }
125           }
126       }
127     }
128 
129     if ((filter_name == "text") || (filter_name == "description")) {
130         handled = true;
131         if (matchesDescription(aFilter->getStringValue())) {
132             return true;
133         }
134     }
135 
136     if (!handled) {
137       SG_LOG(SG_GENERAL, SG_WARN, "unknown filter term:" << filter_name);
138     }
139 
140     return false;
141 }
142 
matchesDescription(const std::string & search) const143 bool Package::matchesDescription(const std::string &search) const
144 {
145     const std::string n = strutils::lowercase (search);
146 
147     bool localized;
148     const auto d = strutils::lowercase (getLocalisedString(m_props, "description", &localized));
149     if (d.find(n) != std::string::npos) {
150         return true;
151     }
152 
153     // try non-localized description too, if the abovce was a localized one
154     if (localized) {
155         const std::string baseDesc = m_props->getStringValue("description");
156         const auto pos = strutils::lowercase (baseDesc).find(n);
157         if (pos != std::string::npos) {
158             return true;
159         }
160     }
161 
162     // try each variant's description
163     for (auto var : m_props->getChildren("variant")) {
164         const auto vd = strutils::lowercase (getLocalisedString(var, "description", &localized));
165         if (!vd.empty()) {
166             if (vd.find(n) != std::string::npos) {
167                 return true;
168             }
169         }
170 
171         if (localized) {
172             // try non-localized variant description
173             const std::string vd = strutils::lowercase (var->getStringValue("description"));
174             if (vd.find(n) != std::string::npos) {
175                 return true;
176             }
177         }
178     } // of variant iteration
179 
180     return false;
181 }
182 
isInstalled() const183 bool Package::isInstalled() const
184 {
185     // anything to check for? look for a valid revision file?
186     return pathOnDisk().exists();
187 }
188 
pathOnDisk() const189 SGPath Package::pathOnDisk() const
190 {
191     SGPath p(m_catalog->installRoot());
192     p.append("Aircraft");
193     p.append(dirName());
194     return p;
195 }
196 
install()197 InstallRef Package::install()
198 {
199     InstallRef ins = existingInstall();
200     if (ins) {
201       // if there's updates, treat this as a 'start update' request
202       if (ins->hasUpdate()) {
203         m_catalog->root()->scheduleToUpdate(ins);
204       }
205 
206         return ins;
207     }
208 
209   // start a new install
210     ins = new Install(this, pathOnDisk());
211     m_catalog->root()->scheduleToUpdate(ins);
212 
213     _install_cb(this, ins);
214 
215     return ins;
216 }
217 
markForInstall()218 InstallRef Package::markForInstall() {
219   InstallRef ins = existingInstall();
220   if (ins) {
221     return ins;
222   }
223 
224   const auto pd = pathOnDisk();
225 
226   Dir dir(pd);
227   if (!dir.create(0700)) {
228     SG_LOG(SG_IO, SG_ALERT,
229            "Package::markForInstall: couldn't create directory at:" << pd);
230     return {};
231   }
232 
233   ins = new Install{this, pd};
234   _install_cb(this, ins); // not sure if we should trigger the callback for this
235 
236   // repeat for dependencies to be kind
237   for (auto dep : dependencies()) {
238     dep->markForInstall();
239   }
240 
241   return ins;
242 }
243 
existingInstall(const InstallCallback & cb) const244 InstallRef Package::existingInstall(const InstallCallback& cb) const
245 {
246     InstallRef install;
247     try {
248         install = m_catalog->root()->existingInstallForPackage(const_cast<Package*>(this));
249     } catch (std::exception& ) {
250       return {};
251     }
252 
253   if( cb )
254   {
255     _install_cb.push_back(cb);
256 
257     if( install )
258       cb(const_cast<Package*>(this), install);
259   }
260 
261   return install;
262 }
263 
id() const264 std::string Package::id() const
265 {
266     return m_id;
267 }
268 
catalog() const269 CatalogRef Package::catalog() const
270 {
271     return {m_catalog};
272 }
273 
qualifiedId() const274 std::string Package::qualifiedId() const
275 {
276     return m_catalog->id() + "." + id();
277 }
278 
qualifiedVariantId(const unsigned int variantIndex) const279 std::string Package::qualifiedVariantId(const unsigned int variantIndex) const
280 {
281     if (variantIndex >= m_variants.size()) {
282         throw sg_range_exception("invalid variant index " + std::to_string(variantIndex));
283     }
284     return m_catalog->id() + "." + m_variants[variantIndex];
285 }
286 
md5() const287 std::string Package::md5() const
288 {
289     return m_props->getStringValue("md5");
290 }
291 
dirName() const292 std::string Package::dirName() const
293 {
294     std::string r(m_props->getStringValue("dir"));
295     if (r.empty())
296         throw sg_exception("missing dir property on catalog package entry for " + m_id);
297     return r;
298 }
299 
revision() const300 unsigned int Package::revision() const
301 {
302     if (!m_props) {
303         return 0;
304     }
305 
306     return m_props->getIntValue("revision");
307 }
308 
name() const309 std::string Package::name() const
310 {
311     return m_props->getStringValue("name");
312 }
313 
fileSizeBytes() const314 size_t Package::fileSizeBytes() const
315 {
316     return m_props->getIntValue("file-size-bytes");
317 }
318 
description() const319 std::string Package::description() const
320 {
321     return getLocalisedProp("description", 0);
322 }
323 
tags() const324 string_set Package::tags() const
325 {
326     return m_tags;
327 }
328 
hasTag(const std::string & tag) const329 bool Package::hasTag(const std::string& tag) const
330 {
331     return m_tags.find(tag) != m_tags.end();
332 }
333 
properties() const334 SGPropertyNode* Package::properties() const
335 {
336     return m_props.ptr();
337 }
338 
thumbnailUrls() const339 string_list Package::thumbnailUrls() const
340 {
341     string_list urls;
342     const Thumbnail& thumb(thumbnailForVariant(0));
343     if (!thumb.url.empty()) {
344         urls.push_back(thumb.url);
345     }
346     return urls;
347 }
348 
downloadUrls() const349 string_list Package::downloadUrls() const
350 {
351     string_list r;
352     if (!m_props) {
353         return r;
354     }
355 
356     for (auto dl : m_props->getChildren("url")) {
357         r.push_back(dl->getStringValue());
358     }
359     return r;
360 }
361 
getLocalisedProp(const std::string & aName,const unsigned int vIndex) const362 std::string Package::getLocalisedProp(const std::string& aName, const unsigned int vIndex) const
363 {
364     return getLocalisedString(propsForVariant(vIndex, aName.c_str()), aName.c_str());
365 }
366 
getLocalisedString(const SGPropertyNode * aRoot,const char * aName,bool * isLocalized) const367 std::string Package::getLocalisedString(const SGPropertyNode* aRoot, const char* aName, bool* isLocalized) const
368 {
369     // we used to place localised strings under /sim/<locale>/name - but this
370     // potentially pollutes the /sim namespace
371     // we now check first in /sim/localized/<locale>/name first
372     const auto& locale = m_catalog->root()->getLocale();
373     if (isLocalized) *isLocalized = false;
374 
375     if (locale.empty()) {
376         return aRoot->getStringValue(aName);
377     }
378 
379     const SGPropertyNode* localeRoot;
380     if (aRoot->hasChild("localized")) {
381         localeRoot = aRoot->getChild("localized")->getChild(locale);
382     } else {
383         // old behaviour where locale nodes are directly beneath /sim
384         localeRoot = aRoot->getChild(locale);
385     }
386 
387     if (localeRoot && localeRoot->hasChild(aName)) {
388         if (isLocalized) *isLocalized = true;
389         return localeRoot->getStringValue(aName);
390     }
391 
392     return aRoot->getStringValue(aName);
393 }
394 
dependencies() const395 PackageList Package::dependencies() const
396 {
397     PackageList result;
398 
399     for (auto dep : m_props->getChildren("depends")) {
400         std::string depName = dep->getStringValue("id");
401         unsigned int rev = dep->getIntValue("revision", 0);
402 
403     // prefer local hangar package if possible, in case someone does something
404     // silly with naming. Of course flightgear's aircraft search doesn't know
405     // about hangars, so names still need to be unique.
406         PackageRef depPkg = m_catalog->getPackageById(depName);
407         if (!depPkg) {
408             Root* rt = m_catalog->root();
409             depPkg = rt->getPackageById(depName);
410             if (!depPkg) {
411                 throw sg_exception("Couldn't satisfy dependency of " + id() + " : " + depName);
412             }
413         }
414 
415         if (depPkg->revision() < rev) {
416             throw sg_range_exception("Couldn't find suitable revision of " + depName);
417         }
418 
419     // forbid recursive dependency graphs, we don't need that level
420     // of complexity for aircraft resources
421         assert(depPkg->dependencies() == PackageList());
422 
423         result.push_back(depPkg);
424     }
425 
426     return result;
427 }
428 
variants() const429 string_list Package::variants() const
430 {
431     return m_variants;
432 }
433 
nameForVariant(const std::string & vid) const434 std::string Package::nameForVariant(const std::string& vid) const
435 {
436     if (vid == id()) {
437         return name();
438     }
439 
440     for (auto var : m_props->getChildren("variant")) {
441         if (vid == var->getStringValue("id")) {
442             return var->getStringValue("name");
443         }
444     }
445 
446 
447     throw sg_exception("Unknow variant +" + vid + " in package " + id());
448 }
449 
indexOfVariant(const std::string & vid) const450 unsigned int Package::indexOfVariant(const std::string& vid) const
451 {
452     // accept fully-qualified IDs here
453     std::string actualId = vid;
454     size_t lastDot = vid.rfind('.');
455     if (lastDot != std::string::npos) {
456         std::string catalogId = vid.substr(0, lastDot);
457         if (catalogId != catalog()->id()) {
458             throw sg_exception("Bad fully-qualified ID:" + vid + ", package mismatch" );
459         }
460         actualId = vid.substr(lastDot + 1);
461     }
462 
463     string_list::const_iterator it = std::find(m_variants.begin(), m_variants.end(), actualId);
464     if (it == m_variants.end()) {
465         throw sg_exception("Unknow variant " + vid + " in package " + id());
466     }
467 
468     return std::distance(m_variants.begin(), it);
469 }
470 
nameForVariant(const unsigned int vIndex) const471 std::string Package::nameForVariant(const unsigned int vIndex) const
472 {
473     return propsForVariant(vIndex, "name")->getStringValue("name");
474 }
475 
propsForVariant(const unsigned int vIndex,const char * propName) const476 SGPropertyNode_ptr Package::propsForVariant(const unsigned int vIndex, const char* propName) const
477 {
478     if (vIndex == 0) {
479         return m_props;
480     }
481 
482     // offset by minus one to allow for index 0 being the primary
483     SGPropertyNode_ptr var = m_props->getChild("variant", vIndex - 1);
484     if (var) {
485         if (!propName || var->hasChild(propName)) {
486             return var;
487         }
488 
489         return m_props;
490     }
491 
492     throw sg_exception("Unknown variant in package " + id());
493 }
494 
parentIdForVariant(unsigned int variantIndex) const495 std::string Package::parentIdForVariant(unsigned int variantIndex) const
496 {
497     const std::string parentId = propsForVariant(variantIndex)->getStringValue("variant-of");
498     if ((variantIndex == 0) || (parentId == "_package_")) {
499         return std::string();
500     }
501 
502     if (parentId.empty()) {
503         // this is a variant without a variant-of, so assume its parent is
504         // the first primary
505         return m_variants.front();
506     }
507 
508     assert(indexOfVariant(parentId) >= 0);
509     return parentId;
510 }
511 
primaryVariants() const512 string_list Package::primaryVariants() const
513 {
514     string_list result;
515     for (unsigned int v = 0; v < m_variants.size(); ++v) {
516         const auto pr = parentIdForVariant(v);
517         if (pr.empty()) {
518             result.push_back(m_variants.at(v));
519         }
520     }
521     assert(!result.empty());
522     assert(result.front() == id());
523     return result;
524 }
525 
thumbnailForVariant(unsigned int vIndex) const526 Package::Thumbnail Package::thumbnailForVariant(unsigned int vIndex) const
527 {
528     SGPropertyNode_ptr var = propsForVariant(vIndex);
529     // allow for variants without distinct thumbnails
530     if (!var->hasChild("thumbnail") || !var->hasChild("thumbnail-path")) {
531         var = m_props;
532     }
533 
534     return {var->getStringValue("thumbnail"), var->getStringValue("thumbnail-path")};
535 }
536 
previewsForVariant(unsigned int vIndex) const537 Package::PreviewVec Package::previewsForVariant(unsigned int vIndex) const
538 {
539     SGPropertyNode_ptr var = propsForVariant(vIndex);
540     return previewsFromProps(var);
541 }
542 
previewTypeFromString(const std::string & s)543 Package::Preview::Type previewTypeFromString(const std::string& s)
544 {
545     if (s == "exterior") return Package::Preview::Type::EXTERIOR;
546     if (s == "interior") return Package::Preview::Type::INTERIOR;
547     if (s == "panel") return Package::Preview::Type::PANEL;
548     return Package::Preview::Type::UNKNOWN;
549 }
550 
Preview(const std::string & aUrl,const std::string & aPath,Type aType)551 Package::Preview::Preview(const std::string& aUrl, const std::string& aPath, Type aType) :
552     url(aUrl),
553     path(aPath),
554     type(aType)
555 {
556 }
557 
previewsFromProps(const SGPropertyNode_ptr & ptr) const558 Package::PreviewVec Package::previewsFromProps(const SGPropertyNode_ptr& ptr) const
559 {
560     PreviewVec result;
561 
562     for (auto thumbNode : ptr->getChildren("preview")) {
563         Preview t(thumbNode->getStringValue("url"),
564                     thumbNode->getStringValue("path"),
565                     previewTypeFromString(thumbNode->getStringValue("type")));
566         result.push_back(t);
567     }
568 
569     return result;
570 }
571 
validate() const572 bool Package::validate() const
573 {
574     if (m_id.empty())
575         return false;
576 
577     std::string nm(m_props->getStringValue("name"));
578     if (nm.empty())
579         return false;
580 
581     std::string dir(m_props->getStringValue("dir"));
582     if (dir.empty())
583         return false;
584 
585     return true;
586 }
587 
588 
589 } // of namespace pkg
590 
591 } // of namespace simgear
592