syncevolution/src/dbus/server/pim/locale-factory-boost.cpp

1277 lines
49 KiB
C++

/*
* Copyright (C) 2012 Intel Corporation
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) version 3.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
* 02110-1301 USA
*/
/**
* The boost::locale based implementation of locale-factory.h.
*/
#include "locale-factory.h"
#include "folks.h"
#include <libebook/libebook.h>
#include <phonenumbers/phonenumberutil.h>
#include <phonenumbers/logger.h>
#include <boost/locale.hpp>
#include <boost/lexical_cast.hpp>
#include <unicode/unistr.h>
#include <unicode/translit.h>
#include <unicode/bytestream.h>
#include <unicode/locid.h>
SE_GLIB_TYPE(EBookQuery, e_book_query)
SE_BEGIN_CXX
typedef boost::shared_ptr<EPhoneNumber> EPhoneNumberCXX;
EPhoneNumberCXX EPhoneNumberCXXNew(EPhoneNumber *number) { return EPhoneNumberCXX(number, e_phone_number_free); }
/**
* Use higher levels to break ties between strings which are
* considered equal at the lower levels. For example, "Façade" and
* "facade" would be compared as equal when using only base
* characters, but compare differently when also considering a higher
* level which includes accents.
*
* The drawback of higher levels is that they are computationally more
* expensive (transformation is slower and leads to longer transformed
* strings, thus a longer string comparisons during compare).
*
* The default here pays attention to accents, case, and
* punctuation. According to
* http://userguide.icu-project.org/collation/concepts, it is required
* for Japanese.
*/
static const boost::locale::collator_base::level_type DEFAULT_COLLATION_LEVEL =
boost::locale::collator_base::quaternary;
class CompareBoost : public IndividualCompare, private boost::noncopyable {
std::locale m_locale;
const boost::locale::collator<char> &m_collator;
std::unique_ptr<icu::Transliterator> m_trans;
public:
CompareBoost(const std::locale &locale);
std::string transform(const char *string) const;
std::string transform(const std::string &string) const;
};
CompareBoost::CompareBoost(const std::locale &locale) :
m_locale(locale),
m_collator(std::use_facet< boost::locale::collator<char> >(m_locale))
{
std::string language = std::use_facet<boost::locale::info>(m_locale).language();
if (language == "zh") {
// Hard-code Pinyin sorting for all Chinese countries.
//
// There are three different ways of sorting Chinese and Western names:
// 1. Sort Chinese characters in pinyin order, but separate from Latin
// 2. Sort them interleaved with Latin, by the first character.
// 3. Sort them fully interleaved with Latin.
// Source: Mark Davis, ICU, http://sourceforge.net/mailarchive/forum.php?thread_name=CAJ2xs_GEnN-u3%3D%2B7P5puaF1%2BU__fX-4tuA-kEybThN9xsw577Q%40mail.gmail.com&forum_name=icu-support
//
// Either 2 or 3 is what apparently more people expect. Implementing 2 is
// harder, whereas 3 fits into the "generate keys, compare keys" concept
// of IndividualCompare, so we kind of arbitrarily implement that.
SE_LOG_DEBUG(NULL, "enabling Pinyin");
UErrorCode status = U_ZERO_ERROR;
icu::Transliterator *trans = icu::Transliterator::createInstance("Han-Latin", UTRANS_FORWARD, status);
m_trans.reset(trans);
if (U_FAILURE(status)) {
SE_LOG_WARNING(NULL, "creating ICU Han-Latin Transliterator for Pinyin failed, error code %s; falling back to normal collation", u_errorName(status));
m_trans.reset();
} else if (!trans) {
SE_LOG_WARNING(NULL, "creating ICU Han-Latin Transliterator for Pinyin failed, no error code; falling back to normal collation");
}
}
}
std::string CompareBoost::transform(const char *string) const
{
if (!string) {
return "";
}
return transform(std::string(string));
}
std::string CompareBoost::transform(const std::string &string) const
{
// TODO: use e_collator_generate_key
if (m_trans.get()) {
// std::string result;
// m_trans->transliterate(icu::StringPiece(string), icu::StringByteSink<std::string>(&result));
icu::UnicodeString buffer(string.c_str());
m_trans->transliterate(buffer);
std::string result;
buffer.toUTF8String(result);
result = m_collator.transform(DEFAULT_COLLATION_LEVEL, result);
return result;
} else {
return m_collator.transform(DEFAULT_COLLATION_LEVEL, string);
}
}
class CompareFirstLastBoost : public CompareBoost {
public:
CompareFirstLastBoost(const std::locale &locale) :
CompareBoost(locale)
{}
virtual void createCriteria(FolksIndividual *individual, Criteria_t &criteria) const {
FolksStructuredName *fn =
folks_name_details_get_structured_name(FOLKS_NAME_DETAILS(individual));
if (fn) {
const char *family = folks_structured_name_get_family_name(fn);
const char *given = folks_structured_name_get_given_name(fn);
criteria.push_back(transform(given));
criteria.push_back(transform(family));
}
}
};
class CompareLastFirstBoost : public CompareBoost {
public:
CompareLastFirstBoost(const std::locale &locale) :
CompareBoost(locale)
{}
virtual void createCriteria(FolksIndividual *individual, Criteria_t &criteria) const {
FolksStructuredName *fn =
folks_name_details_get_structured_name(FOLKS_NAME_DETAILS(individual));
if (fn) {
const char *family = folks_structured_name_get_family_name(fn);
const char *given = folks_structured_name_get_given_name(fn);
criteria.push_back(transform(family));
criteria.push_back(transform(given));
}
}
};
class CompareFullnameBoost : public CompareBoost {
public:
CompareFullnameBoost(const std::locale &locale) :
CompareBoost(locale)
{}
virtual void createCriteria(FolksIndividual *individual, Criteria_t &criteria) const {
const char *fullname = folks_name_details_get_full_name(FOLKS_NAME_DETAILS(individual));
if (fullname) {
criteria.push_back(transform(fullname));
} else {
FolksStructuredName *fn =
folks_name_details_get_structured_name(FOLKS_NAME_DETAILS(individual));
if (fn) {
const char *given = folks_structured_name_get_given_name(fn);
const char *middle = folks_structured_name_get_additional_names(fn);
const char *family = folks_structured_name_get_family_name(fn);
const char *suffix = folks_structured_name_get_suffixes(fn);
std::string buffer;
buffer.reserve(256);
#define APPEND(_str) \
if (_str && *_str) { \
if (!buffer.empty()) { \
buffer += _str; \
} \
}
APPEND(given);
APPEND(middle);
APPEND(family);
APPEND(suffix);
#undef APPEND
criteria.push_back(transform(buffer));
}
}
}
};
/**
* Implements 'any-contains' and acts as utility base class
* for the other text comparison operators.
*/
class AnyContainsBoost : public IndividualFilter
{
public:
enum Mode {
EXACT = 0,
CASE_INSENSITIVE = 1<<0,
ACCENT_INSENSITIVE = 1<<1,
TRANSLITERATE = 1<<2,
ALL =
CASE_INSENSITIVE|
ACCENT_INSENSITIVE|
TRANSLITERATE|
0
};
AnyContainsBoost(const std::locale &locale,
const std::string &searchValue,
int mode) :
m_locale(locale),
// For performance reasons we use ICU directly and thus need
// an ICU::Locale.
// m_ICULocale(std::use_facet<boost::locale::info>(m_locale).language().c_str(),
// std::use_facet<boost::locale::info>(m_locale).country().c_str(),
// std::use_facet<boost::locale::info>(m_locale).variant().c_str()),
// m_collator(std::use_facet<boost::locale::collator>(locale)),
m_searchValue(searchValue),
m_mode(mode)
{
if (mode & TRANSLITERATE) {
UErrorCode status = U_ZERO_ERROR;
m_transliterator.reset(Transliterator::createInstance ("Any-Latin", UTRANS_FORWARD, status));
if (!m_transliterator ||
U_FAILURE(status)) {
SE_LOG_WARNING(NULL, "creating ICU Any-Latin Transliterator failed, error code %s; falling back to not transliterating", u_errorName(status));
m_transliterator.reset();
mode ^= TRANSLITERATE;
m_mode = mode;
}
}
switch (mode) {
case EXACT:
break;
default:
m_searchValueTransformed = transform(m_searchValue);
break;
}
m_searchValueTel = normalizePhoneText(m_searchValue.c_str());
}
/**
* Turn filter arguments into bit field.
*/
static int getFilterMode(const std::vector<LocaleFactory::Filter_t> &terms,
size_t start);
/** simplify according to mode */
std::string transform(const char *in) const;
std::string transform(const std::string &in) const { return transform(in.c_str()); }
/**
* The search text is not necessarily a full phone number,
* therefore we cannot parse it with libphonenumber. Instead
* do a sub-string search after telephone specific normalization,
* to let the search ignore irrelevant formatting aspects:
*
* - Map ASCII characters to the corresponding digit.
* - Reduce to just the digits before comparison (no spaces, no
* punctuation).
*
* Example: +1-800-FOOBAR -> 1800366227
*/
static std::string normalizePhoneText(const char *tel)
{
static const i18n::phonenumbers::PhoneNumberUtil &util(*i18n::phonenumbers::PhoneNumberUtil::GetInstance());
std::string res;
char c;
bool haveAlpha = false;
while ((c = *tel) != '\0') {
if (isdigit(c)) {
res += c;
} else if (isalpha(c)) {
haveAlpha = true;
res += c;
}
++tel;
}
// Only scan through the string again if we really have to.
if (haveAlpha) {
util.ConvertAlphaCharactersInNumber(&res);
}
return res;
}
bool containsSearchText(const char *text) const
{
if (!text) {
return false;
}
switch (m_mode) {
case EXACT:
return boost::contains(text, m_searchValue);
break;
default: {
std::string transformed = transform(text);
return boost::contains(transformed, m_searchValueTransformed);
break;
}
}
// not reached
return false;
}
bool containsSearchTel(const char *text) const
{
std::string tel = normalizePhoneText(text);
return boost::contains(tel, m_searchValueTel);
}
bool isSearchText(const char *text) const
{
if (!text) {
return false;
}
switch (m_mode) {
case EXACT:
return boost::equals(text, m_searchValue);
break;
default: {
std::string transformed = transform(text);
return boost::equals(transformed, m_searchValueTransformed);
break;
}
}
// not reached
return false;
}
bool isSearchTel(const char *text) const
{
std::string tel = normalizePhoneText(text);
return boost::equals(tel, m_searchValueTel);
}
bool beginsWithSearchText(const char *text) const
{
if (!text) {
return false;
}
switch (m_mode) {
case EXACT:
return boost::starts_with(text, m_searchValue);
break;
default: {
std::string transformed = transform(text);
return boost::starts_with(transformed, m_searchValueTransformed);
break;
}
}
// not reached
return false;
}
bool beginsWithSearchTel(const char *text) const
{
std::string tel = normalizePhoneText(text);
return boost::starts_with(tel, m_searchValueTel);
}
bool endsWithSearchText(const char *text) const
{
if (!text) {
return false;
}
switch (m_mode) {
case EXACT:
return boost::ends_with(text, m_searchValue);
break;
default: {
std::string transformed = transform(text);
return boost::ends_with(transformed, m_searchValueTransformed);
break;
}
}
// not reached
return false;
}
bool endsWithSearchTel(const char *text) const
{
std::string tel = normalizePhoneText(text);
return boost::ends_with(tel, m_searchValueTel);
}
virtual bool matches(const IndividualData &data) const
{
FolksIndividual *individual = data.m_individual.get();
FolksNameDetails *name = FOLKS_NAME_DETAILS(individual);
const char *fullname = folks_name_details_get_full_name(name);
if (containsSearchText(fullname)) {
return true;
}
const char *nickname = folks_name_details_get_nickname(name);
if (containsSearchText(nickname)) {
return true;
}
FolksStructuredName *fn =
folks_name_details_get_structured_name(FOLKS_NAME_DETAILS(individual));
if (fn) {
const char *given = folks_structured_name_get_given_name(fn);
if (containsSearchText(given)) {
return true;
}
const char *middle = folks_structured_name_get_additional_names(fn);
if (containsSearchText(middle)) {
return true;
}
const char *family = folks_structured_name_get_family_name(fn);
if (containsSearchText(family)) {
return true;
}
}
FolksEmailDetails *emailDetails = FOLKS_EMAIL_DETAILS(individual);
GeeSet *emails = folks_email_details_get_email_addresses(emailDetails);
BOOST_FOREACH (FolksAbstractFieldDetails *email, GeeCollCXX<FolksAbstractFieldDetails *>(emails, ADD_REF)) {
const gchar *value =
reinterpret_cast<const gchar *>(folks_abstract_field_details_get_value(email));
if (containsSearchText(value)) {
return true;
}
}
FolksPhoneDetails *phoneDetails = FOLKS_PHONE_DETAILS(individual);
GeeSet *phones = folks_phone_details_get_phone_numbers(phoneDetails);
BOOST_FOREACH (FolksAbstractFieldDetails *phone, GeeCollCXX<FolksAbstractFieldDetails *>(phones, ADD_REF)) {
const gchar *value =
reinterpret_cast<const gchar *>(folks_abstract_field_details_get_value(phone));
if (containsSearchTel(value)) {
return true;
}
}
return false;
}
private:
std::locale m_locale;
// icu::Locale m_ICULocale;
boost::shared_ptr<icu::Transliterator> m_transliterator;
std::string m_searchValue;
std::string m_searchValueTransformed;
std::string m_searchValueTel;
int m_mode;
// const bool (*m_contains)(const std::string &, const std::string &, const std::locale &);
};
std::string AnyContainsBoost::transform(const char *in) const
{
icu::UnicodeString unicode = icu::UnicodeString::fromUTF8(in);
if (m_mode & TRANSLITERATE) {
m_transliterator->transliterate(unicode);
}
if (m_mode & CASE_INSENSITIVE) {
unicode.foldCase();
}
std::string utf8;
unicode.toUTF8String(utf8);
if (m_mode & ACCENT_INSENSITIVE) {
// Haven't found an easy way to do this with ICU.
// Use e_util_utf8_remove_accents(), which also ensures
// consistency with EDS.
PlainGStr res = e_util_utf8_remove_accents(utf8.c_str());
return std::string(res);
} else {
return utf8;
}
}
int AnyContainsBoost::getFilterMode(const std::vector<LocaleFactory::Filter_t> &terms,
size_t start)
{
int mode = ALL;
for (size_t i = start; i < terms.size(); i++) {
const std::string &flag = LocaleFactory::getFilterString(terms[i], "any-contains flag");
if (flag == "case-sensitive") {
mode &= ~CASE_INSENSITIVE;
} else if (flag == "case-insensitive") {
mode |= CASE_INSENSITIVE;
} else if (flag == "accent-sensitive") {
mode &= ~ACCENT_INSENSITIVE;
} else if (flag == "accent-insensitive") {
mode |= ACCENT_INSENSITIVE;
} else if (flag == "no-transliteration") {
mode &= ~TRANSLITERATE;
} else if (flag == "transliteration") {
mode |= TRANSLITERATE;
} else {
SE_THROW("unsupported filter flag: " + flag);
}
}
return mode;
}
class FilterFullName : public AnyContainsBoost
{
bool (AnyContainsBoost::*m_operation)(const char *text) const;
public:
FilterFullName(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
AnyContainsBoost(locale, searchValue, mode),
m_operation(operation)
{
}
virtual bool matches(const IndividualData &data) const
{
FolksIndividual *individual = data.m_individual.get();
FolksNameDetails *name = FOLKS_NAME_DETAILS(individual);
const char *fullname = folks_name_details_get_full_name(name);
return (this->*m_operation)(fullname);
}
};
class FilterNickname : public AnyContainsBoost
{
bool (AnyContainsBoost::*m_operation)(const char *text) const;
public:
FilterNickname(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
AnyContainsBoost(locale, searchValue, mode),
m_operation(operation)
{
}
virtual bool matches(const IndividualData &data) const
{
FolksIndividual *individual = data.m_individual.get();
FolksNameDetails *name = FOLKS_NAME_DETAILS(individual);
const char *fullname = folks_name_details_get_nickname(name);
return (this->*m_operation)(fullname);
}
};
class FilterFamilyName : public AnyContainsBoost
{
bool (AnyContainsBoost::*m_operation)(const char *text) const;
public:
FilterFamilyName(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
AnyContainsBoost(locale, searchValue, mode),
m_operation(operation)
{
}
virtual bool matches(const IndividualData &data) const
{
FolksIndividual *individual = data.m_individual.get();
FolksStructuredName *fn =
folks_name_details_get_structured_name(FOLKS_NAME_DETAILS(individual));
if (fn) {
const char *name = folks_structured_name_get_family_name(fn);
return (this->*m_operation)(name);
} else {
return false;
}
}
};
class FilterGivenName : public AnyContainsBoost
{
bool (AnyContainsBoost::*m_operation)(const char *text) const;
public:
FilterGivenName(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
AnyContainsBoost(locale, searchValue, mode),
m_operation(operation)
{
}
virtual bool matches(const IndividualData &data) const
{
FolksIndividual *individual = data.m_individual.get();
FolksStructuredName *fn =
folks_name_details_get_structured_name(FOLKS_NAME_DETAILS(individual));
if (fn) {
const char *name = folks_structured_name_get_given_name(fn);
return (this->*m_operation)(name);
} else {
return false;
}
}
};
class FilterAdditionalName : public AnyContainsBoost
{
bool (AnyContainsBoost::*m_operation)(const char *text) const;
public:
FilterAdditionalName(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
AnyContainsBoost(locale, searchValue, mode),
m_operation(operation)
{
}
virtual bool matches(const IndividualData &data) const
{
FolksIndividual *individual = data.m_individual.get();
FolksStructuredName *fn =
folks_name_details_get_structured_name(FOLKS_NAME_DETAILS(individual));
if (fn) {
const char *name = folks_structured_name_get_additional_names(fn);
return (this->*m_operation)(name);
} else {
return false;
}
}
};
class FilterEmails : public AnyContainsBoost
{
bool (AnyContainsBoost::*m_operation)(const char *text) const;
public:
FilterEmails(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
AnyContainsBoost(locale, searchValue, mode),
m_operation(operation)
{
}
virtual bool matches(const IndividualData &data) const
{
FolksIndividual *individual = data.m_individual.get();
FolksEmailDetails *emailDetails = FOLKS_EMAIL_DETAILS(individual);
GeeSet *emails = folks_email_details_get_email_addresses(emailDetails);
BOOST_FOREACH (FolksAbstractFieldDetails *email, GeeCollCXX<FolksAbstractFieldDetails *>(emails, ADD_REF)) {
const gchar *value =
reinterpret_cast<const gchar *>(folks_abstract_field_details_get_value(email));
if ((this->*m_operation)(value)) {
return true;
}
}
return false;
}
};
class FilterTel : public AnyContainsBoost
{
bool (AnyContainsBoost::*m_operation)(const char *text) const;
public:
FilterTel(const std::locale &locale,
const std::string &searchValue,
bool (AnyContainsBoost::*operation)(const char *text) const) :
AnyContainsBoost(locale, searchValue, 0 /* doesn't matter */),
m_operation(operation)
{
}
virtual bool matches(const IndividualData &data) const
{
FolksIndividual *individual = data.m_individual.get();
FolksPhoneDetails *phoneDetails = FOLKS_PHONE_DETAILS(individual);
GeeSet *phones = folks_phone_details_get_phone_numbers(phoneDetails);
BOOST_FOREACH (FolksAbstractFieldDetails *phone, GeeCollCXX<FolksAbstractFieldDetails *>(phones, ADD_REF)) {
const gchar *value =
reinterpret_cast<const gchar *>(folks_abstract_field_details_get_value(phone));
if ((this->*m_operation)(value)) {
return true;
}
}
return false;
}
};
class FilterAddr : public AnyContainsBoost
{
protected:
bool (AnyContainsBoost::*m_operation)(const char *text) const;
public:
FilterAddr(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
AnyContainsBoost(locale, searchValue, mode),
m_operation(operation)
{
}
virtual bool matches(const IndividualData &data) const
{
FolksIndividual *individual = data.m_individual.get();
FolksPostalAddressDetails *addressDetails = FOLKS_POSTAL_ADDRESS_DETAILS(individual);
GeeSet *addresses = folks_postal_address_details_get_postal_addresses(addressDetails);
BOOST_FOREACH (FolksPostalAddressFieldDetails *address, GeeCollCXX<FolksPostalAddressFieldDetails *>(addresses, ADD_REF)) {
const FolksPostalAddress *value =
reinterpret_cast<const FolksPostalAddress *>(folks_abstract_field_details_get_value(FOLKS_ABSTRACT_FIELD_DETAILS(address)));
if (matchesAddr(value)) {
return true;
}
}
return false;
}
virtual bool matchesAddr(const FolksPostalAddress *addr) const = 0;
};
class FilterAddrPOBox : public FilterAddr
{
public:
FilterAddrPOBox(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
FilterAddr(locale, searchValue, mode, operation)
{
}
virtual bool matchesAddr(const FolksPostalAddress *addr) const
{
const char *attr = folks_postal_address_get_po_box(const_cast<FolksPostalAddress *>(addr));
return (this->*m_operation)(attr);
}
};
class FilterAddrExtension : public FilterAddr
{
public:
FilterAddrExtension(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
FilterAddr(locale, searchValue, mode, operation)
{
}
virtual bool matchesAddr(const FolksPostalAddress *addr) const
{
const char *attr = folks_postal_address_get_extension(const_cast<FolksPostalAddress *>(addr));
return (this->*m_operation)(attr);
}
};
class FilterAddrStreet : public FilterAddr
{
public:
FilterAddrStreet(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
FilterAddr(locale, searchValue, mode, operation)
{
}
virtual bool matchesAddr(const FolksPostalAddress *addr) const
{
const char *attr = folks_postal_address_get_street(const_cast<FolksPostalAddress *>(addr));
return (this->*m_operation)(attr);
}
};
class FilterAddrLocality : public FilterAddr
{
public:
FilterAddrLocality(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
FilterAddr(locale, searchValue, mode, operation)
{
}
virtual bool matchesAddr(const FolksPostalAddress *addr) const
{
const char *attr = folks_postal_address_get_locality(const_cast<FolksPostalAddress *>(addr));
return (this->*m_operation)(attr);
}
};
class FilterAddrRegion : public FilterAddr
{
public:
FilterAddrRegion(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
FilterAddr(locale, searchValue, mode, operation)
{
}
virtual bool matchesAddr(const FolksPostalAddress *addr) const
{
const char *attr = folks_postal_address_get_region(const_cast<FolksPostalAddress *>(addr));
return (this->*m_operation)(attr);
}
};
class FilterAddrPostalCode : public FilterAddr
{
public:
FilterAddrPostalCode(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
FilterAddr(locale, searchValue, mode, operation)
{
}
virtual bool matchesAddr(const FolksPostalAddress *addr) const
{
const char *attr = folks_postal_address_get_postal_code(const_cast<FolksPostalAddress *>(addr));
return (this->*m_operation)(attr);
}
};
class FilterAddrCountry : public FilterAddr
{
public:
FilterAddrCountry(const std::locale &locale,
const std::string &searchValue,
int mode,
bool (AnyContainsBoost::*operation)(const char *text) const) :
FilterAddr(locale, searchValue, mode, operation)
{
}
virtual bool matchesAddr(const FolksPostalAddress *addr) const
{
const char *attr = folks_postal_address_get_country(const_cast<FolksPostalAddress *>(addr));
return (this->*m_operation)(attr);
}
};
SimpleE164 String2E164(const char *tel, const char *country)
{
SimpleE164 e164;
GErrorCXX gerror;
EPhoneNumberCXX number(EPhoneNumberCXXNew(e_phone_number_from_string(tel, country, gerror)));
if (!number) {
gerror.throwError(SE_HERE, "parsing number");
}
EPhoneNumberCountrySource source;
e164.m_countryCode = e_phone_number_get_country_code(number.get(), &source);
if (source == E_PHONE_NUMBER_COUNTRY_FROM_DEFAULT) {
e164.m_countryCode = 0;
}
PlainGStr national(e_phone_number_get_national_number(number.get()));
e164.m_nationalNumber = national.get() ?
boost::lexical_cast<SimpleE164::NationalNumber_t>(national.get()) :
0;
return e164;
}
/**
* Search value must be a valid caller ID (with or without a country
* code). The telephone numbers in the contacts may or may not be
* valid; only valid ones will match. The user is expected to clean up
* that data to get exact matches for the others.
*
* The matching uses the same semantic as EQUALS_NATIONAL_PHONE_NUMBER:
* - If both numbers have an explicit country code, that code must be
* the same for a match.
* - If one or both numbers have no country code, matching the national
* part is enough for a match.
*/
class PhoneStartsWith : public IndividualFilter
{
public:
PhoneStartsWith(const std::locale &m_locale,
const std::string &tel) :
m_phoneNumberUtil(*i18n::phonenumbers::PhoneNumberUtil::GetInstance()),
m_simpleEDSSearch(getenv("SYNCEVOLUTION_PIM_EDS_SUBSTRING") || !e_phone_number_is_supported()),
m_country(std::use_facet<boost::locale::info>(m_locale).country())
{
m_number = String2E164(tel.c_str(), m_country.c_str());
}
virtual bool matches(const IndividualData &data) const
{
BOOST_FOREACH(const SimpleE164 &number, data.m_precomputed.m_phoneNumbers) {
// National part must always match, country code only if
// set explicitly in both (NSN_MATCH in libphonenumber,
// EQUALS_NATIONAL_PHONE_NUMBER in EDS).
if (number.m_nationalNumber == m_number.m_nationalNumber &&
(!number.m_countryCode || !m_number.m_countryCode ||
number.m_countryCode == m_number.m_countryCode)) {
return true;
}
}
return false;
}
virtual std::string getEBookFilter() const
{
std::string tel = m_number.toString();
size_t len = std::min((size_t)4, tel.size());
EBookQueryCXX query(m_simpleEDSSearch ?
// A suffix match with a limited number of digits is most
// likely to find the right contacts.
e_book_query_field_test(E_CONTACT_TEL, E_BOOK_QUERY_ENDS_WITH,
tel.substr(tel.size() - len, len).c_str()) :
// We use EQUALS_NATIONAL_PHONE_NUMBER
// instead of EQUALS_PHONE_NUMBER here,
// because it will also match contacts
// were the country code was not set
// explicitly. EQUALS_PHONE_NUMBER would
// do a stricter comparison and not match
// those.
//
// If the contact has a country code set,
// then EQUALS_NATIONAL_PHONE_NUMBER will
// check that and not return a false match
// if the country code is different.
//
// We try to pass the E164 string here. If
// the search term had no country code,
// that's a bit difficult because we can't
// just add the default country code.
// That would break the
// NATIONAL_PHONE_NUMBER semantic because
// EDS wouldn't know that the search term
// had no country code. We resort to the
// format of SimpleE164.toString(), which
// is passing the national number
// formatted as string.
e_book_query_field_test(E_CONTACT_TEL, E_BOOK_QUERY_EQUALS_NATIONAL_PHONE_NUMBER,
tel.c_str()),
TRANSFER_REF);
PlainGStr filter(e_book_query_to_string(query.get()));
return filter.get();
}
private:
const i18n::phonenumbers::PhoneNumberUtil &m_phoneNumberUtil;
bool m_simpleEDSSearch;
std::string m_country;
SimpleE164 m_number;
};
class PhoneNumberLogger : public i18n::phonenumbers::Logger
{
const char *getPrefix()
{
switch (level()) {
case i18n::phonenumbers::LOG_FATAL:
return "phonenumber fatal";
break;
case i18n::phonenumbers::LOG_ERROR:
return "phonenumber error";
break;
case i18n::phonenumbers::LOG_WARNING:
return "phonenumber warning";
break;
case i18n::phonenumbers::LOG_INFO:
return "phonenumber info";
break;
case i18n::phonenumbers::LOG_DEBUG:
return "phonenumber debug";
break;
default:
return "phonenumber ???";
break;
}
}
public:
virtual void WriteMessage(const std::string &msg)
{
SE_LOG(getPrefix(),
level() == i18n::phonenumbers::LOG_FATAL ? SyncEvo::Logger::ERROR : SyncEvo::Logger::DEBUG,
"%s", msg.c_str());
}
};
class LocaleFactoryBoost : public LocaleFactory
{
const i18n::phonenumbers::PhoneNumberUtil &m_phoneNumberUtil;
bool m_edsSupportsPhoneNumbers;
std::locale m_locale;
std::string m_country;
std::string m_defaultCountryCode;
PhoneNumberLogger m_logger;
public:
LocaleFactoryBoost() :
m_phoneNumberUtil(*i18n::phonenumbers::PhoneNumberUtil::GetInstance()),
m_edsSupportsPhoneNumbers(e_phone_number_is_supported() && !getenv("SYNCEVOLUTION_PIM_EDS_NO_E164")),
m_locale(genLocale()),
m_country(std::use_facet<boost::locale::info>(m_locale).country()),
m_defaultCountryCode(StringPrintf("+%d", m_phoneNumberUtil.GetCountryCodeForRegion(m_country)))
{
// Redirect output of libphonenumber and make it a bit quieter
// than it is by default. We map fatal libphonenumber errors
// to ERROR and everything else to DEBUG.
//
// The PhoneNumberUtil instance owns the logger, so we don't
// need (and must not) free it. libphonenumber < r571 has the
// same API and thus this code compiles. However, older
// libphonenumer does not actually free the instance, causing
// a minor memory leak.
i18n::phonenumbers::PhoneNumberUtil::GetInstance()->SetLogger(new PhoneNumberLogger);
}
static std::locale genLocale()
{
// Get current locale from environment. Configure the
// generated locale so that it supports what we need and
// nothing more.
boost::locale::generator gen;
gen.characters(boost::locale::char_facet);
gen.categories(boost::locale::collation_facet |
boost::locale::convert_facet |
boost::locale::information_facet);
// Hard-code "phonebook" collation for certain languages
// where we know that it is desirable. We could use it
// in all cases, except that ICU has a bug where it does not
// fall back properly to the base collation. See
// http://sourceforge.net/mailarchive/message.php?msg_id=30802924
// and http://bugs.icu-project.org/trac/ticket/10149
std::locale locale = gen("");
std::string name = std::use_facet<boost::locale::info>(locale).name();
std::string language = std::use_facet<boost::locale::info>(locale).language();
std::string country = std::use_facet<boost::locale::info>(locale).country();
SE_LOG_DEV(NULL, "PIM Manager running with locale %s = language %s in country %s",
name.c_str(),
language.c_str(),
country.c_str());
if (language == "de" ||
language == "fi") {
SE_LOG_DEV(NULL, "enabling phonebook collation for language %s", language.c_str());
locale = gen(name + "@collation=phonebook");
}
return locale;
}
virtual boost::shared_ptr<IndividualCompare> createCompare(const std::string &order)
{
boost::shared_ptr<IndividualCompare> res;
if (order == "first/last") {
res.reset(new CompareFirstLastBoost(m_locale));
} else if (order == "last/first") {
res.reset(new CompareLastFirstBoost(m_locale));
} else if (order == "fullname") {
res.reset(new CompareFullnameBoost(m_locale));
} else {
SE_THROW("boost locale factory: sort order '" + order + "' not supported");
}
return res;
}
virtual boost::shared_ptr<IndividualFilter> createFilter(const Filter_t &filter, int level)
{
boost::shared_ptr<IndividualFilter> res;
try {
const std::vector<Filter_t> &terms = getFilterArray(filter, "array of terms");
// Only handle arrays where the first entry is a string
// that we recognize. All other cases are handled by the generic
// LocaleFactory.
if (!terms.empty() &&
boost::get<std::string>(&terms[0])) {
const std::string &operation = getFilterString(terms[0], "operation name");
// Pick default operation. Will be replaced with
// telephone-specific operation once we know that the
// field is 'phones/value'.
bool (AnyContainsBoost::*func)(const char *text) const;
if (operation == "contains") {
func = &AnyContainsBoost::containsSearchText;
} else if (operation == "is") {
func = &AnyContainsBoost::isSearchText;
} else if (operation == "begins-with") {
func = &AnyContainsBoost::beginsWithSearchText;
} else if (operation == "ends-with") {
func = &AnyContainsBoost::endsWithSearchText;
} else {
func = NULL;
}
if (func) {
switch (terms.size()) {
case 1:
SE_THROW("missing field name and search value");
break;
case 2:
SE_THROW("missing search value");
break;
}
const std::string &field = getFilterString(terms[1], "search field");
const std::string &value = getFilterString(terms[2], "search string");
if (field == "phones/value") {
if (terms.size() > 3) {
SE_THROW("Additional entries after 'phones/value' field filter not allowed.");
}
// Use the telephone specific functions.
res.reset(new FilterTel(m_locale, value,
func == &AnyContainsBoost::containsSearchText ? &AnyContainsBoost::containsSearchTel :
func == &AnyContainsBoost::isSearchText ? &AnyContainsBoost::isSearchTel :
func == &AnyContainsBoost::beginsWithSearchText ? &AnyContainsBoost::beginsWithSearchTel :
func == &AnyContainsBoost::endsWithSearchText ? &AnyContainsBoost::endsWithSearchTel :
func));
} else {
int mode = AnyContainsBoost::getFilterMode(terms, 3);
if (field == "full-name") {
res.reset(new FilterFullName(m_locale, value, mode, func));
} else if (field == "nickname") {
res.reset(new FilterNickname(m_locale, value, mode, func));
} else if (field == "structured-name/family") {
res.reset(new FilterFamilyName(m_locale, value, mode, func));
} else if (field == "structured-name/given") {
res.reset(new FilterGivenName(m_locale, value, mode, func));
} else if (field == "structured-name/additional") {
res.reset(new FilterAdditionalName(m_locale, value, mode, func));
} else if (field == "emails/value") {
res.reset(new FilterEmails(m_locale, value, mode, func));
} else if (field == "addresses/po-box") {
res.reset(new FilterAddrPOBox(m_locale, value, mode, func));
} else if (field == "addresses/extension") {
res.reset(new FilterAddrExtension(m_locale, value, mode, func));
} else if (field == "addresses/street") {
res.reset(new FilterAddrStreet(m_locale, value, mode, func));
} else if (field == "addresses/locality") {
res.reset(new FilterAddrLocality(m_locale, value, mode, func));
} else if (field == "addresses/region") {
res.reset(new FilterAddrRegion(m_locale, value, mode, func));
} else if (field == "addresses/postal-code") {
res.reset(new FilterAddrPostalCode(m_locale, value, mode, func));
} else if (field == "addresses/country") {
res.reset(new FilterAddrCountry(m_locale, value, mode, func));
} else {
SE_THROW("Unknown field name: " + field);
}
}
} else if (operation == "any-contains") {
if (terms.size() < 2) {
SE_THROW("missing search value");
}
const std::string &value = getFilterString(terms[1], "search string");
int mode = AnyContainsBoost::getFilterMode(terms, 2);
res.reset(new AnyContainsBoost(m_locale, value, mode));
} else if (operation == "phone") {
if (terms.size() != 2) {
SE_THROW("'phone' filter needs exactly one parameter.");
}
const std::string &value = getFilterString(terms[1], "search string");
res.reset(new PhoneStartsWith(m_locale, value));
}
}
} catch (const Exception &ex) {
handleFilterException(filter, level, &ex.m_file, ex.m_line);
} catch (...) {
handleFilterException(filter, level, NULL, 0);
}
// Let base class handle it if we didn't recognize the operation.
return res ? res : LocaleFactory::createFilter(filter, level);
}
virtual bool precompute(FolksIndividual *individual, Precomputed &precomputed) const
{
LocaleFactory::Precomputed old;
std::swap(old, precomputed);
FolksPhoneDetails *phoneDetails = FOLKS_PHONE_DETAILS(individual);
GeeSet *phones = folks_phone_details_get_phone_numbers(phoneDetails);
precomputed.m_phoneNumbers.reserve(gee_collection_get_size(GEE_COLLECTION(phones)));
BOOST_FOREACH (FolksAbstractFieldDetails *phone, GeeCollCXX<FolksAbstractFieldDetails *>(phones, ADD_REF)) {
const gchar *value =
reinterpret_cast<const gchar *>(folks_abstract_field_details_get_value(phone));
if (value) {
if (m_edsSupportsPhoneNumbers) {
// Check X-EVOLUTION-E164 (made lowercase by folks!).
//
// It has the format <local number>,<country code>,
// where <country code> happens to be in quotation marks.
// This ends up being split into individual values which
// are returned in random order by folks (a bug?!).
//
// Example: TEL;X-EVOLUTION-E164=891234,"+49":+49-89-1234
// => value '+49-89-1234', params [ '+49', '891234' ].
//
// We restore the right order by sorting, which puts the
// country code first, and then joining.
GeeCollectionCXX coll(folks_abstract_field_details_get_parameter_values(phone, "x-evolution-e164"), TRANSFER_REF);
if (coll) {
std::vector<std::string> components;
components.reserve(2);
BOOST_FOREACH (const gchar *component, GeeStringCollection(coll)) {
// Empty component represents an unset
// country code. Note that it is not
// certain whether we get to see the empty
// component. At the moment (EDS 3.7,
// folks 0.9.1), someone swallows it.
components.push_back(component);
}
if (!components.empty()) {
// Only one component? We must still miss the country code.
if (components.size() == 1) {
components.push_back("");
}
std::sort(components.begin(), components.end());
try {
SimpleE164 number;
number.m_countryCode = components[0].empty() ?
0 :
boost::lexical_cast<SimpleE164::CountryCode_t>(components[0]);
number.m_nationalNumber = components[1].empty() ?
0 :
boost::lexical_cast<SimpleE164::NationalNumber_t>(components[1]);
precomputed.m_phoneNumbers.push_back(number);
} catch (const boost::bad_lexical_cast &ex) {
SE_LOG_WARNING(NULL, "ignoring malformed X-EVOLUTION-E164 (sorted): %s",
boost::join(components, ", ").c_str());
}
}
}
// Either EDS had a normalized value or there is none because
// the value is not a phone number. No need to try parsing again.
continue;
}
try {
// This fallback for missing X-EVOLUTION-E164 in EDS still relies
// on libphonenumber support in libebook, so it does not really help
// if EDS was compiled without libphonenumber. It is primarily useful
// for testing (see TestContacts.testLocaledPhone).
SimpleE164 e164 = String2E164(value, m_country.c_str());
if (e164.m_countryCode || e164.m_nationalNumber) {
precomputed.m_phoneNumbers.push_back(e164);
}
} catch (const Exception &ex) {
// Silently ignore parse errors.
SE_LOG_DEBUG(NULL, "ignoring unparsable TEL '%s': %s", value, ex.what());
}
}
}
// Now check if any phone number changed.
return old != precomputed;
}
};
boost::shared_ptr<LocaleFactory> LocaleFactory::createFactory()
{
return boost::shared_ptr<LocaleFactory>(new LocaleFactoryBoost());
}
SE_END_CXX