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