File indexing completed on 2024-04-21 03:51:47

0001 /*
0002     SPDX-FileCopyrightText: 2014-2015 Vishesh Handa <vhanda@kde.org>
0003     SPDX-FileCopyrightText: 2014 Denis Steckelmacher <steckdenis@yahoo.fr>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-or-later
0006 */
0007 
0008 #include "advancedqueryparser.h"
0009 
0010 #include <QStringList>
0011 #include <QStack>
0012 #include <QDate>
0013 
0014 using namespace Baloo;
0015 
0016 AdvancedQueryParser::AdvancedQueryParser()
0017 {
0018 }
0019 
0020 static QStringList lex(const QString& text)
0021 {
0022     QStringList tokenList;
0023     QString token;
0024     bool inQuotes = false;
0025 
0026     for (int i = 0, end = text.size(); i != end; ++i) {
0027         QChar c = text.at(i);
0028 
0029         if (c == QLatin1Char('"')) {
0030             // Quotes start or end string literals
0031             if (inQuotes) {
0032                 tokenList.append(token);
0033                 token.clear();
0034             }
0035             inQuotes = !inQuotes;
0036         } else if (inQuotes) {
0037             // Don't do any processing in strings
0038             token.append(c);
0039         } else if (c.isSpace()) {
0040             // Spaces end tokens
0041             if (!token.isEmpty()) {
0042                 tokenList.append(token);
0043                 token.clear();
0044             }
0045         } else if (c == QLatin1Char('(') || c == QLatin1Char(')')) {
0046             // Parentheses end tokens, and are tokens by themselves
0047             if (!token.isEmpty()) {
0048                 tokenList.append(token);
0049                 token.clear();
0050             }
0051             tokenList.append(c);
0052         } else if (c == QLatin1Char('>') || c == QLatin1Char('<') || c == QLatin1Char(':') || c == QLatin1Char('=')) {
0053             // Operators end tokens
0054             if (!token.isEmpty()) {
0055                 tokenList.append(token);
0056                 token.clear();
0057             }
0058             // accept '=' after any of the above
0059             if (((i + 1) < end) && (text.at(i + 1) == QLatin1Char('='))) {
0060                 tokenList.append(text.mid(i, 2));
0061                 i++;
0062             } else {
0063                 tokenList.append(c);
0064             }
0065         } else {
0066             // Simply extend the current token
0067             token.append(c);
0068         }
0069     }
0070 
0071     if (!token.isEmpty()) {
0072         tokenList.append(token);
0073     }
0074 
0075     return tokenList;
0076 }
0077 
0078 static void addTermToStack(QStack<Term>& stack, const Term& termInConstruction, Term::Operation op)
0079 {
0080     Term &tos = stack.top();
0081 
0082     tos = Term(tos, op, termInConstruction);
0083 }
0084 
0085 Term AdvancedQueryParser::parse(const QString& text)
0086 {
0087     // The parser does not do any look-ahead but has to store some state
0088     QStack<Term> stack;
0089     QStack<Term::Operation> ops;
0090     Term termInConstruction;
0091     bool valueExpected = false;
0092 
0093     stack.push(Term());
0094     ops.push(Term::And);
0095 
0096     // Lex the input string
0097     QStringList tokens = lex(text);
0098     for (const QString &token : tokens) {
0099         // If a key and an operator have been parsed, now is time for a value
0100         if (valueExpected) {
0101             // When the parser encounters a literal, it puts it in the value of
0102             // termInConstruction so that "foo bar baz" is parsed as expected.
0103             auto property = termInConstruction.value().toString();
0104             if (property.isEmpty()) {
0105                 qDebug() << "Binary operator without first argument encountered:" << text;
0106                 return Term();
0107             }
0108             termInConstruction.setProperty(property);
0109 
0110             termInConstruction.setValue(token);
0111             valueExpected = false;
0112             continue;
0113         }
0114 
0115         // Handle the logic operators
0116         if (token == QLatin1String("AND")) {
0117             if (!termInConstruction.isEmpty()) {
0118                 addTermToStack(stack, termInConstruction, ops.top());
0119                 termInConstruction = Term();
0120             }
0121             ops.top() = Term::And;
0122             continue;
0123         } else if (token == QLatin1String("OR")) {
0124             if (!termInConstruction.isEmpty()) {
0125                 addTermToStack(stack, termInConstruction, ops.top());
0126                 termInConstruction = Term();
0127             }
0128             ops.top() = Term::Or;
0129             continue;
0130         }
0131 
0132         // Handle the different comparators (and braces)
0133         Term::Comparator comparator = Term::Auto;
0134 
0135         switch (token.isEmpty() ? '\0' : token.at(0).toLatin1()) {
0136             case ':':
0137                 comparator = Term::Contains;
0138                 break;
0139             case '=':
0140                 comparator = Term::Equal;
0141                 break;
0142             case '<': {
0143                 if (token.size() == 1) {
0144                     comparator = Term::Less;
0145                 } else if (token[1] == QLatin1Char('=')) {
0146                     comparator = Term::LessEqual;
0147                 }
0148                 break;
0149             }
0150             case '>': {
0151                 if (token.size() == 1) {
0152                     comparator = Term::Greater;
0153                 } else if (token[1] == QLatin1Char('=')) {
0154                     comparator = Term::GreaterEqual;
0155                 }
0156                 break;
0157             }
0158             case '(':
0159                 if (!termInConstruction.isEmpty()) {
0160                     addTermToStack(stack, termInConstruction, ops.top());
0161                     ops.top() = Term::And;
0162                 }
0163 
0164                 stack.push(Term());
0165                 ops.push(Term::And);
0166                 termInConstruction = Term();
0167 
0168                 continue;
0169             case ')':
0170                 // Prevent a stack underflow if the user writes "a b ))))"
0171                 if (stack.size() > 1) {
0172                     // Don't forget the term just before the closing brace
0173                     if (termInConstruction.value().isValid()) {
0174                         addTermToStack(stack, termInConstruction, ops.top());
0175                     }
0176 
0177                     // stack.pop() is the term that has just been closed. Append
0178                     // it to the term just above it.
0179                     ops.pop();
0180                     addTermToStack(stack, stack.pop(), ops.top());
0181                     ops.top() = Term::And;
0182                     termInConstruction = Term();
0183                 }
0184 
0185                 continue;
0186             default:
0187                 break;
0188         }
0189 
0190         if (comparator != Term::Auto) {
0191             // Set the comparator of the term in construction and expect a value
0192             termInConstruction.setComparator(comparator);
0193             valueExpected = true;
0194         } else {
0195             // A new term will be started, so termInConstruction has to be appended
0196             // to the top-level subterm list.
0197             if (!termInConstruction.isEmpty()) {
0198                 addTermToStack(stack, termInConstruction, ops.top());
0199                 ops.top() = Term::And;
0200             }
0201 
0202             termInConstruction = Term(QString(), token);
0203         }
0204     }
0205 
0206     if (valueExpected) {
0207         termInConstruction.setProperty(termInConstruction.value().toString());
0208         termInConstruction.setValue(QString());
0209         termInConstruction.setComparator(Term::Contains);
0210     }
0211 
0212     if (termInConstruction.value().isValid()) {
0213         addTermToStack(stack, termInConstruction, ops.top());
0214     }
0215 
0216     // Process unclosed parentheses
0217     ops.pop();
0218     while (stack.size() > 1) {
0219     // stack.pop() is the term that has to be closed. Append
0220     // it to the term just above it.
0221     addTermToStack(stack, stack.pop(), ops.top());
0222     }
0223 
0224     return stack.top();
0225 }