looqs/shared/looqsquery.cpp
Albert S 42b49fa43e gui: Perform content search and path search by default
Search for content and paths. Merge lone words for content search.

This behaviour is much more natural than typing "c:()".
2022-01-01 17:58:52 +01:00

318 lines
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;
}