* Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
* Copyright 2014, Axel Dörfler <axeld@pinc-software.de>.
* Copyright 2016-2022, Andrew Lindesay <apl@lindesay.co.nz>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
#include "Model.h"
#include <algorithm>
#include <ctime>
#include <vector>
#include <stdarg.h>
#include <time.h>
#include <Autolock.h>
#include <Catalog.h>
#include <Directory.h>
#include <Entry.h>
#include <File.h>
#include <KeyStore.h>
#include <Locale.h>
#include <LocaleRoster.h>
#include <Message.h>
#include <Path.h>
#include "HaikuDepotConstants.h"
#include "Logger.h"
#include "LocaleUtils.h"
#include "StorageUtils.h"
#include "RepositoryUrlUtils.h"
#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Model"
#define KEY_STORE_IDENTIFIER_PREFIX "hds.password."
static const char* kHaikuDepotKeyring = "HaikuDepot";
PackageFilter::~PackageFilter()
{
}
ModelListener::~ModelListener()
{
}
class AnyFilter : public PackageFilter {
public:
virtual bool AcceptsPackage(const PackageInfoRef& package) const
{
return true;
}
};
class CategoryFilter : public PackageFilter {
public:
CategoryFilter(const BString& category)
:
fCategory(category)
{
}
virtual bool AcceptsPackage(const PackageInfoRef& package) const
{
if (!package.IsSet())
return false;
for (int i = package->CountCategories() - 1; i >= 0; i--) {
const CategoryRef& category = package->CategoryAtIndex(i);
if (!category.IsSet())
continue;
if (category->Code() == fCategory)
return true;
}
return false;
}
const BString& Category() const
{
return fCategory;
}
private:
BString fCategory;
};
class StateFilter : public PackageFilter {
public:
StateFilter(PackageState state)
:
fState(state)
{
}
virtual bool AcceptsPackage(const PackageInfoRef& package) const
{
return package->State() == NONE;
}
private:
PackageState fState;
};
class SearchTermsFilter : public PackageFilter {
public:
SearchTermsFilter(const BString& searchTerms)
{
int32 index = 0;
while (index < searchTerms.Length()) {
int32 nextSpace = searchTerms.FindFirst(" ", index);
if (nextSpace < 0)
nextSpace = searchTerms.Length();
if (nextSpace > index) {
BString term;
searchTerms.CopyInto(term, index, nextSpace - index);
term.ToLower();
fSearchTerms.Add(term);
}
index = nextSpace + 1;
}
}
virtual bool AcceptsPackage(const PackageInfoRef& package) const
{
if (!package.IsSet())
return false;
for (int32 i = fSearchTerms.CountStrings() - 1; i >= 0; i--) {
const BString& term = fSearchTerms.StringAt(i);
if (!_TextContains(package->Name(), term)
&& !_TextContains(package->Title(), term)
&& !_TextContains(package->Publisher().Name(), term)
&& !_TextContains(package->ShortDescription(), term)
&& !_TextContains(package->FullDescription(), term)) {
return false;
}
}
return true;
}
BString SearchTerms() const
{
BString searchTerms;
for (int32 i = 0; i < fSearchTerms.CountStrings(); i++) {
const BString& term = fSearchTerms.StringAt(i);
if (term.IsEmpty())
continue;
if (!searchTerms.IsEmpty())
searchTerms.Append(" ");
searchTerms.Append(term);
}
return searchTerms;
}
private:
bool _TextContains(BString text, const BString& string) const
{
text.ToLower();
int32 index = text.FindFirst(string);
return index >= 0;
}
private:
BStringList fSearchTerms;
};
static inline bool
is_source_package(const PackageInfoRef& package)
{
const BString& packageName = package->Name();
return packageName.EndsWith("_source");
}
static inline bool
is_develop_package(const PackageInfoRef& package)
{
const BString& packageName = package->Name();
return packageName.EndsWith("_devel")
|| packageName.EndsWith("_debuginfo");
}
Model::Model()
:
fDepots(),
fCategories(),
fCategoryFilter(PackageFilterRef(new AnyFilter(), true)),
fDepotFilter(""),
fSearchTermsFilter(PackageFilterRef(new AnyFilter(), true)),
fPackageListViewMode(PROMINENT),
fShowAvailablePackages(true),
fShowInstalledPackages(true),
fShowSourcePackages(false),
fShowDevelopPackages(false),
fCanShareAnonymousUsageData(false)
{
}
Model::~Model()
{
}
LanguageModel*
Model::Language()
{
return &fLanguageModel;
}
PackageIconRepository&
Model::GetPackageIconRepository()
{
return fPackageIconRepository;
}
status_t
Model::InitPackageIconRepository()
{
BPath tarPath;
status_t result = IconTarPath(tarPath);
if (result == B_OK)
result = fPackageIconRepository.Init(tarPath);
return result;
}
void
Model::AddListener(const ModelListenerRef& listener)
{
fListeners.push_back(listener);
}
PackageInfoRef
Model::PackageForName(const BString& name)
{
std::vector<DepotInfoRef>::iterator it;
for (it = fDepots.begin(); it != fDepots.end(); it++) {
DepotInfoRef depotInfoRef = *it;
PackageInfoRef packageInfoRef = depotInfoRef->PackageByName(name);
if (packageInfoRef.Get() != NULL)
return packageInfoRef;
}
return PackageInfoRef();
}
bool
Model::MatchesFilter(const PackageInfoRef& package) const
{
return fCategoryFilter->AcceptsPackage(package)
&& fSearchTermsFilter->AcceptsPackage(package)
&& (fDepotFilter.IsEmpty() || fDepotFilter == package->DepotName())
&& (fShowAvailablePackages || package->State() != NONE)
&& (fShowInstalledPackages || package->State() != ACTIVATED)
&& (fShowSourcePackages || !is_source_package(package))
&& (fShowDevelopPackages || !is_develop_package(package));
}
void
Model::MergeOrAddDepot(const DepotInfoRef& depot)
{
BString depotName = depot->Name();
for(uint32 i = 0; i < fDepots.size(); i++) {
if (fDepots[i]->Name() == depotName) {
DepotInfoRef ersatzDepot(new DepotInfo(*(fDepots[i].Get())), true);
ersatzDepot->SyncPackagesFromDepot(depot);
fDepots[i] = ersatzDepot;
return;
}
}
fDepots.push_back(depot);
}
bool
Model::HasDepot(const BString& name) const
{
return NULL != DepotForName(name).Get();
}
const DepotInfoRef
Model::DepotForName(const BString& name) const
{
std::vector<DepotInfoRef>::const_iterator it;
for (it = fDepots.begin(); it != fDepots.end(); it++) {
DepotInfoRef aDepot = *it;
if (aDepot->Name() == name)
return aDepot;
}
return DepotInfoRef();
}
int32
Model::CountDepots() const
{
return fDepots.size();
}
DepotInfoRef
Model::DepotAtIndex(int32 index) const
{
return fDepots[index];
}
bool
Model::HasAnyProminentPackages()
{
std::vector<DepotInfoRef>::iterator it;
for (it = fDepots.begin(); it != fDepots.end(); it++) {
DepotInfoRef aDepot = *it;
if (aDepot->HasAnyProminentPackages())
return true;
}
return false;
}
void
Model::Clear()
{
GetPackageIconRepository().Clear();
fDepots.clear();
fPopulatedPackageNames.MakeEmpty();
}
void
Model::SetStateForPackagesByName(BStringList& packageNames, PackageState state)
{
for (int32 i = 0; i < packageNames.CountStrings(); i++) {
BString packageName = packageNames.StringAt(i);
PackageInfoRef packageInfo = PackageForName(packageName);
if (packageInfo.IsSet()) {
packageInfo->SetState(state);
HDINFO("did update package [%s] with state [%s]",
packageName.String(), package_state_to_string(state));
}
else {
HDINFO("was unable to find package [%s] so was not possible to set"
" the state to [%s]", packageName.String(),
package_state_to_string(state));
}
}
}
void
Model::SetCategory(const BString& category)
{
PackageFilter* filter;
if (category.Length() == 0)
filter = new AnyFilter();
else
filter = new CategoryFilter(category);
fCategoryFilter.SetTo(filter, true);
}
BString
Model::Category() const
{
CategoryFilter* filter
= dynamic_cast<CategoryFilter*>(fCategoryFilter.Get());
if (filter == NULL)
return "";
return filter->Category();
}
void
Model::SetDepot(const BString& depot)
{
fDepotFilter = depot;
}
BString
Model::Depot() const
{
return fDepotFilter;
}
void
Model::SetSearchTerms(const BString& searchTerms)
{
PackageFilter* filter;
if (searchTerms.Length() == 0)
filter = new AnyFilter();
else
filter = new SearchTermsFilter(searchTerms);
fSearchTermsFilter.SetTo(filter, true);
}
BString
Model::SearchTerms() const
{
SearchTermsFilter* filter
= dynamic_cast<SearchTermsFilter*>(fSearchTermsFilter.Get());
if (filter == NULL)
return "";
return filter->SearchTerms();
}
void
Model::SetPackageListViewMode(package_list_view_mode mode)
{
fPackageListViewMode = mode;
}
void
Model::SetCanShareAnonymousUsageData(bool value)
{
fCanShareAnonymousUsageData = value;
}
void
Model::SetShowAvailablePackages(bool show)
{
fShowAvailablePackages = show;
}
void
Model::SetShowInstalledPackages(bool show)
{
fShowInstalledPackages = show;
}
void
Model::SetShowSourcePackages(bool show)
{
fShowSourcePackages = show;
}
void
Model::SetShowDevelopPackages(bool show)
{
fShowDevelopPackages = show;
}
server side because the repository is not represented in the server.
In such a case, there is little point in communicating with the server
only to hear back that the package does not exist.
*/
bool
Model::CanPopulatePackage(const PackageInfoRef& package)
{
const BString& depotName = package->DepotName();
if (depotName.IsEmpty())
return false;
const DepotInfoRef& depot = DepotForName(depotName);
if (depot.Get() == NULL)
return false;
return !depot->WebAppRepositoryCode().IsEmpty();
}
model of the packages. When the package is viewed, additional data needs
to be populated including ratings. This method takes care of that.
*/
void
Model::PopulatePackage(const PackageInfoRef& package, uint32 flags)
{
HDTRACE("will populate package for [%s]", package->Name().String());
if (!CanPopulatePackage(package)) {
HDINFO("unable to populate package [%s]", package->Name().String());
return;
}
{
BAutolock locker(&fLock);
bool alreadyPopulated = fPopulatedPackageNames.HasString(
package->Name());
if ((flags & POPULATE_FORCE) == 0 && alreadyPopulated)
return;
if (!alreadyPopulated)
fPopulatedPackageNames.Add(package->Name());
}
if ((flags & POPULATE_CHANGELOG) != 0 && package->HasChangelog()) {
_PopulatePackageChangelog(package);
}
if ((flags & POPULATE_USER_RATINGS) != 0) {
BMessage info;
BString packageName;
BString webAppRepositoryCode;
BString webAppRepositorySourceCode;
{
BAutolock locker(&fLock);
packageName = package->Name();
const DepotInfo* depot = DepotForName(package->DepotName());
if (depot != NULL) {
webAppRepositoryCode = depot->WebAppRepositoryCode();
webAppRepositorySourceCode
= depot->WebAppRepositorySourceCode();
}
}
status_t status = fWebAppInterface
.RetreiveUserRatingsForPackageForDisplay(packageName,
webAppRepositoryCode, webAppRepositorySourceCode, 0,
PACKAGE_INFO_MAX_USER_RATINGS, info);
if (status == B_OK) {
BMessage result;
BMessage items;
if (info.FindMessage("result", &result) == B_OK
&& result.FindMessage("items", &items) == B_OK) {
BAutolock locker(&fLock);
package->ClearUserRatings();
int32 index = 0;
while (true) {
BString name;
name << index++;
BMessage item;
if (items.FindMessage(name, &item) != B_OK)
break;
BString code;
if (item.FindString("code", &code) != B_OK) {
HDERROR("corrupt user rating at index %" B_PRIi32,
index);
continue;
}
BString user;
BMessage userInfo;
if (item.FindMessage("user", &userInfo) != B_OK
|| userInfo.FindString("nickname", &user) != B_OK) {
HDERROR("ignored user rating [%s] without a user "
"nickname", code.String());
continue;
}
BString languageCode;
BString comment;
double rating;
item.FindString("naturalLanguageCode", &languageCode);
item.FindString("comment", &comment);
if (item.FindDouble("rating", &rating) != B_OK)
rating = -1;
if (comment.Length() == 0 && rating == -1) {
HDERROR("rating [%s] has no comment or rating so will"
" be ignored", code.String());
continue;
}
BString major = "?";
BString minor = "?";
BString micro = "";
double revision = -1;
BString architectureCode = "";
BMessage version;
if (item.FindMessage("pkgVersion", &version) == B_OK) {
version.FindString("major", &major);
version.FindString("minor", &minor);
version.FindString("micro", µ);
version.FindDouble("revision", &revision);
version.FindString("architectureCode",
&architectureCode);
}
BString versionString = major;
versionString << ".";
versionString << minor;
if (!micro.IsEmpty()) {
versionString << ".";
versionString << micro;
}
if (revision > 0) {
versionString << "-";
versionString << (int) revision;
}
if (!architectureCode.IsEmpty()) {
versionString << " " << STR_MDASH << " ";
versionString << architectureCode;
}
double createTimestamp;
item.FindDouble("createTimestamp", &createTimestamp);
UserRatingRef userRating(new UserRating(
UserInfo(user), rating,
comment, languageCode, versionString,
(uint64) createTimestamp), true);
package->AddUserRating(userRating);
HDDEBUG("rating [%s] retrieved from server", code.String());
}
HDDEBUG("did retrieve %" B_PRIi32 " user ratings for [%s]",
index - 1, packageName.String());
} else {
BString message;
message.SetToFormat("failure to retrieve user ratings for [%s]",
packageName.String());
_MaybeLogJsonRpcError(info, message.String());
}
} else
HDERROR("unable to retrieve user ratings");
}
if ((flags & POPULATE_SCREEN_SHOTS) != 0) {
std::vector<ScreenshotInfoRef> screenshotInfos;
{
BAutolock locker(&fLock);
for (int32 i = 0; i < package->CountScreenshotInfos(); i++)
screenshotInfos.push_back(package->ScreenshotInfoAtIndex(i));
package->ClearScreenshots();
}
std::vector<ScreenshotInfoRef>::iterator it;
for (it = screenshotInfos.begin(); it != screenshotInfos.end(); it++) {
const ScreenshotInfoRef& info = *it;
_PopulatePackageScreenshot(package, info, 320, false);
}
}
}
void
Model::_PopulatePackageChangelog(const PackageInfoRef& package)
{
BMessage info;
BString packageName;
{
BAutolock locker(&fLock);
packageName = package->Name();
}
status_t status = fWebAppInterface.GetChangelog(packageName, info);
if (status == B_OK) {
BMessage result;
BString content;
if (info.FindMessage("result", &result) == B_OK) {
if (result.FindString("content", &content) == B_OK
&& 0 != content.Length()) {
BAutolock locker(&fLock);
package->SetChangelog(content);
HDDEBUG("changelog populated for [%s]", packageName.String());
} else
HDDEBUG("no changelog present for [%s]", packageName.String());
} else
_MaybeLogJsonRpcError(info, "populate package changelog");
} else {
HDERROR("unable to obtain the changelog for the package [%s]",
packageName.String());
}
}
static void
model_remove_key_for_user(const BString& nickname)
{
if (nickname.IsEmpty())
return;
BKeyStore keyStore;
BPasswordKey key;
BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
<< nickname;
status_t result = keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
passwordIdentifier, key);
switch (result) {
case B_OK:
result = keyStore.RemoveKey(kHaikuDepotKeyring, key);
if (result != B_OK) {
HDERROR("error occurred when removing password for nickname "
"[%s] : %s", nickname.String(), strerror(result));
}
break;
case B_ENTRY_NOT_FOUND:
return;
default:
HDERROR("error occurred when finding password for nickname "
"[%s] : %s", nickname.String(), strerror(result));
break;
}
}
void
Model::SetNickname(BString nickname)
{
BString password;
BString existingNickname = Nickname();
if (!existingNickname.IsEmpty() && nickname.IsEmpty())
model_remove_key_for_user(existingNickname);
if (nickname.Length() > 0) {
BPasswordKey key;
BKeyStore keyStore;
BString passwordIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
<< nickname;
if (keyStore.GetKey(kHaikuDepotKeyring, B_KEY_TYPE_PASSWORD,
passwordIdentifier, key) == B_OK) {
password = key.Password();
}
if (password.IsEmpty())
nickname = "";
}
SetAuthorization(nickname, password, false);
}
const BString&
Model::Nickname() const
{
return fWebAppInterface.Nickname();
}
void
Model::SetAuthorization(const BString& nickname, const BString& passwordClear,
bool storePassword)
{
BString existingNickname = Nickname();
if (storePassword) {
if (!existingNickname.IsEmpty())
model_remove_key_for_user(existingNickname);
if (!nickname.IsEmpty())
model_remove_key_for_user(nickname);
if (!nickname.IsEmpty() && !passwordClear.IsEmpty()) {
BString keyIdentifier = BString(KEY_STORE_IDENTIFIER_PREFIX)
<< nickname;
BPasswordKey key(passwordClear, B_KEY_PURPOSE_WEB, keyIdentifier);
BKeyStore keyStore;
keyStore.AddKeyring(kHaikuDepotKeyring);
keyStore.AddKey(kHaikuDepotKeyring, key);
}
}
BAutolock locker(&fLock);
fWebAppInterface.SetAuthorization(UserCredentials(nickname, passwordClear));
if (nickname != existingNickname)
_NotifyAuthorizationChanged();
}
arrive as a json.gz payload. This is stored locally as a cache
and this method will provide the on-disk storage location for
this file.
*/
status_t
Model::DumpExportRepositoryDataPath(BPath& path)
{
BString leaf;
leaf.SetToFormat("repository-all_%s.json.gz",
Language()->PreferredLanguage()->Code());
return StorageUtils::LocalWorkingFilesPath(leaf, path);
}
then the downloaded data is stored and cached at the path defined by this
method.
*/
status_t
Model::DumpExportReferenceDataPath(BPath& path)
{
BString leaf;
leaf.SetToFormat("reference-all_%s.json.gz",
Language()->PreferredLanguage()->Code());
return StorageUtils::LocalWorkingFilesPath(leaf, path);
}
status_t
Model::IconTarPath(BPath& path) const
{
return StorageUtils::LocalWorkingFilesPath("pkgicon-all.tar", path);
}
status_t
Model::DumpExportPkgDataPath(BPath& path,
const BString& repositorySourceCode)
{
BString leaf;
leaf.SetToFormat("pkg-all-%s-%s.json.gz", repositorySourceCode.String(),
Language()->PreferredLanguage()->Code());
return StorageUtils::LocalWorkingFilesPath(leaf, path);
}
void
Model::_PopulatePackageScreenshot(const PackageInfoRef& package,
const ScreenshotInfoRef& info, int32 scaledWidth, bool fromCacheOnly)
{
BFile screenshotFile;
BPath screenshotCachePath;
status_t result = StorageUtils::LocalWorkingDirectoryPath(
"Screenshots", screenshotCachePath);
if (result != B_OK) {
HDERROR("unable to get the screenshot dir - unable to proceed");
return;
}
bool fileExists = false;
BString screenshotName(info->Code());
screenshotName << "@" << scaledWidth;
screenshotName << ".png";
time_t modifiedTime;
if (screenshotCachePath.Append(screenshotName) == B_OK) {
fileExists = screenshotFile.SetTo(screenshotCachePath.Path(),
B_READ_ONLY) == B_OK;
if (fileExists)
screenshotFile.GetModificationTime(&modifiedTime);
}
if (fileExists) {
time_t now;
time(&now);
if (fromCacheOnly || now - modifiedTime < 60 * 60) {
BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(screenshotFile),
true);
BAutolock locker(&fLock);
package->AddScreenshot(bitmapRef);
return;
}
}
if (fromCacheOnly)
return;
BMallocIO buffer;
int32 scaledHeight = scaledWidth * info->Height() / info->Width();
status_t status = fWebAppInterface.RetrieveScreenshot(info->Code(),
scaledWidth, scaledHeight, &buffer);
if (status == B_OK) {
BitmapRef bitmapRef(new(std::nothrow)SharedBitmap(buffer), true);
BAutolock locker(&fLock);
package->AddScreenshot(bitmapRef);
locker.Unlock();
if (screenshotFile.SetTo(screenshotCachePath.Path(),
B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE) == B_OK) {
screenshotFile.Write(buffer.Buffer(), buffer.BufferLength());
}
} else {
HDERROR("Failed to retrieve screenshot for code '%s' "
"at %" B_PRIi32 "x%" B_PRIi32 ".", info->Code().String(),
scaledWidth, scaledHeight);
}
}
void
Model::_NotifyAuthorizationChanged()
{
std::vector<ModelListenerRef>::const_iterator it;
for (it = fListeners.begin(); it != fListeners.end(); it++) {
const ModelListenerRef& listener = *it;
if (listener.IsSet())
listener->AuthorizationChanged();
}
}
void
Model::_NotifyCategoryListChanged()
{
std::vector<ModelListenerRef>::const_iterator it;
for (it = fListeners.begin(); it != fListeners.end(); it++) {
const ModelListenerRef& listener = *it;
if (listener.IsSet())
listener->CategoryListChanged();
}
}
void
Model::_MaybeLogJsonRpcError(const BMessage &responsePayload,
const char *sourceDescription) const
{
BMessage error;
BString errorMessage;
double errorCode;
if (responsePayload.FindMessage("error", &error) == B_OK
&& error.FindString("message", &errorMessage) == B_OK
&& error.FindDouble("code", &errorCode) == B_OK) {
HDERROR("[%s] --> error : [%s] (%f)", sourceDescription,
errorMessage.String(), errorCode);
} else
HDERROR("[%s] --> an undefined error has occurred", sourceDescription);
}
int32
Model::CountRatingStabilities() const
{
return fRatingStabilities.size();
}
RatingStabilityRef
Model::RatingStabilityByCode(BString& code) const
{
std::vector<RatingStabilityRef>::const_iterator it;
for (it = fRatingStabilities.begin(); it != fRatingStabilities.end();
it++) {
RatingStabilityRef aRatingStability = *it;
if (aRatingStability->Code() == code)
return aRatingStability;
}
return RatingStabilityRef();
}
RatingStabilityRef
Model::RatingStabilityAtIndex(int32 index) const
{
return fRatingStabilities[index];
}
void
Model::AddRatingStabilities(std::vector<RatingStabilityRef>& values)
{
std::vector<RatingStabilityRef>::const_iterator it;
for (it = values.begin(); it != values.end(); it++)
_AddRatingStability(*it);
}
void
Model::_AddRatingStability(const RatingStabilityRef& value)
{
std::vector<RatingStabilityRef>::const_iterator itInsertionPtConst
= std::lower_bound(
fRatingStabilities.begin(),
fRatingStabilities.end(),
value,
&IsRatingStabilityBefore);
std::vector<RatingStabilityRef>::iterator itInsertionPt =
fRatingStabilities.begin()
+ (itInsertionPtConst - fRatingStabilities.begin());
if (itInsertionPt != fRatingStabilities.end()
&& (*itInsertionPt)->Code() == value->Code()) {
itInsertionPt = fRatingStabilities.erase(itInsertionPt);
}
fRatingStabilities.insert(itInsertionPt, value);
}
int32
Model::CountCategories() const
{
return fCategories.size();
}
CategoryRef
Model::CategoryByCode(BString& code) const
{
std::vector<CategoryRef>::const_iterator it;
for (it = fCategories.begin(); it != fCategories.end(); it++) {
CategoryRef aCategory = *it;
if (aCategory->Code() == code)
return aCategory;
}
return CategoryRef();
}
CategoryRef
Model::CategoryAtIndex(int32 index) const
{
return fCategories[index];
}
void
Model::AddCategories(std::vector<CategoryRef>& values)
{
std::vector<CategoryRef>::iterator it;
for (it = values.begin(); it != values.end(); it++)
_AddCategory(*it);
_NotifyCategoryListChanged();
}
*/
void
Model::_AddCategory(const CategoryRef& category)
{
std::vector<CategoryRef>::const_iterator itInsertionPtConst
= std::lower_bound(
fCategories.begin(),
fCategories.end(),
category,
&IsPackageCategoryBefore);
std::vector<CategoryRef>::iterator itInsertionPt =
fCategories.begin() + (itInsertionPtConst - fCategories.begin());
if (itInsertionPt != fCategories.end()
&& (*itInsertionPt)->Code() == category->Code()) {
itInsertionPt = fCategories.erase(itInsertionPt);
}
fCategories.insert(itInsertionPt, category);
}