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
41static const char* kHaikuDepotKeyring = "HaikuDepot";
42
43
44PackageFilter::~PackageFilter()
45{
46}
47
48
49ModelListener::~ModelListener()
50{
51}
52
53
54// #pragma mark - PackageFilters
55
56
57class AnyFilter : public PackageFilter {
58public:
59	virtual bool AcceptsPackage(const PackageInfoRef& package) const
60	{
61		return true;
62	}
63};
64
65
66class CategoryFilter : public PackageFilter {
67public:
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
94private:
95	BString		fCategory;
96};
97
98
99class StateFilter : public PackageFilter {
100public:
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
112private:
113	PackageState	fState;
114};
115
116
117class SearchTermsFilter : public PackageFilter {
118public:
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
169private:
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
177private:
178	BStringList fSearchTerms;
179};
180
181
182static inline bool
183is_source_package(const PackageInfoRef& package)
184{
185	const BString& packageName = package->Name();
186	return packageName.EndsWith("_source");
187}
188
189
190static inline bool
191is_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
202Model::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
222Model::~Model()
223{
224	delete fPackageScreenshotRepository;
225}
226
227
228LanguageModel*
229Model::Language()
230{
231	return &fLanguageModel;
232}
233
234
235PackageIconRepository&
236Model::GetPackageIconRepository()
237{
238	return fPackageIconRepository;
239}
240
241
242status_t
243Model::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
253PackageScreenshotRepository*
254Model::GetPackageScreenshotRepository()
255{
256	return fPackageScreenshotRepository;
257}
258
259
260void
261Model::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
269PackageInfoRef
270Model::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
283bool
284Model::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
296void
297Model::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
312bool
313Model::HasDepot(const BString& name) const
314{
315	return NULL != DepotForName(name).Get();
316}
317
318
319const DepotInfoRef
320Model::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
332int32
333Model::CountDepots() const
334{
335	return fDepots.size();
336}
337
338
339DepotInfoRef
340Model::DepotAtIndex(int32 index) const
341{
342	return fDepots[index];
343}
344
345
346bool
347Model::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
359void
360Model::Clear()
361{
362	GetPackageIconRepository().Clear();
363	fDepots.clear();
364	fPopulatedPackageNames.MakeEmpty();
365}
366
367
368void
369Model::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
392void
393Model::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
406BString
407Model::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
417void
418Model::SetDepot(const BString& depot)
419{
420	fDepotFilter = depot;
421}
422
423
424BString
425Model::Depot() const
426{
427	return fDepotFilter;
428}
429
430
431void
432Model::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
445BString
446Model::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
456void
457Model::SetPackageListViewMode(package_list_view_mode mode)
458{
459	fPackageListViewMode = mode;
460}
461
462
463void
464Model::SetCanShareAnonymousUsageData(bool value)
465{
466	fCanShareAnonymousUsageData = value;
467}
468
469
470void
471Model::SetShowAvailablePackages(bool show)
472{
473	fShowAvailablePackages = show;
474}
475
476
477void
478Model::SetShowInstalledPackages(bool show)
479{
480	fShowInstalledPackages = show;
481}
482
483
484void
485Model::SetShowSourcePackages(bool show)
486{
487	fShowSourcePackages = show;
488}
489
490
491void
492Model::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
506bool
507Model::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
528void
529Model::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
690void
691Model::_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
724static void
725model_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
754void
755Model::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
783const BString&
784Model::Nickname()
785{
786	return fWebAppInterface.Nickname();
787}
788
789
790void
791Model::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
832status_t
833Model::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
847status_t
848Model::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
857status_t
858Model::IconTarPath(BPath& path) const
859{
860	return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path);
861}
862
863
864status_t
865Model::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
878void
879Model::_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
890void
891Model::_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
902void
903Model::_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
923int32
924Model::CountRatingStabilities() const
925{
926	return fRatingStabilities.size();
927}
928
929
930RatingStabilityRef
931Model::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
944RatingStabilityRef
945Model::RatingStabilityAtIndex(int32 index) const
946{
947	return fRatingStabilities[index];
948}
949
950
951void
952Model::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
960void
961Model::_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
986int32
987Model::CountCategories() const
988{
989	return fCategories.size();
990}
991
992
993CategoryRef
994Model::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
1006CategoryRef
1007Model::CategoryAtIndex(int32 index) const
1008{
1009	return fCategories[index];
1010}
1011
1012
1013void
1014Model::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
1025void
1026Model::_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
1047void
1048Model::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