#include #include #include #include #include #include #include #include #include "looqsquery.h" const QVector &LooqsQuery::getTokens() const { return tokens; } const QVector &LooqsQuery::getSortConditions() const { return sortConditions; } QueryType LooqsQuery::getQueryType() { return static_cast(tokensMask & COMBINED); } void LooqsQuery::addSortCondition(SortCondition sc) { this->sortConditions.append(sc); } bool LooqsQuery::checkParanthesis(QString expression) { QStack open; QStack 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 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 createSortConditions(QString sortExpression) { QVector 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) { if(!checkParanthesis(expression)) { throw LooqsGeneralException("Invalid paranthesis"); } LooqsQuery result; // TODO: merge lonewords QRegularExpression rx("((?(\\.|\\w)+):(?\\((?[^\\)]+)\\)|([\\w,])+)|(?AND|OR)" "|(?!)|(?\\(|\\))|(?\\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 != "") { 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 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)); } } 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; }