xref: /haiku/src/apps/haikudepot/model/Model.cpp (revision f758e73fe6df01190d54716802d51635b609e1fd)
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,
669 						languageCode,
670 							// note that language identifiers are "code" in HDS and "id" in Haiku
671 						versionString,
672 						(uint64) createTimestamp), true);
673 					package->AddUserRating(userRating);
674 					HDDEBUG("rating [%s] retrieved from server", code.String());
675 				}
676 				HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]",
677 						index - 1, packageName.String());
678 			} else {
679 				BString message;
680 				message.SetToFormat("failure to retrieve user ratings for [%s]",
681 					packageName.String());
682 				_MaybeLogJsonRpcError(info, message.String());
683 			}
684 		} else
685 			HDERROR("unable to retrieve user ratings");
686 	}
687 }
688 
689 
690 void
691 Model::_PopulatePackageChangelog(const PackageInfoRef& package)
692 {
693 	BMessage info;
694 	BString packageName;
695 
696 	{
697 		BAutolock locker(&fLock);
698 		packageName = package->Name();
699 	}
700 
701 	status_t status = fWebAppInterface.GetChangelog(packageName, info);
702 
703 	if (status == B_OK) {
704 		// Parse message
705 		BMessage result;
706 		BString content;
707 		if (info.FindMessage("result", &result) == B_OK) {
708 			if (result.FindString("content", &content) == B_OK
709 				&& 0 != content.Length()) {
710 				BAutolock locker(&fLock);
711 				package->SetChangelog(content);
712 				HDDEBUG("changelog populated for [%s]", packageName.String());
713 			} else
714 				HDDEBUG("no changelog present for [%s]", packageName.String());
715 		} else
716 			_MaybeLogJsonRpcError(info, "populate package changelog");
717 	} else {
718 		HDERROR("unable to obtain the changelog for the package [%s]",
719 			packageName.String());
720 	}
721 }
722 
723 
724 static void
725 model_remove_key_for_user(const BString& nickname)
726 {
727 	if (nickname.IsEmpty())
728 		return;
729 	BKeyStore keyStore;
730 	BPasswordKey key;
731 	BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
732 		<< nickname;
733 	status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
734 			passwordIdentifier, key);
735 
736 	switch (result) {
737 		case B_OK:
738 			result = keyStore.RemoveKey(kHaikuDepotKeyring, key);
739 			if (result != B_OK) {
740 				HDERROR("error occurred when removing password for nickname "
741 					"[%s] : %s", nickname.String(), strerror(result));
742 			}
743 			break;
744 		case B_ENTRY_NOT_FOUND:
745 			return;
746 		default:
747 			HDERROR("error occurred when finding password for nickname "
748 				"[%s] : %s", nickname.String(), strerror(result));
749 			break;
750 	}
751 }
752 
753 
754 void
755 Model::SetNickname(BString nickname)
756 {
757 	BString password;
758 	BString existingNickname = Nickname();
759 
760 	// this happens when the user is logging out.  Best to remove the password
761 	// stored for the existing user since it is no longer required.
762 
763 	if (!existingNickname.IsEmpty() && nickname.IsEmpty())
764 		model_remove_key_for_user(existingNickname);
765 
766 	if (nickname.Length() > 0) {
767 		BPasswordKey key;
768 		BKeyStore keyStore;
769 		BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
770 			<< nickname;
771 		if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
772 				passwordIdentifier, key) == B_OK) {
773 			password = key.Password();
774 		}
775 		if (password.IsEmpty())
776 			nickname = "";
777 	}
778 
779 	SetCredentials(nickname, password, false);
780 }
781 
782 
783 const BString&
784 Model::Nickname()
785 {
786 	return fWebAppInterface.Nickname();
787 }
788 
789 
790 void
791 Model::SetCredentials(const BString& nickname, const BString& passwordClear,
792 	bool storePassword)
793 {
794 	BString existingNickname = Nickname();
795 
796 	if (storePassword) {
797 		// no point continuing to store the password for the previous user.
798 
799 		if (!existingNickname.IsEmpty())
800 			model_remove_key_for_user(existingNickname);
801 
802 		// adding a key that is already there does not seem to override the
803 		// existing key so the old key needs to be removed first.
804 
805 		if (!nickname.IsEmpty())
806 			model_remove_key_for_user(nickname);
807 
808 		if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) {
809 			BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
810 				<< nickname;
811 			BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier);
812 			BKeyStore keyStore;
813 			keyStore.AddKeyring(kHaikuDepotKeyring);
814 			keyStore.AddKey(kHaikuDepotKeyring, key);
815 		}
816 	}
817 
818 	BAutolock locker(&fLock);
819 	fWebAppInterface.SetCredentials(UserCredentials(nickname, passwordClear));
820 
821 	if (nickname != existingNickname)
822 		_NotifyAuthorizationChanged();
823 }
824 
825 
826 /*! When bulk repository data comes down from the server, it will
827     arrive as a json.gz payload.  This is stored locally as a cache
828     and this method will provide the on-disk storage location for
829     this file.
830 */
831 
832 status_t
833 Model::DumpExportRepositoryDataPath(BPath& path)
834 {
835 	BString leaf;
836 	leaf.SetToFormat("repository-all_%s.json.gz",
837 		Language()->PreferredLanguage()->ID());
838 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
839 }
840 
841 
842 /*! When the system downloads reference data (eg; categories) from the server
843     then the downloaded data is stored and cached at the path defined by this
844     method.
845 */
846 
847 status_t
848 Model::DumpExportReferenceDataPath(BPath& path)
849 {
850 	BString leaf;
851 	leaf.SetToFormat("reference-all_%s.json.gz",
852 		Language()->PreferredLanguage()->ID());
853 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
854 }
855 
856 
857 status_t
858 Model::IconTarPath(BPath& path) const
859 {
860 	return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path);
861 }
862 
863 
864 status_t
865 Model::DumpExportPkgDataPath(BPath& path,
866 	const BString& repositorySourceCode)
867 {
868 	BString leaf;
869 	leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(),
870 		Language()->PreferredLanguage()->ID());
871 	return StorageUtils::LocalWorkingFilesPath(leaf, path);
872 }
873 
874 
875 // #pragma mark - listener notification methods
876 
877 
878 void
879 Model::_NotifyAuthorizationChanged()
880 {
881 	std::vector<ModelListenerRef>::const_iterator it;
882 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
883 		const ModelListenerRef& listener = *it;
884 		if (listener.IsSet())
885 			listener->AuthorizationChanged();
886 	}
887 }
888 
889 
890 void
891 Model::_NotifyCategoryListChanged()
892 {
893 	std::vector<ModelListenerRef>::const_iterator it;
894 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
895 		const ModelListenerRef& listener = *it;
896 		if (listener.IsSet())
897 			listener->CategoryListChanged();
898 	}
899 }
900 
901 
902 void
903 Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
904 	const char *sourceDescription) const
905 {
906 	BMessage error;
907 	BString errorMessage;
908 	double errorCode;
909 
910 	if (responsePayload.FindMessage("error", &error) == B_OK
911 		&& error.FindString("message", &errorMessage) == B_OK
912 		&& error.FindDouble("code", &errorCode) == B_OK) {
913 		HDERROR("[%s] --> error : [%s] (%f)", sourceDescription,
914 			errorMessage.String(), errorCode);
915 	} else
916 		HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
917 }
918 
919 
920 // #pragma mark - Rating Stabilities
921 
922 
923 int32
924 Model::CountRatingStabilities() const
925 {
926 	return fRatingStabilities.size();
927 }
928 
929 
930 RatingStabilityRef
931 Model::RatingStabilityByCode(BString& code) const
932 {
933 	std::vector<RatingStabilityRef>::const_iterator it;
934 	for (it = fRatingStabilities.begin(); it != fRatingStabilities.end();
935 			it++) {
936 		RatingStabilityRef aRatingStability = *it;
937 		if (aRatingStability->Code() == code)
938 			return aRatingStability;
939 	}
940 	return RatingStabilityRef();
941 }
942 
943 
944 RatingStabilityRef
945 Model::RatingStabilityAtIndex(int32 index) const
946 {
947 	return fRatingStabilities[index];
948 }
949 
950 
951 void
952 Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values)
953 {
954 	std::vector<RatingStabilityRef>::const_iterator it;
955 	for (it = values.begin(); it != values.end(); it++)
956 		_AddRatingStability(*it);
957 }
958 
959 
960 void
961 Model::_AddRatingStability(const RatingStabilityRef& value)
962 {
963 	std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst
964 		= std::lower_bound(
965 			fRatingStabilities.begin(),
966 			fRatingStabilities.end(),
967 			value,
968 			&IsRatingStabilityBefore);
969 	std::vector<RatingStabilityRef>::iterator itInsertionPt =
970 		fRatingStabilities.begin()
971 			+ (itInsertionPtConst - fRatingStabilities.begin());
972 
973 	if (itInsertionPt != fRatingStabilities.end()
974 		&& (*itInsertionPt)->Code() == value->Code()) {
975 		itInsertionPt = fRatingStabilities.erase(itInsertionPt);
976 			// replace the one with the same code.
977 	}
978 
979 	fRatingStabilities.insert(itInsertionPt, value);
980 }
981 
982 
983 // #pragma mark - Categories
984 
985 
986 int32
987 Model::CountCategories() const
988 {
989 	return fCategories.size();
990 }
991 
992 
993 CategoryRef
994 Model::CategoryByCode(BString& code) const
995 {
996 	std::vector<CategoryRef>::const_iterator it;
997 	for (it = fCategories.begin(); it != fCategories.end(); it++) {
998 		CategoryRef aCategory = *it;
999 		if (aCategory->Code() == code)
1000 			return aCategory;
1001 	}
1002 	return CategoryRef();
1003 }
1004 
1005 
1006 CategoryRef
1007 Model::CategoryAtIndex(int32 index) const
1008 {
1009 	return fCategories[index];
1010 }
1011 
1012 
1013 void
1014 Model::AddCategories(std::vector<CategoryRef>& values)
1015 {
1016 	std::vector<CategoryRef>::iterator it;
1017 	for (it = values.begin(); it != values.end(); it++)
1018 		_AddCategory(*it);
1019 	_NotifyCategoryListChanged();
1020 }
1021 
1022 /*! This will insert the category in order.
1023  */
1024 
1025 void
1026 Model::_AddCategory(const CategoryRef& category)
1027 {
1028 	std::vector<CategoryRef>::const_iterator itInsertionPtConst
1029 		= std::lower_bound(
1030 			fCategories.begin(),
1031 			fCategories.end(),
1032 			category,
1033 			&IsPackageCategoryBefore);
1034 	std::vector<CategoryRef>::iterator itInsertionPt =
1035 		fCategories.begin() + (itInsertionPtConst - fCategories.begin());
1036 
1037 	if (itInsertionPt != fCategories.end()
1038 		&& (*itInsertionPt)->Code() == category->Code()) {
1039 		itInsertionPt = fCategories.erase(itInsertionPt);
1040 			// replace the one with the same code.
1041 	}
1042 
1043 	fCategories.insert(itInsertionPt, category);
1044 }
1045 
1046 
1047 void
1048 Model::ScreenshotCached(const ScreenshotCoordinate& coord)
1049 {
1050 	std::vector<ModelListenerRef>::const_iterator it;
1051 	for (it = fListeners.begin(); it != fListeners.end(); it++) {
1052 		const ModelListenerRef& listener = *it;
1053 		if (listener.IsSet())
1054 			listener->ScreenshotCached(coord);
1055 	}
1056 }
1057