Albert S
42b49fa43e
Search for content and paths. Merge lone words for content search. This behaviour is much more natural than typing "c:()".
318 řádky
7.1 KiB
C++
318 řádky
7.1 KiB
C++
#include <QStack>
|
|
#include <QRegularExpression>
|
|
#include <QSqlQuery>
|
|
#include <QSqlError>
|
|
#include <QStringList>
|
|
#include <QDebug>
|
|
#include <optional>
|
|
#include <algorithm>
|
|
#include "looqsquery.h"
|
|
|
|
const QVector<Token> &LooqsQuery::getTokens() const
|
|
{
|
|
return tokens;
|
|
}
|
|
|
|
const QVector<SortCondition> &LooqsQuery::getSortConditions() const
|
|
{
|
|
return sortConditions;
|
|
}
|
|
|
|
QueryType LooqsQuery::getQueryType()
|
|
{
|
|
return static_cast<QueryType>(tokensMask & COMBINED);
|
|
}
|
|
|
|
void LooqsQuery::addSortCondition(SortCondition sc)
|
|
{
|
|
this->sortConditions.append(sc);
|
|
}
|
|
|
|
bool LooqsQuery::checkParanthesis(QString expression)
|
|
{
|
|
QStack<QChar> open;
|
|
QStack<QChar> close;
|
|
|
|
bool inQuotes = false;
|
|
|
|
for(QChar &c : expression)
|
|
{
|
|
if(!inQuotes)
|
|
{
|
|
if(c == '(')
|
|
{
|
|
open.push(c);
|
|
}
|
|
if(c == ')')
|
|
{
|
|
close.push(c);
|
|
}
|
|
}
|
|
if(c == '"')
|
|
{
|
|
inQuotes = !inQuotes;
|
|
}
|
|
}
|
|
if(open.size() != close.size())
|
|
{
|
|
return false;
|
|
}
|
|
while(!open.empty() && !close.empty())
|
|
{
|
|
QChar o = open.pop();
|
|
QChar c = close.pop();
|
|
if(o != '(' && c != ')')
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::optional<QueryField> fromString(QString fieldString)
|
|
{
|
|
if(fieldString == "path" || fieldString == "file.path")
|
|
{
|
|
return FILE_PATH;
|
|
}
|
|
else if(fieldString == "mtime" || fieldString == "file.mtime")
|
|
{
|
|
return FILE_MTIME;
|
|
}
|
|
else if(fieldString == "size" || fieldString == "file.size")
|
|
{
|
|
return FILE_SIZE;
|
|
}
|
|
else if(fieldString == "content.text")
|
|
{
|
|
return CONTENT_TEXT;
|
|
}
|
|
else if(fieldString == "content.page" || fieldString == "page")
|
|
{
|
|
return CONTENT_TEXT_PAGE;
|
|
}
|
|
return {};
|
|
}
|
|
|
|
// sort:(mtime desc, page asc)
|
|
QVector<SortCondition> createSortConditions(QString sortExpression)
|
|
{
|
|
QVector<SortCondition> result;
|
|
QStringList splitted_inner = sortExpression.split(",");
|
|
for(int i = 0; i < splitted_inner.length(); i++)
|
|
{
|
|
QStringList splitted = splitted_inner[i].split(" ");
|
|
if(splitted.length() < 1 || splitted.length() > 2)
|
|
{
|
|
throw LooqsGeneralException("sort specifier must have format [field] (asc|desc)");
|
|
}
|
|
|
|
QString field = splitted[0];
|
|
auto queryField = fromString(field);
|
|
if(!queryField)
|
|
{
|
|
throw LooqsGeneralException("Unknown sort field supplied");
|
|
}
|
|
|
|
SortOrder order;
|
|
if(splitted.length() == 2)
|
|
{
|
|
QString orderstr = splitted[1];
|
|
if(orderstr.compare("asc", Qt::CaseInsensitive) == 0)
|
|
{
|
|
order = ASC;
|
|
}
|
|
else if(orderstr.compare("desc", Qt::CaseInsensitive) == 0)
|
|
{
|
|
order = DESC;
|
|
}
|
|
else
|
|
{
|
|
throw LooqsGeneralException("Unknown order specifier: " + orderstr);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
order = ASC;
|
|
}
|
|
|
|
SortCondition condition;
|
|
condition.field = queryField.value();
|
|
condition.order = order;
|
|
result.append(condition);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void LooqsQuery::addToken(Token t)
|
|
{
|
|
tokens.append(t);
|
|
tokensMask |= t.type;
|
|
}
|
|
|
|
/* Builds the query from the supplied expression
|
|
*
|
|
* AND is the default boolean operator, when the user does not provide any
|
|
* thus, "Downloads zip" becomes essentailly "path.contains:(Downloads) AND path.contains:(zip)"
|
|
*
|
|
* TODO: It's a bit ugly still*/
|
|
LooqsQuery LooqsQuery::build(QString expression, TokenType loneWordsTokenType, bool mergeLoneWords)
|
|
{
|
|
if(!checkParanthesis(expression))
|
|
{
|
|
throw LooqsGeneralException("Invalid paranthesis");
|
|
}
|
|
|
|
QStringList loneWords;
|
|
LooqsQuery result;
|
|
QRegularExpression rx("((?<filtername>(\\.|\\w)+):(?<args>\\((?<innerargs>[^\\)]+)\\)|([\\w,])+)|(?<boolean>AND|OR)"
|
|
"|(?<negation>!)|(?<bracket>\\(|\\))|(?<loneword>\\w+))");
|
|
QRegularExpressionMatchIterator i = rx.globalMatch(expression);
|
|
auto previousWasBool = [&result] { return !result.tokens.empty() && ((result.tokens.last().type & BOOL) == BOOL); };
|
|
auto previousWas = [&result](TokenType t) { return !result.tokens.empty() && (result.tokens.last().type == t); };
|
|
|
|
while(i.hasNext())
|
|
{
|
|
QRegularExpressionMatch m = i.next();
|
|
QString boolean = m.captured("boolean");
|
|
QString negation = m.captured("negation");
|
|
QString filtername = m.captured("filtername");
|
|
QString bracket = m.captured("bracket");
|
|
QString loneword = m.captured("loneword");
|
|
|
|
if(boolean != "")
|
|
{
|
|
if(previousWasBool())
|
|
{
|
|
throw LooqsGeneralException("Can't have two booleans following each other");
|
|
}
|
|
if(previousWas(NEGATION))
|
|
{
|
|
throw LooqsGeneralException("Can't have a negation preceeding a boolean");
|
|
}
|
|
if(boolean == "AND")
|
|
{
|
|
result.addToken(Token(BOOL_AND));
|
|
}
|
|
else if(boolean == "OR")
|
|
{
|
|
result.addToken(Token(BOOL_OR));
|
|
}
|
|
}
|
|
if(negation != "")
|
|
{
|
|
if(previousWas(NEGATION))
|
|
{
|
|
throw LooqsGeneralException("Can't have two negations following each other");
|
|
}
|
|
if(!previousWasBool())
|
|
{
|
|
result.addToken(Token(BOOL_AND)); // Implicit and, our default operation
|
|
}
|
|
result.addToken(Token(NEGATION));
|
|
}
|
|
if(!result.tokens.isEmpty() && !previousWasBool() && !previousWas(NEGATION) && !previousWas(BRACKET_OPEN) &&
|
|
bracket != ")")
|
|
{
|
|
// the current token isn't a negation, isn't a boolean. Thus, implicit AND is required
|
|
result.addToken(Token(BOOL_AND));
|
|
}
|
|
|
|
if(bracket != "")
|
|
{
|
|
if(bracket == "(")
|
|
{
|
|
result.addToken(Token(BRACKET_OPEN));
|
|
}
|
|
else
|
|
{
|
|
result.addToken(Token(BRACKET_CLOSE));
|
|
}
|
|
}
|
|
|
|
if(loneword != "")
|
|
{
|
|
if(mergeLoneWords)
|
|
{
|
|
loneWords.append(loneword);
|
|
}
|
|
else
|
|
{
|
|
result.addToken(Token(loneWordsTokenType, loneword));
|
|
}
|
|
}
|
|
|
|
if(filtername != "")
|
|
{
|
|
TokenType tokenType;
|
|
QString value = m.captured("innerargs");
|
|
if(value == "")
|
|
{
|
|
value = m.captured("args");
|
|
}
|
|
|
|
if(filtername == "path.contains")
|
|
{
|
|
tokenType = FILTER_PATH_CONTAINS;
|
|
}
|
|
else if(filtername == "path.starts")
|
|
{
|
|
tokenType = FILTER_PATH_STARTS;
|
|
}
|
|
else if(filtername == "path.ends")
|
|
{
|
|
tokenType = FILTER_PATH_ENDS;
|
|
}
|
|
else if(filtername == "file.size" || filtername == "size")
|
|
{
|
|
tokenType = FILTER_PATH_SIZE;
|
|
}
|
|
else if(filtername == "c" || filtername == "contains")
|
|
{
|
|
tokenType = FILTER_CONTENT_CONTAINS;
|
|
}
|
|
else if(filtername == "page" || filtername == "content.page")
|
|
{
|
|
tokenType = FILTER_CONTENT_PAGE;
|
|
}
|
|
else if(filtername ==
|
|
"sort") // TODO: given this is not really a "filter", this feels slightly misplaced here
|
|
{
|
|
if(!result.sortConditions.empty())
|
|
{
|
|
throw LooqsGeneralException("Two sort statements are illegal");
|
|
}
|
|
// TODO: hack, since we are not a "filter", we must remove a preceeding (implicit) boolean
|
|
if((result.tokens.last().type & BOOL) == BOOL)
|
|
{
|
|
result.tokens.pop_back();
|
|
}
|
|
result.sortConditions = createSortConditions(value);
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
throw LooqsGeneralException("Unknown filter provided!");
|
|
}
|
|
result.addToken(Token(tokenType, value));
|
|
}
|
|
}
|
|
|
|
if(mergeLoneWords)
|
|
{
|
|
result.addToken(Token(loneWordsTokenType, loneWords.join(' ')));
|
|
}
|
|
|
|
bool contentsearch = (result.getTokensMask() & FILTER_CONTENT) == FILTER_CONTENT;
|
|
bool sortsForContent = std::any_of(result.sortConditions.begin(), result.sortConditions.end(),
|
|
[](SortCondition c) { return c.field == CONTENT_TEXT; });
|
|
|
|
if(!contentsearch && sortsForContent)
|
|
{
|
|
throw LooqsGeneralException("We cannot sort by text if we don't search for it");
|
|
}
|
|
|
|
return result;
|
|
}
|