README: Rename project from 'qss' to 'looqs'
This commit is contained in:
305
shared/looqsquery.cpp
Normal file
305
shared/looqsquery.cpp
Normal file
@ -0,0 +1,305 @@
|
||||
#include <QStack>
|
||||
#include <QRegularExpression>
|
||||
#include <QSqlQuery>
|
||||
#include <QSqlError>
|
||||
#include <QStringList>
|
||||
#include <QDebug>
|
||||
#include <optional>
|
||||
#include <algorithm>
|
||||
#include "qssquery.h"
|
||||
|
||||
const QVector<Token> &QSSQuery::getTokens() const
|
||||
{
|
||||
return tokens;
|
||||
}
|
||||
|
||||
const QVector<SortCondition> &QSSQuery::getSortConditions() const
|
||||
{
|
||||
return sortConditions;
|
||||
}
|
||||
|
||||
QueryType QSSQuery::getQueryType()
|
||||
{
|
||||
return static_cast<QueryType>(tokensMask & COMBINED);
|
||||
}
|
||||
|
||||
void QSSQuery::addSortCondition(SortCondition sc)
|
||||
{
|
||||
this->sortConditions.append(sc);
|
||||
}
|
||||
|
||||
bool QSSQuery::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 QSSGeneralException("sort specifier must have format [field] (asc|desc)");
|
||||
}
|
||||
|
||||
QString field = splitted[0];
|
||||
auto queryField = fromString(field);
|
||||
if(!queryField)
|
||||
{
|
||||
throw QSSGeneralException("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 QSSGeneralException("Unknown order specifier: " + order);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
order = ASC;
|
||||
}
|
||||
|
||||
SortCondition condition;
|
||||
condition.field = queryField.value();
|
||||
condition.order = order;
|
||||
result.append(condition);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void QSSQuery::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*/
|
||||
QSSQuery QSSQuery::build(QString expression)
|
||||
{
|
||||
if(!checkParanthesis(expression))
|
||||
{
|
||||
throw QSSGeneralException("Invalid paranthesis");
|
||||
}
|
||||
|
||||
QSSQuery result;
|
||||
// TODO: merge lonewords
|
||||
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 QSSGeneralException("Can't have two booleans following each other");
|
||||
}
|
||||
if(previousWas(NEGATION))
|
||||
{
|
||||
throw QSSGeneralException("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 QSSGeneralException("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 != "")
|
||||
{
|
||||
result.addToken(Token(FILTER_PATH_CONTAINS, 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 QSSGeneralException("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 QSSGeneralException("Unknown filter provided!");
|
||||
}
|
||||
result.addToken(Token(tokenType, value));
|
||||
}
|
||||
}
|
||||
|
||||
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 QSSGeneralException("We cannot sort by text if we don't search for it");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
Reference in New Issue
Block a user