source: trunk/SL/QUERY/query_expr.cxx

Last change on this file was 16799, checked in by westram, 7 years ago
  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 12.2 KB
Line 
1// ============================================================= //
2//                                                               //
3//   File      : query_expr.cxx                                  //
4//   Purpose   : gui independent query functionality             //
5//                                                               //
6//   Coded by Ralf Westram (coder@reallysoft.de) in April 2017   //
7//   http://www.arb-home.de/                                     //
8//                                                               //
9// ============================================================= //
10
11#include "query_expr.h"
12#include <arb_global_defs.h>
13
14using namespace std;
15
16QueryExpr::QueryExpr(query_operator aqo, QueryKeyPtr key, bool not_equal, const char *expression) :
17    op(aqo),
18    qkey(key),
19    Not(not_equal),
20    expr(strdup(expression)),
21    type(AQT_INVALID),
22    error(NULp),
23    lastACIresult(NULp),
24    next(NULp)
25{
26    qe_assert(op == OR || op == AND);
27    detect_query_type();
28}
29
30QueryExpr *QueryExpr::remove_tail() {
31    QueryExpr *tail = NULp;
32    if (next) {
33        QueryExpr *body_last = this;
34        while (body_last->next && body_last->next->next) {
35            body_last = body_last->next;
36        }
37        qe_assert(body_last->next);
38        qe_assert(!body_last->next->next);
39
40        tail            = body_last->next;
41        body_last->next = NULp;
42    }
43    return tail;
44}
45
46void QueryExpr::append(QueryExpr*& tail) {
47    qe_assert(this != tail);
48
49    if (next) next->append(tail);
50    else {
51        next = tail;
52        tail = NULp;
53    }
54}
55
56void QueryExpr::negate() {
57    if (next) {
58        QueryExpr *tail = remove_tail();
59
60        negate();
61        tail->negate();
62
63        switch (tail->op) {
64            case AND: tail->op = OR;  break;
65            case OR:  tail->op = AND; break;
66            default: qe_assert(0); break;
67        }
68
69        append(tail);
70    }
71    else {
72        Not = !Not;
73        qkey->negate();
74    }
75}
76
77GB_ERROR QueryExpr::getError(int count) const {
78    GB_ERROR err = error;
79    error        = NULp;
80
81    if (err) {
82        err = GBS_global_string("%s (in %i. active query)", err, count+1);
83    }
84
85    if (next) {
86        if (err) {
87            char *dup = strdup(err);
88
89            err = next->getError(count+1);
90            if (err) err = GBS_global_string("%s\n%s", dup, err);
91            else err = GBS_static_string(dup);
92            free(dup);
93        }
94        else {
95            err = next->getError(count+1);
96        }
97    }
98
99    return err;
100}
101
102
103inline bool containsWildcards(const char *str) { return strpbrk(str, "*?"); }
104inline bool containsWildcards(const string& str) { return str.find_first_of("*?") != string::npos; }
105
106void QueryExpr::detect_query_type() {
107    char    first = expr[0];
108    string& str   = xquery.str;
109    str           = expr;
110
111    type = AQT_INVALID;
112
113    if (!first)            type = AQT_EMPTY;
114    else if (first == '/') {
115        GB_CASE     case_flag;
116        GB_ERROR    err       = NULp;
117        const char *unwrapped = GBS_unwrap_regexpr(expr, &case_flag, &err);
118        if (unwrapped) {
119            xquery.regexp = GBS_compile_regexpr(unwrapped, case_flag, &err);
120            if (xquery.regexp) type = AQT_REGEXPR;
121        }
122        if (err) freedup(error, err);
123    }
124    else if (first == '|') type = AQT_ACI;
125    else if (first == '<' || first == '>') {
126        const char *rest = expr+1;
127        const char *end;
128        float       f    = strtof(rest, const_cast<char**>(&end));
129
130        if (end != rest) { // did convert part or all of rest to float
131            if (end[0] == 0) { // all of rest has been converted
132                type          = expr[0] == '<' ? AQT_LOWER : AQT_GREATER;
133                xquery.number = f;
134            }
135            else {
136                freeset(error, GBS_global_string_copy("Could not convert '%s' to number (unexpected content '%s')", rest, end));
137            }
138        }
139        // otherwise handle as non-special search string
140    }
141
142    if (type == AQT_INVALID && !error) {            // no type detected above
143        if (containsWildcards(expr)) {
144            size_t qlen = strlen(expr);
145            char   last = expr[qlen-1];
146
147            if (first == '*') {
148                if (last == '*') {
149                    str  = string(str, 1, str.length()-2); // cut off first and last
150                    type = str.length() ? AQT_OCCURS : AQT_NON_EMPTY;
151                }
152                else {
153                    str  = string(str, 1);          // cut of first
154                    type = AQT_ENDS_WITH;
155                }
156            }
157            else {
158                if (last == '*') {
159                    str  = string(str, 0, str.length()-1); // cut of last
160                    type = AQT_STARTS_WITH;
161                }
162                else type = AQT_WILDCARD;
163            }
164
165            if (type != AQT_WILDCARD && containsWildcards(str)) { // still contains wildcards -> fallback
166                str  = expr;
167                type = AQT_WILDCARD;
168            }
169        }
170        else type = AQT_EXACT_MATCH;
171    }
172
173    // qe_assert(type != AQT_INVALID || error);
174    qe_assert(correlated(error, type == AQT_INVALID));
175}
176
177bool QueryExpr::first_matches(const QueryTarget& target, char*& matched_data) const {
178    bool      hit           = false;
179    GB_ERROR  retrieveError = NULp;
180    char     *data          = qkey->get_target_data(target, retrieveError);
181
182    qe_assert(contradicted(data, retrieveError));
183    if (!data && !error) setError(retrieveError);
184
185    if (error) {
186        hit = false; // as soon as an error has been set, the query will no longer match
187    }
188    else switch (type) {
189        case AQT_EMPTY: {
190            hit = (data[0] == 0);
191            break;
192        }
193        case AQT_NON_EMPTY: {
194            hit = (data[0] != 0);
195            break;
196        }
197        case AQT_EXACT_MATCH: {                     // exact match (but ignoring case)
198            hit = strcasecmp(data, expr) == 0;
199            break;
200        }
201        case AQT_OCCURS: {                          // query expression occurs in data (equiv to '*expr*')
202            hit = GBS_find_string(data, xquery.str.c_str(), 1);
203            break;
204        }
205        case AQT_STARTS_WITH: {                     // data starts with query expression (equiv to 'expr*')
206            hit = strncasecmp(data, xquery.str.c_str(), xquery.str.length()) == 0;
207            break;
208        }
209        case AQT_ENDS_WITH: {                       // data ends with query expression (equiv to '*expr')
210            int dlen = strlen(data);
211            hit = strcasecmp(data+dlen-xquery.str.length(), xquery.str.c_str()) == 0;
212            break;
213        }
214        case AQT_WILDCARD: {                        // expr contains wildcards (use GBS_string_matches for compare)
215            hit = GBS_string_matches(data, expr, GB_IGNORE_CASE);
216            break;
217        }
218        case AQT_GREATER:                           // data is greater than query
219        case AQT_LOWER: {                           // data is lower than query
220            const char *start = data;
221            while (start[0] == ' ') ++start;
222
223            const char *end;
224            float       f = strtof(start, const_cast<char**>(&end));
225
226            if (end == start) { // nothing was converted
227                hit = false;
228            }
229            else {
230                bool is_numeric = (end[0] == 0);
231
232                if (!is_numeric) {
233                    while (end[0] == ' ') ++end;
234                    is_numeric = (end[0] == 0);
235                }
236                if (is_numeric) {
237                    hit = (type == AQT_GREATER)
238                        ? f > xquery.number
239                        : f < xquery.number;
240                }
241                else {
242                    hit = false;
243                }
244            }
245            break;
246        }
247        case AQT_REGEXPR: {                         // expr is a regexpr ('/.../')
248            hit = GBS_regmatch_compiled(data, xquery.regexp, NULp);
249            break;
250        }
251        case AQT_ACI: {                             // expr is a ACI ('|...'); result = "0" -> no hit; otherwise hit
252            GBL_call_env callEnv(target.get_ACI_item(), target.get_env());
253
254            char *aci_result = GB_command_interpreter_in_env(data, expr, callEnv);
255            if (!aci_result) {
256                freedup(error, GB_await_error());
257                hit   = false;
258            }
259            else {
260                hit = strcmp(aci_result, "0") != 0;
261            }
262            freeset(lastACIresult, aci_result);
263            break;
264        }
265        case AQT_INVALID: {                     // invalid
266            qe_assert(0);
267            freedup(error, "Invalid search expression");
268            hit   = false;
269            break;
270        }
271    }
272
273    matched_data = data; // provide data used for query (transfers ownership to caller)
274    return Not ? !hit : hit;
275}
276
277bool QueryExpr::matches(const QueryTarget& target, std::string& hit_reason) const {
278    bool hit    = false;
279    int  qindex = 0;
280
281    qe_assert(hit_reason.empty());
282
283    for (const QueryExpr *subexpr = this; // iterate over all single queries
284         subexpr;
285         ++qindex, subexpr = subexpr ? subexpr->next : NULp)
286    {
287        if ((subexpr->op == OR) == hit) {
288            continue; // skip query Q for '1 OR Q' and for '0 AND Q' (result can't change)
289        }
290
291        string this_hit_reason;
292
293        const QueryKey& query_key = subexpr->get_key();
294        query_key.reset();
295
296        query_key_type key_type = subexpr->get_key_type();
297        bool           this_hit = (key_type == QKEY_ALL || key_type == QKEY_ALL_REC);
298
299        bool do_match = true;
300        while (do_match) { // loop over multi-target-keys
301            char *matched_data = NULp;
302            bool  matched      = subexpr->first_matches(target, matched_data); // includes not-op
303            bool  sub_decided  = // done?
304                key_type == QKEY_EXPLICIT ||
305                (matched == (key_type == QKEY_ANY || key_type == QKEY_ANY_REC));
306
307            if (sub_decided) {
308                qe_assert(implicated(key_type != QKEY_EXPLICIT, contradicted(this_hit, matched)));
309                this_hit = matched;
310
311                const char *reason_key = query_key.get_name();
312
313                if (strlen(matched_data)>MAX_SHOWN_DATA_SIZE) {
314                    size_t shortened_len = GBS_shorten_repeated_data(matched_data);
315                    if (shortened_len>MAX_SHOWN_DATA_SIZE) {
316                        strcpy(matched_data+MAX_SHOWN_DATA_SIZE-5, "[...]");
317                    }
318                }
319                this_hit_reason                = string(reason_key)+"="+matched_data;
320                const char *ACIresult          = subexpr->get_last_ACI_result();
321                if (ACIresult) this_hit_reason = string("[ACI=")+ACIresult+"] "+this_hit_reason;
322
323                do_match  = false;
324            }
325            else {
326                do_match = do_match && query_key.next();
327            }
328            free(matched_data);
329        }
330
331        if (this_hit && (key_type == QKEY_ALL || key_type == QKEY_ALL_REC)) {
332            qe_assert(this_hit_reason.empty());
333            this_hit_reason = subexpr->shallMatch() ? "<matched all>" : "<matched none>";
334        }
335
336
337        if (this_hit) {
338            qe_assert(!this_hit_reason.empty()); // if we got a hit, we also need a reason
339            const char *prefix = GBS_global_string("%c%c", '1'+qindex, subexpr->shallMatch() ? ' ' : '!');
340            this_hit_reason    = string(prefix)+this_hit_reason;
341        }
342
343        // calculate result
344        // (Note: the operator of the 1st query is always OR)
345        switch (subexpr->op) {
346            case AND: {
347                qe_assert(hit); // otherwise there was no need to run this sub-query
348                hit        = this_hit;
349                hit_reason = hit_reason.empty() ? this_hit_reason : hit_reason+" & "+this_hit_reason;
350                break;
351            }
352            case OR: {
353                qe_assert(!hit); // otherwise there was no need to run this sub-query
354                hit        = this_hit;
355                hit_reason = this_hit_reason;
356                break;
357            }
358            default:
359                qe_assert(0);
360                break;
361        }
362        qe_assert(!hit || !hit_reason.empty()); // if we got a hit, we also need a reason
363    }
364
365    return hit;
366}
367
Note: See TracBrowser for help on using the repository browser.