xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision 1978089f7cec856677e46204e992c7273d70b9af)
1 /*
2  * Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
3  * Copyright 2014, Axel Dörfler <axeld@pinc-software.de>.
4  * Copyright 2016-2024, Andrew Lindesay <apl@lindesay.co.nz>.
5  * All rights reserved. Distributed under the terms of the MIT License.
6  */
7 #include "Model.h"
8 
9 #include <algorithm>
10 #include <ctime>
11 #include <vector>
12 
13 #include <stdarg.h>
14 #include <time.h>
15 
16 #include <Autolock.h>
17 #include <Catalog.h>
18 #include <Directory.h>
19 #include <Entry.h>
20 #include <File.h>
21 #include <KeyStore.h>
22 #include <Locale.h>
23 #include <LocaleRoster.h>
24 #include <Message.h>
25 #include <Path.h>
26 
27 #include "HaikuDepotConstants.h"
28 #include "Logger.h"
29 #include "LocaleUtils.h"
30 #include "StorageUtils.h"
31 
32 
33 #undef B_TRANSLATION_CONTEXT
34 #define B_TRANSLATION_CONTEXT "Model"
35 
36 
37 #define KEY_STORE_IDENTIFIER_PREFIX "hds.password."
38 	// this prefix is added before the nickname in the keystore
39 	// so that HDS username/password pairs can be identified.
40 
41 static const char* kHaikuDepotKeyring = "HaikuDepot";
42 
43 
44 PackageFilter::~PackageFilter()
45 {
46 }
47 
48 
49 ModelListener::~ModelListener()
50 {
51 }
52 
53 
54 // #pragma mark - PackageFilters
55 
56 
57 class AnyFilter : public PackageFilter {
58 public:
59 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
60 	{
61 		return true;
62 	}
63 };
64 
65 
66 class CategoryFilter : public PackageFilter {
67 public:
68 	CategoryFilter(const BString& category)
69 		:
70 		fCategory(category)
71 	{
72 	}
73 
74 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
75 	{
76 		if (!package.IsSet())
77 			return false;
78 
79 		for (int i = package->CountCategories() - 1; i >= 0; i--) {
80 			const CategoryRef& category = package->CategoryAtIndex(i);
81 			if (!category.IsSet())
82 				continue;
83 			if (category->Code() == fCategory)
84 				return true;
85 		}
86 		return false;
87 	}
88 
89 	const BString& Category() const
90 	{
91 		return fCategory;
92 	}
93 
94 private:
95 	BString		fCategory;
96 };
97 
98 
99 class StateFilter : public PackageFilter {
100 public:
101 	StateFilter(PackageState state)
102 		:
103 		fState(state)
104 	{
105 	}
106 
107 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
108 	{
109 		return package->State() == NONE;
110 	}
111 
112 private:
113 	PackageState	fState;
114 };
115 
116 
117 class SearchTermsFilter : public PackageFilter {
118 public:
119 	SearchTermsFilter(const BString& searchTerms)
120 	{
121 		// Separate the string into terms at spaces
122 		int32 index = 0;
123 		while (index < searchTerms.Length()) {
124 			int32 nextSpace = searchTerms.FindFirst(" ", index);
125 			if (nextSpace < 0)
126 				nextSpace = searchTerms.Length();
127 			if (nextSpace > index) {
128 				BString term;
129 				searchTerms.CopyInto(term, index, nextSpace - index);
130 				term.ToLower();
131 				fSearchTerms.Add(term);
132 			}
133 			index = nextSpace + 1;
134 		}
135 	}
136 
137 	virtual bool AcceptsPackage(const PackageInfoRef& package) const
138 	{
139 		if (!package.IsSet())
140 			return false;
141 		// Every search term must be found in one of the package texts
142 		for (int32 i = fSearchTerms.CountStrings() - 1; i >= 0; i--) {
143 			const BString& term = fSearchTerms.StringAt(i);
144 			if (!_TextContains(package->Name(), term)
145 				&& !_TextContains(package->Title(), term)
146 				&& !_TextContains(package->Publisher().Name(), term)
147 				&& !_TextContains(package->ShortDescription(), term)
148 				&& !_TextContains(package->FullDescription(), term)) {
149 				return false;
150 			}
151 		}
152 		return true;
153 	}
154 
155 	BString SearchTerms() const
156 	{
157 		BString searchTerms;
158 		for (int32 i = 0; i < fSearchTerms.CountStrings(); i++) {
159 			const BString& term = fSearchTerms.StringAt(i);
160 			if (term.IsEmpty())
161 				continue;
162 			if (!searchTerms.IsEmpty())
163 				searchTerms.Append(" ");
164 			searchTerms.Append(term);
165 		}
166 		return searchTerms;
167 	}
168 
169 private:
170 	bool _TextContains(BString text, const BString& string) const
171 	{
172 		text.ToLower();
173 		int32 index = text.FindFirst(string);
174 		return index >= 0;
175 	}
176 
177 private:
178 	BStringList fSearchTerms;
179 };
180 
181 
182 static inline bool
183 is_source_package(const PackageInfoRef& package)
184 {
185 	const BString& packageName = package->Name();
186 	return packageName.EndsWith("_source");
187 }
188 
189 
190 static inline bool
191 is_develop_package(const PackageInfoRef& package)
192 {
193 	const BString& packageName = package->Name();
194 	return packageName.EndsWith("_devel")
195 		|| packageName.EndsWith("_debuginfo");
196 }
197 
198 
199 // #pragma mark - Model
200 
201 
202 Model::Model()
203 	:
204 	fDepots(),
205 	fCategories(),
206 	fCategoryFilter(PackageFilterRef(new AnyFilter(), true)),
207 	fDepotFilter(""),
208 	fSearchTermsFilter(PackageFilterRef(new AnyFilter(), true)),
209 	fPackageListViewMode(PROMINENT),
210 	fShowAvailablePackages(true),
211 	fShowInstalledPackages(true),
212 	fShowSourcePackages(false),
213 	fShowDevelopPackages(false),
214 	fCanShareAnonymousUsageData(false)
215 {
216 	fPackageScreenshotRepository = new PackageScreenshotRepository(
217 		PackageScreenshotRepositoryListenerRef(this),
218 		&fWebAppInterface);
219 }
220 
221 
222 Model::~Model()
223 {
224 	delete fPackageScreenshotRepository;
225 }
226 
227 
228 LanguageModel*
229 Model::Language()
230 {
231 	return &fLanguageModel;
232 }
233 
234 
235 PackageIconRepository&
236 Model::GetPackageIconRepository()
237 {
238 	return fPackageIconRepository;
239 }
240 
241 
242 status_t
243 Model::InitPackageIconRepository()
244 {
245 	BPath tarPath;
246 	status_t result = IconTarPath(tarPath);
247 	if (result == B_OK)
248 		result = fPackageIconRepository.Init(tarPath);
249 	return result;
250 }
251 
252 
253 PackageScreenshotRepository*
254 Model::GetPackageScreenshotRepository()
255 {
256 	return fPackageScreenshotRepository;
257 }
258 
259 
260 void
261 Model::AddListener(const ModelListenerRef& listener)
262 {
263 	fListeners.push_back(listener);
264 }
265 
266 
267 // TODO; part of a wider change; cope with the package being in more than one
268 // depot
269 PackageInfoRef
270 Model::PackageForName(const BString& name)
271 {
272 	std::vector<DepotInfoRef>::iterator it;
273 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
274 		DepotInfoRef depotInfoRef = *it;
275 		PackageInfoRef packageInfoRef = depotInfoRef->PackageByName(name);
276 		if (packageInfoRef.Get() != NULL)
277 			return packageInfoRef;
278 	}
279 	return PackageInfoRef();
280 }
281 
282 
283 bool
284 Model::MatchesFilter(const PackageInfoRef& package) const
285 {
286 	return fCategoryFilter->AcceptsPackage(package)
287 			&& fSearchTermsFilter->AcceptsPackage(package)
288 			&& (fDepotFilter.IsEmpty() || fDepotFilter == package->DepotName())
289 			&& (fShowAvailablePackages || package->State() != NONE)
290 			&& (fShowInstalledPackages || package->State() != ACTIVATED)
291 			&& (fShowSourcePackages || !is_source_package(package))
292 			&& (fShowDevelopPackages || !is_develop_package(package));
293 }
294 
295 
296 void
297 Model::MergeOrAddDepot(const DepotInfoRef& depot)
298 {
299 	BString depotName = depot->Name();
300 	for(uint32 i = 0; i < fDepots.size(); i++) {
301 		if (fDepots[i]->Name() == depotName) {
302 			DepotInfoRef ersatzDepot(new DepotInfo(*(fDepots[i].Get())), true);
303 			ersatzDepot->SyncPackagesFromDepot(depot);
304 			fDepots[i] = ersatzDepot;
305 			return;
306 		}
307 	}
308 	fDepots.push_back(depot);
309 }
310 
311 
312 bool
313 Model::HasDepot(const BString& name) const
314 {
315 	return NULL != DepotForName(name).Get();
316 }
317 
318 
319 const DepotInfoRef
320 Model::DepotForName(const BString& name) const
321 {
322 	std::vector<DepotInfoRef>::const_iterator it;
323 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
324 		DepotInfoRef aDepot = *it;
325 		if (aDepot->Name() == name)
326 			return aDepot;
327 	}
328 	return DepotInfoRef();
329 }
330 
331 
332 int32
333 Model::CountDepots() const
334 {
335 	return fDepots.size();
336 }
337 
338 
339 DepotInfoRef
340 Model::DepotAtIndex(int32 index) const
341 {
342 	return fDepots[index];
343 }
344 
345 
346 bool
347 Model::HasAnyProminentPackages()
348 {
349 	std::vector<DepotInfoRef>::iterator it;
350 	for (it = fDepots.begin(); it != fDepots.end(); it++) {
351 		DepotInfoRef aDepot = *it;
352 		if (aDepot->HasAnyProminentPackages())
353 			return true;
354 	}
355 	return false;
356 }
357 
358 
359 void
360 Model::Clear()
361 {
362 	GetPackageIconRepository().Clear();
363 	fDepots.clear();
364 	fPopulatedPackageNames.MakeEmpty();
365 }
366 
367 
368 void
369 Model::SetStateForPackagesByName(BStringList& packageNames, PackageState state)
370 {
371 	for (int32 i = 0; i < packageNames.CountStrings(); i++) {
372 		BString packageName = packageNames.StringAt(i);
373 		PackageInfoRef packageInfo = PackageForName(packageName);
374 
375 		if (packageInfo.IsSet()) {
376 			packageInfo->SetState(state);
377 			HDINFO("did update package [%s] with state [%s]",
378 				packageName.String(), package_state_to_string(state));
379 		}
380 		else {
381 			HDINFO("was unable to find package [%s] so was not possible to set"
382 				" the state to [%s]", packageName.String(),
383 				package_state_to_string(state));
384 		}
385 	}
386 }
387 
388 
389 // #pragma mark - filters
390 
391 
392 void
393 Model::SetCategory(const BString& category)
394 {
395 	PackageFilter* filter;
396 
397 	if (category.Length() == 0)
398 		filter = new AnyFilter();
399 	else
400 		filter = new CategoryFilter(category);
401 
402 	fCategoryFilter.SetTo(filter, true);
403 }
404 
405 
406 BString
407 Model::Category() const
408 {
409 	CategoryFilter* filter
410 		= dynamic_cast<CategoryFilter*>(fCategoryFilter.Get());
411 	if (filter == NULL)
412 		return "";
413 	return filter->Category();
414 }
415 
416 
417 void
418 Model::SetDepot(const BString& depot)
419 {
420 	fDepotFilter = depot;
421 }
422 
423 
424 BString
425 Model::Depot() const
426 {
427 	return fDepotFilter;
428 }
429 
430 
431 void
432 Model::SetSearchTerms(const BString& searchTerms)
433 {
434 	PackageFilter* filter;
435 
436 	if (searchTerms.Length() == 0)
437 		filter = new AnyFilter();
438 	else
439 		filter = new SearchTermsFilter(searchTerms);
440 
441 	fSearchTermsFilter.SetTo(filter, true);
442 }
443 
444 
445 BString
446 Model::SearchTerms() const
447 {
448 	SearchTermsFilter* filter
449 		= dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get());
450 	if (filter == NULL)
451 		return "";
452 	return filter->SearchTerms();
453 }
454 
455 
456 void
457 Model::SetPackageListViewMode(package_list_view_mode mode)
458 {
459 	fPackageListViewMode = mode;
460 }
461 
462 
463 void
464 Model::SetCanShareAnonymousUsageData(bool value)
465 {
466 	fCanShareAnonymousUsageData = value;
467 }
468 
469 
470 void
471 Model::SetShowAvailablePackages(bool show)
472 {
473 	fShowAvailablePackages = show;
474 }
475 
476 
477 void
478 Model::SetShowInstalledPackages(bool show)
479 {
480 	fShowInstalledPackages = show;
481 }
482 
483 
484 void
485 Model::SetShowSourcePackages(bool show)
486 {
487 	fShowSourcePackages = show;
488 }
489 
490 
491 void
492 Model::SetShowDevelopPackages(bool show)
493 {
494 	fShowDevelopPackages = show;
495 }
496 
497 
498 // #pragma mark - information retrieval
499 
500 /*!	It may transpire that the package has no corresponding record on the
501 	server side because the repository is not represented in the server.
502 	In such a case, there is little point in communicating with the server
503 	only to hear back that the package does not exist.
504 */
505 
506 bool
507 Model::CanPopulatePackage(const PackageInfoRef& package)
508 {
509 	const BString& depotName = package->DepotName();
510 
511 	if (depotName.IsEmpty())
512 		return false;
513 
514 	const DepotInfoRef& depot = DepotForName(depotName);
515 
516 	if (depot.Get() == NULL)
517 		return false;
518 
519 	return !depot->WebAppRepositoryCode().IsEmpty();
520 }
521 
522 
523 /*! Initially only superficial data is loaded from the server into the data
524     model of the packages.  When the package is viewed, additional data needs
525     to be populated including ratings.  This method takes care of that.
526 */
527 
528 void
529 Model::PopulatePackage(const PackageInfoRef& package, uint32 flags)
530 {
531 	HDTRACE("will populate package for [%s]", package->Name().String());
532 
533 	if (!CanPopulatePackage(package)) {
534 		HDINFO("unable to populate package [%s]", package->Name().String());
535 		return;
536 	}
537 
538 	// TODO: There should probably also be a way to "unpopulate" the
539 	// package information. Maybe a cache of populated packages, so that
540 	// packages loose their extra information after a certain amount of
541 	// time when they have not been accessed/displayed in the UI. Otherwise
542 	// HaikuDepot will consume more and more resources in the packages.
543 	{
544 		BAutolock locker(&fLock);
545 		bool alreadyPopulated = fPopulatedPackageNames.HasString(
546 			package->Name());
547 		if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated)
548 			return;
549 		if (!alreadyPopulated)
550 			fPopulatedPackageNames.Add(package->Name());
551 	}
552 
553 	if ((flags & POPULATE_CHANGELOG) != 0 && package->HasChangelog()) {
554 		_PopulatePackageChangelog(package);
555 	}
556 
557 	if ((flags & POPULATE_USER_RATINGS) != 0) {
558 		// Retrieve info from web-app
559 		BMessage info;
560 
561 		BString packageName;
562 		BString webAppRepositoryCode;
563 		BString webAppRepositorySourceCode;
564 
565 		{
566 			BAutolock locker(&fLock);
567 			packageName = package->Name();
568 			const DepotInfo* depot = DepotForName(package->DepotName());
569 
570 			if (depot != NULL) {
571 				webAppRepositoryCode = depot->WebAppRepositoryCode();
572 				webAppRepositorySourceCode
573 					= depot->WebAppRepositorySourceCode();
574 			}
575 		}
576 
577 		status_t status = fWebAppInterface
578 			.RetrieveUserRatingsForPackageForDisplay(packageName,
579 				webAppRepositoryCode, webAppRepositorySourceCode, 0,
580 				PACKAGE_INFO_MAX_USER_RATINGS, info);
581 		if (status == B_OK) {
582 			// Parse message
583 			BMessage result;
584 			BMessage items;
585 			if (info.FindMessage("result", &result) == B_OK
586 				&& result.FindMessage("items", &items) == B_OK) {
587 
588 				BAutolock locker(&fLock);
589 				package->ClearUserRatings();
590 
591 				int32 index = 0;
592 				while (true) {
593 					BString name;
594 					name << index++;
595 
596 					BMessage item;
597 					if (items.FindMessage(name, &item) != B_OK)
598 						break;
599 
600 					BString code;
601 					if (item.FindString("code", &code) != B_OK) {
602 						HDERROR("corrupt user rating at index %" B_PRIi32,
603 							index);
604 						continue;
605 					}
606 
607 					BString user;
608 					BMessage userInfo;
609 					if (item.FindMessage("user", &userInfo) != B_OK
610 							|| userInfo.FindString("nickname", &user) != B_OK) {
611 						HDERROR("ignored user rating [%s] without a user "
612 							"nickname", code.String());
613 						continue;
614 					}
615 
616 					// Extract basic info, all items are optional
617 					BString languageCode;
618 					BString comment;
619 					double rating;
620 					item.FindString("naturalLanguageCode", &languageCode);
621 					item.FindString("comment", &comment);
622 					if (item.FindDouble("rating", &rating) != B_OK)
623 						rating = -1;
624 					if (comment.Length() == 0 && rating == -1) {
625 						HDERROR("rating [%s] has no comment or rating so will"
626 							" be ignored", code.String());
627 						continue;
628 					}
629 
630 					// For which version of the package was the rating?
631 					BString major = "?";
632 					BString minor = "?";
633 					BString micro = "";
634 					double revision = -1;
635 					BString architectureCode = "";
636 					BMessage version;
637 					if (item.FindMessage("pkgVersion", &version) == B_OK) {
638 						version.FindString("major", &major);
639 						version.FindString("minor", &minor);
640 						version.FindString("micro", &micro);
641 						version.FindDouble("revision", &revision);
642 						version.FindString("architectureCode",
643 							&architectureCode);
644 					}
645 					BString versionString = major;
646 					versionString << ".";
647 					versionString << minor;
648 					if (!micro.IsEmpty()) {
649 						versionString << ".";
650 						versionString << micro;
651 					}
652 					if (revision > 0) {
653 						versionString << "-";
654 						versionString << (int) revision;
655 					}
656 
657 					if (!architectureCode.IsEmpty()) {
658 						versionString << " " << STR_MDASH << " ";
659 						versionString << architectureCode;
660 					}
661 
662 					double createTimestamp;
663 					item.FindDouble("createTimestamp", &createTimestamp);
664 
665 					// Add the rating to the PackageInfo
666 					UserRatingRef userRating(new UserRating(
667 						UserInfo(user), rating,
668 						comment, languageCode, versionString,
669 						(uint64) createTimestamp), true);
670 					package->AddUserRating(userRating);
671 					HDDEBUG("rating [%s] retrieved from server", code.String());
672 				}
673 				HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]",
674 						index - 1, packageName.String());
675 			} else {
676 				BString message;
677 				message.SetToFormat("failure to retrieve user ratings for [%s]",
678 					packageName.String());
679 				_MaybeLogJsonRpcError(info, message.String());
680 			}
681 		} else
682 			HDERROR("unable to retrieve user ratings");
683 	}
684 }
685 
686 
687 void
688 Model::_PopulatePackageChangelog(const PackageInfoRef& package)
689 {
690 	BMessage info;
691 	BString packageName;
692 
693 	{
694 		BAutolock locker(&fLock);
695 		packageName = package->Name();
696 	}
697 
698 	status_t status = fWebAppInterface.GetChangelog(packageName, info);
699 
700 	if (status == B_OK) {
701 		// Parse message
702 		BMessage result;
703 		BString content;
704 		if (info.FindMessage("result", &result) == B_OK) {
705 			if (result.FindString("content", &content) == B_OK
706 				&& 0 != content.Length()) {
707 				BAutolock locker(&fLock);
708 				package->SetChangelog(content);
709 				HDDEBUG("changelog populated for [%s]", packageName.String());
710 			} else
711 				HDDEBUG("no changelog present for [%s]", packageName.String());
712 		} else
713 			_MaybeLogJsonRpcError(info, "populate package changelog");
714 	} else {
715 		HDERROR("unable to obtain the changelog for the package [%s]",
716 			packageName.String());
717 	}
718 }
719 
720 
721 static void
722 model_remove_key_for_user(const BString& nickname)
723 {
724 	if (nickname.IsEmpty())
725 		return;
726 	BKeyStore keyStore;
727 	BPasswordKey key;
728 	BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
729 		<< nickname;
730 	status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
731 			passwordIdentifier, key);
732 
733 	switch (result) {
734 		case B_OK:
735 			result = keyStore.RemoveKey(kHaikuDepotKeyring, key);
736 			if (result != B_OK) {
737 				HDERROR("error occurred when removing password for nickname "
738 					"[%s] : %s", nickname.String(), strerror(result));
739 			}
740 			break;
741 		case B_ENTRY_NOT_FOUND:
742 			return;
743 		default:
744 			HDERROR("error occurred when finding password for nickname "
745 				"[%s] : %s", nickname.String(), strerror(result));
746 			break;
747 	}
748 }
749 
750 
751 void
752 Model::SetNickname(BString nickname)
753 {
754 	BString password;
755 	BString existingNickname = Nickname();
756 
757 	// this happens when the user is logging out.  Best to remove the password
758 	// stored for the existing user since it is no longer required.
759 
760 	if (!existingNickname.IsEmpty() && nickname.IsEmpty())
761 		model_remove_key_for_user(existingNickname);
762 
763 	if (nickname.Length() > 0) {
764 		BPasswordKey key;
765 		BKeyStore keyStore;
766 		BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
767 			<< nickname;
768 		if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
769 				passwordIdentifier, key) == B_OK) {
770 			password = key.Password();
771 		}
772 		if (password.IsEmpty())
773 			nickname = "";
774 	}
775 
776 	SetCredentials(nickname, password, false);
777 }
778 
779 
780 const BString&
781 Model::Nickname()
782 {
783 	return fWebAppInterface.Nickname();
784 }
785 
786 
787 void
788 Model::SetCredentials(const BString& nickname, const BString& passwordClear,
789 	bool storePassword)
790 {
791 	BString existingNickname = Nickname();
792 
793 	if (storePassword) {
794 		// no point continuing to store the password for the previous user.
795 
796 		if (!existingNickname.IsEmpty())
797 			model_remove_key_for_user(existingNickname);
798 
799 		// adding a key that is already there does not seem to override the
800 		// existing key so the old key needs to be removed first.
801 
802 		if (!nickname.IsEmpty())
803 			model_remove_key_for_user(nickname);
804 
805 		if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) {
806 			BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
807 				<< nickname;
808 			BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier);
809 			BKeyStore keyStore;
810 			keyStore.AddKeyring(kHaikuDepotKeyring);
811 			keyStore.AddKey(kHaikuDepotKeyring, key);
812 		}
813 	}
814 
815 	BAutolock locker(&fLock);
816 	fWebAppInterface.SetCredentials(UserCredentials(nickname, passwordClear));
817 
818 	if (nickname != existingNickname)
819 		_NotifyAuthorizationChanged();
820 }
821 
822 
823 /*! When bulk repository data comes down from the server, it will
824     arrive as a json.gz payload.  This is stored locally as a cache
825     and this method will provide the on-disk storage location for
826     this file.
827 */
828 
829 status_t
830 Model::DumpExportRepositoryDataPath(BPath& path)
831 {
832 	BString leaf;
833 	leaf.SetToFormat("repository-all_%s.json.gz",
834 		Language()->PreferredLanguage()->Code());
835 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
836 }
837 
838 
839 /*! When the system downloads reference data (eg; categories) from the server
840     then the downloaded data is stored and cached at the path defined by this
841     method.
842 */
843 
844 status_t
845 Model::DumpExportReferenceDataPath(BPath& path)
846 {
847 	BString leaf;
848 	leaf.SetToFormat("reference-all_%s.json.gz",
849 		Language()->PreferredLanguage()->Code());
850 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
851 }
852 
853 
854 status_t
855 Model::IconTarPath(BPath& path) const
856 {
857 	return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path);
858 }
859 
860 
861 status_t
862 Model::DumpExportPkgDataPath(BPath& path,
863 	const BString& repositorySourceCode)
864 {
865 	BString leaf;
866 	leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(),
867 		Language()->PreferredLanguage()->Code());
868 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
869 }
870 
871 
872 // #pragma mark - listener notification methods
873 
874 
875 void
876 Model::_NotifyAuthorizationChanged()
877 {
878 	std::vector<ModelListenerRef>::const_iterator it;
879 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
880 		const ModelListenerRef& listener = *it;
881 		if (listener.IsSet())
882 			listener->AuthorizationChanged();
883 	}
884 }
885 
886 
887 void
888 Model::_NotifyCategoryListChanged()
889 {
890 	std::vector<ModelListenerRef>::const_iterator it;
891 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
892 		const ModelListenerRef& listener = *it;
893 		if (listener.IsSet())
894 			listener->CategoryListChanged();
895 	}
896 }
897 
898 
899 void
900 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
901 	const char *sourceDescription) const
902 {
903 	BMessage error;
904 	BString errorMessage;
905 	double errorCode;
906 
907 	if (responsePayload.FindMessage("error", &error) == B_OK
908 		&& error.FindString("message", &errorMessage) == B_OK
909 		&& error.FindDouble("code", &errorCode) == B_OK) {
910 		HDERROR("[%s] --> error : [%s] (%f)", sourceDescription,
911 			errorMessage.String(), errorCode);
912 	} else
913 		HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
914 }
915 
916 
917 // #pragma mark - Rating Stabilities
918 
919 
920 int32
921 Model::CountRatingStabilities() const
922 {
923 	return fRatingStabilities.size();
924 }
925 
926 
927 RatingStabilityRef
928 Model::RatingStabilityByCode(BString& code) const
929 {
930 	std::vector<RatingStabilityRef>::const_iterator it;
931 	for (it = fRatingStabilities.begin(); it != fRatingStabilities.end();
932 			it++) {
933 		RatingStabilityRef aRatingStability = *it;
934 		if (aRatingStability->Code() == code)
935 			return aRatingStability;
936 	}
937 	return RatingStabilityRef();
938 }
939 
940 
941 RatingStabilityRef
942 Model::RatingStabilityAtIndex(int32 index) const
943 {
944 	return fRatingStabilities[index];
945 }
946 
947 
948 void
949 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values)
950 {
951 	std::vector<RatingStabilityRef>::const_iterator it;
952 	for (it = values.begin(); it != values.end(); it++)
953 		_AddRatingStability(*it);
954 }
955 
956 
957 void
958 Model::_AddRatingStability(const RatingStabilityRef& value)
959 {
960 	std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst
961 		= std::lower_bound(
962 			fRatingStabilities.begin(),
963 			fRatingStabilities.end(),
964 			value,
965 			&IsRatingStabilityBefore);
966 	std::vector<RatingStabilityRef>::iterator itInsertionPt =
967 		fRatingStabilities.begin()
968 			+ (itInsertionPtConst - fRatingStabilities.begin());
969 
970 	if (itInsertionPt != fRatingStabilities.end()
971 		&& (*itInsertionPt)->Code() == value->Code()) {
972 		itInsertionPt = fRatingStabilities.erase(itInsertionPt);
973 			// replace the one with the same code.
974 	}
975 
976 	fRatingStabilities.insert(itInsertionPt, value);
977 }
978 
979 
980 // #pragma mark - Categories
981 
982 
983 int32
984 Model::CountCategories() const
985 {
986 	return fCategories.size();
987 }
988 
989 
990 CategoryRef
991 Model::CategoryByCode(BString& code) const
992 {
993 	std::vector<CategoryRef>::const_iterator it;
994 	for (it = fCategories.begin(); it != fCategories.end(); it++) {
995 		CategoryRef aCategory = *it;
996 		if (aCategory->Code() == code)
997 			return aCategory;
998 	}
999 	return CategoryRef();
1000 }
1001 
1002 
1003 CategoryRef
1004 Model::CategoryAtIndex(int32 index) const
1005 {
1006 	return fCategories[index];
1007 }
1008 
1009 
1010 void
1011 Model::AddCategories(std::vector<CategoryRef>& values)
1012 {
1013 	std::vector<CategoryRef>::iterator it;
1014 	for (it = values.begin(); it != values.end(); it++)
1015 		_AddCategory(*it);
1016 	_NotifyCategoryListChanged();
1017 }
1018 
1019 /*! This will insert the category in order.
1020  */
1021 
1022 void
1023 Model::_AddCategory(const CategoryRef& category)
1024 {
1025 	std::vector<CategoryRef>::const_iterator itInsertionPtConst
1026 		= std::lower_bound(
1027 			fCategories.begin(),
1028 			fCategories.end(),
1029 			category,
1030 			&IsPackageCategoryBefore);
1031 	std::vector<CategoryRef>::iterator itInsertionPt =
1032 		fCategories.begin() + (itInsertionPtConst - fCategories.begin());
1033 
1034 	if (itInsertionPt != fCategories.end()
1035 		&& (*itInsertionPt)->Code() == category->Code()) {
1036 		itInsertionPt = fCategories.erase(itInsertionPt);
1037 			// replace the one with the same code.
1038 	}
1039 
1040 	fCategories.insert(itInsertionPt, category);
1041 }
1042 
1043 
1044 void
1045 Model::ScreenshotCached(const ScreenshotCoordinate& coord)
1046 {
1047 	std::vector<ModelListenerRef>::const_iterator it;
1048 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
1049 		const ModelListenerRef& listener = *it;
1050 		if (listener.IsSet())
1051 			listener->ScreenshotCached(coord);
1052 	}
1053 }
1054