PIM: support recursive search filter

This changes the signature of the filter parameter in Search(),
RefineSearch() and ReplaceSearch() from 'as' (array of strings) to
'av' (array of variants). Allowed entries in the variant are arrays
containing strings and/or other such arrays (recursive!).

A single string as value is not supported, which is the reason why
'av' instead of just plain 'v' makes sense (mismatches can already be
found in the sender, potentially at compile time). It also helps
Python choose the right type when asked to send an empty list. When
just using 'v', Python cannot decide automatically.

Error messages include a backtrace of all terms that the current,
faulty term was included in. That helps to locate the error in a
potentially larger filter.

The scope of supported searches has not changed (yet).
This commit is contained in:
Patrick Ohly 2013-05-28 15:22:58 +02:00
parent 4a617c1cc9
commit 9942cecd84
7 changed files with 275 additions and 68 deletions

View File

@ -189,6 +189,15 @@ class MatchAll : public IndividualFilter
virtual bool matches(const IndividualData &data) const { return true; }
};
/**
* A fake filter which just carries the maximum result parameter.
* Separate type because the dynamic_cast<> can be used to detect
* this special case.
*/
class ParamFilter : public MatchAll
{
};
class FullView;
/**

View File

@ -29,7 +29,6 @@
#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>
@ -552,65 +551,53 @@ public:
return res;
}
virtual boost::shared_ptr<IndividualFilter> createFilter(const Filter_t &filter)
virtual boost::shared_ptr<IndividualFilter> createFilter(const Filter_t &filter, int level)
{
int maxResults = -1;
boost::shared_ptr<IndividualFilter> res;
BOOST_FOREACH (const Filter_t::value_type &term, filter) {
if (term.empty()) {
SE_THROW("boost locale factory: empty search term not supported");
}
// Check for flags.
if (term[0] == "limit") {
if (term.size() != 2) {
SE_THROW("boost locale factory: 'limit' needs exactly one parameter");
}
maxResults = boost::lexical_cast<int>(term[1]);
} else if (term[0] == "any-contains") {
if (res) {
SE_THROW("boost locale factory: already have a search filter, 'any-contains' not valid");
}
AnyContainsBoost::Mode mode = AnyContainsBoost::CASE_INSENSITIVE;
if (term.size() < 2) {
SE_THROW("boost locale factory: any-contains search needs one parameter");
}
for (size_t i = 2; i < term.size(); i++) {
const std::string &flag = term[i];
if (flag == "case-sensitive") {
mode = AnyContainsBoost::CASE_SENSITIVE;
} else if (flag == "case-insensitive") {
mode = AnyContainsBoost::CASE_INSENSITIVE;
} else {
SE_THROW("boost locale factory: unknown flag for any-contains: " + flag);
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");
if (operation == "any-contains") {
if (terms.size() < 2) {
SE_THROW("missing search value");
}
const std::string &value = getFilterString(terms[1], "search string");
AnyContainsBoost::Mode mode = AnyContainsBoost::CASE_INSENSITIVE;
for (size_t i = 2; i < terms.size(); i++) {
const std::string flag = getFilterString(terms[i], "any-contains flag");
if (flag == "case-sensitive") {
mode = AnyContainsBoost::CASE_SENSITIVE;
} else if (flag == "case-insensitive") {
mode = AnyContainsBoost::CASE_INSENSITIVE;
} else {
SE_THROW("unsupported flag for any-contains: " + flag);
}
}
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));
}
res.reset(new AnyContainsBoost(m_locale, term[1], mode));
} else if (term[0] == "phone") {
if (res) {
SE_THROW("boost locale factory: already have a search filter, 'phone' not valid");
}
if (filter.size() != 1) {
SE_THROW(StringPrintf("boost locale factory: only filter with one term are supported (was given %ld)",
(long)filter.size()));
}
res.reset(new PhoneStartsWith(m_locale,
term[1]));
} else {
SE_THROW("boost locale factory: unknown search term: " + term[0]);
}
} catch (const Exception &ex) {
handleFilterException(filter, level, &ex.m_file, ex.m_line);
} catch (...) {
handleFilterException(filter, level, NULL, 0);
}
// May be empty (unfiltered). Create a filter which matches
// everything, because otherwise we end up using the FullView,
// which cannot apply a limit or later switch to a different
// search.
if (!res) {
res.reset(new MatchAll());
}
res->setMaxResults(maxResults);
return res;
// Let base class handle it if we didn't recognize the operation.
return res ? res : LocaleFactory::createFilter(filter, level);
}
virtual void precompute(FolksIndividual *individual, Precomputed &precomputed) const

View File

@ -0,0 +1,172 @@
/*
* 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
*/
/**
* Common code for sorting and searching.
*/
#include "locale-factory.h"
#include "folks.h"
#include <boost/lexical_cast.hpp>
#include <sstream>
SE_BEGIN_CXX
class Filter2StringVisitor : public boost::static_visitor<void>
{
std::ostringstream m_out;
public:
void operator () (const std::string &str)
{
m_out << "'" << str << "'";
}
void operator () (const std::vector<LocaleFactory::Filter_t> &filter)
{
m_out << "[";
for (size_t i = 0; i < filter.size(); i++) {
if (i == 0) {
m_out << " ";
} else {
m_out << ", ";
}
boost::apply_visitor(*this, filter[i]);
}
m_out << " ]";
}
std::string toString() { return m_out.str(); }
};
std::string LocaleFactory::Filter2String(const Filter_t &filter)
{
Filter2StringVisitor visitor;
boost::apply_visitor(visitor, filter);
return visitor.toString();
}
template <class V> const V &getFilter(const LocaleFactory::Filter_t &filter, const char *expected)
{
const V *value = boost::get<V>(&filter);
if (!value) {
throw std::runtime_error(StringPrintf("expected %s, got instead: %s",
expected, LocaleFactory::Filter2String(filter).c_str()));
}
return *value;
}
const std::string &LocaleFactory::getFilterString(const Filter_t &filter, const char *expected)
{
return getFilter<std::string>(filter, expected);
}
const std::vector<LocaleFactory::Filter_t> &LocaleFactory::getFilterArray(const Filter_t &filter, const char *expected)
{
return getFilter< std::vector<Filter_t> >(filter, expected);
}
void LocaleFactory::handleFilterException(const Filter_t &filter, int level, const std::string *file, int line)
{
std::string what;
Exception::handle(what, HANDLE_EXCEPTION_NO_ERROR);
what = StringPrintf("%s nesting level %d: %s\n%s",
level == 0 ? "Error while parsing a search filter.\nMost specific term comes last, then the error message:\n" : "",
level,
Filter2String(filter).c_str(),
what.c_str());
if (file) {
throw Exception(*file, line, what);
} else {
throw std::runtime_error(what);
}
}
boost::shared_ptr<IndividualFilter> LocaleFactory::createFilter(const Filter_t &filter, int level)
{
boost::shared_ptr<IndividualFilter> res;
try {
const std::vector<Filter_t> &terms = getFilterArray(filter, "array of terms");
if (terms.empty()) {
res.reset(new MatchAll());
return res;
}
// Array of arrays?
// May contain search parameters ('limit') and one
// filter expression.
if (boost::get< std::vector<Filter_t> >(&terms[0])) {
boost::shared_ptr<IndividualFilter> params;
BOOST_FOREACH (const Filter_t &subfilter, terms) {
boost::shared_ptr<IndividualFilter> tmp = createFilter(subfilter, level + 1);
if (dynamic_cast<ParamFilter *>(tmp.get())) {
// New parameter overwrites old one. If we ever
// want to support more than one parameter, we
// need to be more selective here.
params = tmp;
} else if (!res) {
res = tmp;
} else {
SE_THROW("Filter can only be combined with other filters inside a logical operation.");
}
}
if (params) {
if (res) {
// Copy parameter(s) to real filter.
res->setMaxResults(params->getMaxResults());
} else {
// Or just use it as-is because no filter was
// given. It'll work like MatchAll.
res = params;
}
}
} else {
// Not an array, so must be string.
const std::string &operation = getFilterString(terms[0], "operation name");
if (operation == "limit") {
// Level 0 is the [] containing the ['limit', ...].
// We thus expect it at level 1.
if (level != 1) {
SE_THROW("'limit' parameter only allowed at top level.");
}
if (terms.size() != 2) {
SE_THROW("'limit' needs exactly one parameter.");
}
const std::string &limit = getFilterString(terms[1], "'filter' value as string");
int maxResults = boost::lexical_cast<int>(limit);
res.reset(new ParamFilter());
res->setMaxResults(maxResults);
} else {
SE_THROW(StringPrintf("Unknown operation '%s'", operation.c_str()));
}
}
} catch (const Exception &ex) {
handleFilterException(filter, level, &ex.m_file, ex.m_line);
} catch (...) {
handleFilterException(filter, level, NULL, 0);
}
return res;
}
SE_END_CXX

View File

@ -27,6 +27,7 @@
#define INCL_SYNCEVO_DBUS_SERVER_PIM_LOCALE_FACTORY
#include <boost/shared_ptr.hpp>
#include <boost/variant.hpp>
#include <folks/folks.h>
@ -62,22 +63,46 @@ class LocaleFactory
virtual boost::shared_ptr<IndividualCompare> createCompare(const std::string &order) = 0;
/**
* An array of search terms which all must match. Each search term
* itself is again an array of strings, with the first one choosing
* the search criteria and the rest providing parameters for that
* search term.
* A recursive definition of a search expression.
* All operand names, field names and values are strings.
*/
typedef std::vector< std::vector<std::string> > Filter_t;
typedef boost::make_recursive_variant<
std::string,
std::vector< boost::recursive_variant_ >
>::type Filter_t;
/**
* Simplified JSON representation (= no escaping of special characters),
* for debugging and error reporting.
*/
static std::string Filter2String(const Filter_t &filter);
/**
* Throws "expected <item>, got instead: <filter as string>" when
* conversion to V fails.
*/
static const std::string &getFilterString(const Filter_t &filter, const char *expected);
static const std::vector<Filter_t> &getFilterArray(const Filter_t &filter, const char *expected);
/**
* Creates a filter instance or throws an error when that is not
* possible.
*
* @param order factory-specific string which chooses one of
* the search criteria supported by the factory
* @param represents a (sub-)filter
* @level 0 at the root of the filter, incremented by one for each
* non-trivial indirection; i.e., [ [ <filter> ] ] still
* treats <filter> as if it was the root search
*
* @return a valid instance, must not be NULL
*/
virtual boost::shared_ptr<IndividualFilter> createFilter(const Filter_t &filter) = 0;
virtual boost::shared_ptr<IndividualFilter> createFilter(const Filter_t &filter, int level) = 0;
/**
* To be called when parsing a Filter_t caused an exception.
* Will add information about the filter and a preamble, if
* called at the top level.
*/
static void handleFilterException(const Filter_t &filter, int level, const std::string *file, int line);
/**
* Pre-computed data for a single FolksIndividual which will be needed

View File

@ -762,14 +762,16 @@ public:
}
/** ViewControl.RefineSearch() */
void refineSearch(const LocaleFactory::Filter_t &filter)
void refineSearch(const std::vector<LocaleFactory::Filter_t> &filterArray)
{
replaceSearch(filter, true);
replaceSearch(filterArray, true);
}
void replaceSearch(const LocaleFactory::Filter_t &filter, bool refine)
void replaceSearch(const std::vector<LocaleFactory::Filter_t> &filterArray, bool refine)
{
boost::shared_ptr<IndividualFilter> individualFilter = m_locale->createFilter(filter);
// Same as in Search().
LocaleFactory::Filter_t filter = filterArray;
boost::shared_ptr<IndividualFilter> individualFilter = m_locale->createFilter(filter, 0);
m_view->replaceFilter(individualFilter, refine);
}
};
@ -778,7 +780,7 @@ unsigned int ViewResource::m_counter;
void Manager::search(const boost::shared_ptr< GDBusCXX::Result1<GDBusCXX::DBusObject_t> > &result,
const GDBusCXX::Caller_t &ID,
const boost::shared_ptr<GDBusCXX::Watch> &watch,
const LocaleFactory::Filter_t &filter,
const std::vector<LocaleFactory::Filter_t> &filterVector,
const GDBusCXX::DBusObject_t &agentPath)
{
// TODO: figure out a native, thread-safe API for this.
@ -786,6 +788,18 @@ void Manager::search(const boost::shared_ptr< GDBusCXX::Result1<GDBusCXX::DBusOb
// Start folks in parallel with asking for an ESourceRegistry.
start();
// We use a std::vector as outer type to help Python decide how to
// send the empty list []. When we declare our parameter as
// variant instead of array of variants, as we do now, then the
// Python programmer has to use dbus.Array([], signature='s'),
// which breaks backwards compatibility (wasn't necessary earlier)
// and is not easy to use.
//
// But before we can pass the filter on, we need to turn it into
// a variant containing the vector.
LocaleFactory::Filter_t filter;
filter = filterVector;
// We don't know for sure whether we'll need the ESourceRegistry.
// Ask for it, just to be sure. If we need to hurry because we are
// doing a caller ID lookup during startup, then we'll need it.
@ -840,7 +854,7 @@ void Manager::doSearch(const ESourceRegistryCXX &registry,
std::string ebookFilter;
// Always use a filtered view. That way we can implement ReplaceView or RefineView
// without having to switch from a FullView to a FilteredView.
boost::shared_ptr<IndividualFilter> individualFilter = m_locale->createFilter(filter);
boost::shared_ptr<IndividualFilter> individualFilter = m_locale->createFilter(filter, 0);
ebookFilter = individualFilter->getEBookFilter();
if (quiescent) {
// Don't search via EDS directly because the unified

View File

@ -80,7 +80,7 @@ class Manager : public GDBusCXX::DBusObjectHelper
void search(const boost::shared_ptr< GDBusCXX::Result1<GDBusCXX::DBusObject_t> > &result,
const GDBusCXX::Caller_t &ID,
const boost::shared_ptr<GDBusCXX::Watch> &watch,
const LocaleFactory::Filter_t &filter,
const std::vector<LocaleFactory::Filter_t> &filter,
const GDBusCXX::DBusObject_t &agentPath);
private:
void searchWithRegistry(const ESourceRegistryCXX &registry,

View File

@ -67,13 +67,13 @@ src_dbus_server_server_cpp_files += \
src/dbus/server/pim/full-view.cpp \
src/dbus/server/pim/filtered-view.cpp \
src/dbus/server/pim/edsf-view.cpp \
src/dbus/server/pim/locale-factory.cpp \
src/dbus/server/pim/merge-view.cpp \
src/dbus/server/pim/individual-traits.cpp \
src/dbus/server/pim/folks.cpp \
src/dbus/server/pim/manager.cpp
src_dbus_server_server_h_files += \
src/dbus/server/pim/locale-factory.h \
src/dbus/server/pim/persona-details.h
nodist_src_dbus_server_libsyncevodbusserver_la_SOURCES += \