]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
Merge branch 'master' of git+ssh://yeti.esilibrary.com/home/evergreen/evergreen-equin...
[Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Storage / QueryParser.pm
1 use strict;
2 use warnings;
3
4 package QueryParser;
5 use OpenSRF::Utils::JSON;
6 our %parser_config = (
7     QueryParser => {
8         filters => [],
9         modifiers => [],
10         operators => { 
11             'and' => '&&',
12             'or' => '||',
13             group_start => '(',
14             group_end => ')',
15             required => '+',
16             modifier => '#'
17         }
18     }
19 );
20
21 sub facet_class_count {
22     my $self = shift;
23     return @{$self->facet_classes};
24 }
25
26 sub search_class_count {
27     my $self = shift;
28     return @{$self->search_classes};
29 }
30
31 sub filter_count {
32     my $self = shift;
33     return @{$self->filters};
34 }
35
36 sub modifier_count {
37     my $self = shift;
38     return @{$self->modifiers};
39 }
40
41 sub custom_data {
42     my $class = shift;
43     $class = ref($class) || $class;
44
45     $parser_config{$class}{custom_data} ||= {};
46     return $parser_config{$class}{custom_data};
47 }
48
49 sub operators {
50     my $class = shift;
51     $class = ref($class) || $class;
52
53     $parser_config{$class}{operators} ||= {};
54     return $parser_config{$class}{operators};
55 }
56
57 sub filters {
58     my $class = shift;
59     $class = ref($class) || $class;
60
61     $parser_config{$class}{filters} ||= [];
62     return $parser_config{$class}{filters};
63 }
64
65 sub modifiers {
66     my $class = shift;
67     $class = ref($class) || $class;
68
69     $parser_config{$class}{modifiers} ||= [];
70     return $parser_config{$class}{modifiers};
71 }
72
73 sub new {
74     my $class = shift;
75     $class = ref($class) || $class;
76
77     my %opts = @_;
78
79     my $self = bless {} => $class;
80
81     for my $o (keys %{QueryParser->operators}) {
82         $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
83     }
84
85     for my $opt ( keys %opts) {
86         $self->$opt( $opts{$opt} ) if ($self->can($opt));
87     }
88
89     return $self;
90 }
91
92 sub new_plan {
93     my $self = shift;
94     my $pkg = ref($self) || $self;
95     return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
96 }
97
98 sub add_search_filter {
99     my $pkg = shift;
100     $pkg = ref($pkg) || $pkg;
101     my $filter = shift;
102
103     return $filter if (grep { $_ eq $filter } @{$pkg->filters});
104     push @{$pkg->filters}, $filter;
105     return $filter;
106 }
107
108 sub add_search_modifier {
109     my $pkg = shift;
110     $pkg = ref($pkg) || $pkg;
111     my $modifier = shift;
112
113     return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
114     push @{$pkg->modifiers}, $modifier;
115     return $modifier;
116 }
117
118 sub add_facet_class {
119     my $pkg = shift;
120     $pkg = ref($pkg) || $pkg;
121     my $class = shift;
122
123     return $class if (grep { $_ eq $class } @{$pkg->facet_classes});
124
125     push @{$pkg->facet_classes}, $class;
126     $pkg->facet_fields->{$class} = [];
127
128     return $class;
129 }
130
131 sub add_search_class {
132     my $pkg = shift;
133     $pkg = ref($pkg) || $pkg;
134     my $class = shift;
135
136     return $class if (grep { $_ eq $class } @{$pkg->search_classes});
137
138     push @{$pkg->search_classes}, $class;
139     $pkg->search_fields->{$class} = [];
140     $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
141
142     return $class;
143 }
144
145 sub operator {
146     my $class = shift;
147     $class = ref($class) || $class;
148     my $opname = shift;
149     my $op = shift;
150
151     return undef unless ($opname);
152
153     $parser_config{$class}{operators} ||= {};
154     $parser_config{$class}{operators}{$opname} = $op if ($op);
155
156     return $parser_config{$class}{operators}{$opname};
157 }
158
159 sub facet_classes {
160     my $class = shift;
161     $class = ref($class) || $class;
162     my $classes = shift;
163
164     $parser_config{$class}{facet_classes} ||= [];
165     $parser_config{$class}{facet_classes} = $classes if (ref($classes) && @$classes);
166     return $parser_config{$class}{facet_classes};
167 }
168
169 sub search_classes {
170     my $class = shift;
171     $class = ref($class) || $class;
172     my $classes = shift;
173
174     $parser_config{$class}{classes} ||= [];
175     $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
176     return $parser_config{$class}{classes};
177 }
178
179 sub add_query_normalizer {
180     my $pkg = shift;
181     $pkg = ref($pkg) || $pkg;
182     my $class = shift;
183     my $field = shift;
184     my $func = shift;
185     my $params = shift || [];
186
187     # do not add if function AND params are identical to existing member
188     return $func if (grep {
189         $_->{function} eq $func and 
190         OpenSRF::Utils::JSON->perl2JSON($_->{params}) eq OpenSRF::Utils::JSON->perl2JSON($params)
191     } @{$pkg->query_normalizers->{$class}->{$field}});
192
193     push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
194
195     return $func;
196 }
197
198 sub query_normalizers {
199     my $pkg = shift;
200     $pkg = ref($pkg) || $pkg;
201
202     my $class = shift;
203     my $field = shift;
204
205     $parser_config{$pkg}{normalizers} ||= {};
206     if ($class) {
207         if ($field) {
208             $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
209             return $parser_config{$pkg}{normalizers}{$class}{$field};
210         } else {
211             return $parser_config{$pkg}{normalizers}{$class};
212         }
213     }
214
215     return $parser_config{$pkg}{normalizers};
216 }
217
218 sub add_filter_normalizer {
219     my $pkg = shift;
220     $pkg = ref($pkg) || $pkg;
221     my $filter = shift;
222     my $func = shift;
223     my $params = shift || [];
224
225     return $func if (grep { $_ eq $func } @{$pkg->filter_normalizers->{$filter}});
226
227     push(@{$pkg->filter_normalizers->{$filter}}, { function => $func, params => $params });
228
229     return $func;
230 }
231
232 sub filter_normalizers {
233     my $pkg = shift;
234     $pkg = ref($pkg) || $pkg;
235
236     my $filter = shift;
237
238     $parser_config{$pkg}{filter_normalizers} ||= {};
239     if ($filter) {
240         $parser_config{$pkg}{filter_normalizers}{$filter} ||= [];
241         return $parser_config{$pkg}{filter_normalizers}{$filter};
242     }
243
244     return $parser_config{$pkg}{filter_normalizers};
245 }
246
247 sub default_search_class {
248     my $pkg = shift;
249     $pkg = ref($pkg) || $pkg;
250     my $class = shift;
251     $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
252
253     return $QueryParser::parser_config{$pkg}{default_class};
254 }
255
256 sub remove_facet_class {
257     my $pkg = shift;
258     $pkg = ref($pkg) || $pkg;
259     my $class = shift;
260
261     return $class if (!grep { $_ eq $class } @{$pkg->facet_classes});
262
263     $pkg->facet_classes( [ grep { $_ ne $class } @{$pkg->facet_classes} ] );
264     delete $QueryParser::parser_config{$pkg}{facet_fields}{$class};
265
266     return $class;
267 }
268
269 sub remove_search_class {
270     my $pkg = shift;
271     $pkg = ref($pkg) || $pkg;
272     my $class = shift;
273
274     return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
275
276     $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
277     delete $QueryParser::parser_config{$pkg}{fields}{$class};
278
279     return $class;
280 }
281
282 sub add_facet_field {
283     my $pkg = shift;
284     $pkg = ref($pkg) || $pkg;
285     my $class = shift;
286     my $field = shift;
287
288     $pkg->add_facet_class( $class );
289
290     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
291
292     push @{$pkg->facet_fields->{$class}}, $field;
293
294     return { $class => $field };
295 }
296
297 sub facet_fields {
298     my $class = shift;
299     $class = ref($class) || $class;
300
301     $parser_config{$class}{facet_fields} ||= {};
302     return $parser_config{$class}{facet_fields};
303 }
304
305 sub add_search_field {
306     my $pkg = shift;
307     $pkg = ref($pkg) || $pkg;
308     my $class = shift;
309     my $field = shift;
310
311     $pkg->add_search_class( $class );
312
313     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
314
315     push @{$pkg->search_fields->{$class}}, $field;
316
317     return { $class => $field };
318 }
319
320 sub search_fields {
321     my $class = shift;
322     $class = ref($class) || $class;
323
324     $parser_config{$class}{fields} ||= {};
325     return $parser_config{$class}{fields};
326 }
327
328 sub add_search_class_alias {
329     my $pkg = shift;
330     $pkg = ref($pkg) || $pkg;
331     my $class = shift;
332     my $alias = shift;
333
334     $pkg->add_search_class( $class );
335
336     return { $class => $alias }  if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
337
338     push @{$pkg->search_class_aliases->{$class}}, $alias;
339
340     return { $class => $alias };
341 }
342
343 sub search_class_aliases {
344     my $class = shift;
345     $class = ref($class) || $class;
346
347     $parser_config{$class}{class_map} ||= {};
348     return $parser_config{$class}{class_map};
349 }
350
351 sub add_search_field_alias {
352     my $pkg = shift;
353     $pkg = ref($pkg) || $pkg;
354     my $class = shift;
355     my $field = shift;
356     my $alias = shift;
357
358     return { $class => { $field => $alias } }  if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
359
360     push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
361
362     return { $class => { $field => $alias } };
363 }
364
365 sub search_field_aliases {
366     my $class = shift;
367     $class = ref($class) || $class;
368
369     $parser_config{$class}{field_alias_map} ||= {};
370     return $parser_config{$class}{field_alias_map};
371 }
372
373 sub remove_facet_field {
374     my $pkg = shift;
375     $pkg = ref($pkg) || $pkg;
376     my $class = shift;
377     my $field = shift;
378
379     return { $class => $field }  if (!$pkg->facet_fields->{$class} || !grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
380
381     $pkg->facet_fields->{$class} = [ grep { $_ ne $field } @{$pkg->facet_fields->{$class}} ];
382
383     return { $class => $field };
384 }
385
386 sub remove_search_field {
387     my $pkg = shift;
388     $pkg = ref($pkg) || $pkg;
389     my $class = shift;
390     my $field = shift;
391
392     return { $class => $field }  if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
393
394     $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
395
396     return { $class => $field };
397 }
398
399 sub remove_search_field_alias {
400     my $pkg = shift;
401     $pkg = ref($pkg) || $pkg;
402     my $class = shift;
403     my $field = shift;
404     my $alias = shift;
405
406     return { $class => { $field => $alias } }  if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
407
408     $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
409
410     return { $class => { $field => $alias } };
411 }
412
413 sub remove_search_class_alias {
414     my $pkg = shift;
415     $pkg = ref($pkg) || $pkg;
416     my $class = shift;
417     my $alias = shift;
418
419     return { $class => $alias }  if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
420
421     $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
422
423     return { $class => $alias };
424 }
425
426 sub debug {
427     my $self = shift;
428     my $q = shift;
429     $self->{_debug} = $q if (defined $q);
430     return $self->{_debug};
431 }
432
433 sub query {
434     my $self = shift;
435     my $q = shift;
436     $self->{_query} = $q if (defined $q);
437     return $self->{_query};
438 }
439
440 sub parse_tree {
441     my $self = shift;
442     my $q = shift;
443     $self->{_parse_tree} = $q if (defined $q);
444     return $self->{_parse_tree};
445 }
446
447 sub parse {
448     my $self = shift;
449     my $pkg = ref($self) || $self;
450     warn " ** parse package is $pkg\n" if $self->debug;
451     $self->parse_tree(
452         $self->decompose(
453             $self->query( shift() )
454         )
455     );
456
457     return $self;
458 }
459
460 sub decompose {
461     my $self = shift;
462     my $pkg = ref($self) || $self;
463
464     warn " ** decompose package is $pkg\n" if $self->debug;
465
466     $_ = shift;
467     my $current_class = shift || $self->default_search_class;
468
469     my $recursing = shift || 0;
470
471     # Build the search class+field uber-regexp
472     my $search_class_re = '^\s*(';
473     my $first_class = 1;
474
475     my %seen_classes;
476     for my $class ( keys %{$pkg->search_fields} ) {
477
478         for my $field ( @{$pkg->search_fields->{$class}} ) {
479
480             for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
481                 $alias = qr/$alias/;
482                 s/(^|\s+)$alias[:=]/$1$class\|$field:/g;
483             }
484         }
485
486         $search_class_re .= '|' unless ($first_class);
487         $first_class = 0;
488         $search_class_re .= $class . '(?:\|\w+)*';
489         $seen_classes{$class} = 1;
490     }
491
492     for my $class ( keys %{$pkg->search_class_aliases} ) {
493
494         for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
495             $alias = qr/$alias/;
496             s/(^|[^|])\b$alias\|/$1$class\|/g;
497             s/(^|[^|])\b$alias[:=]/$1$class:/g;
498         }
499
500         $search_class_re .= '|' unless ($first_class);
501         $first_class = 0;
502
503         $search_class_re .= $class . '(?:\|\w+)*' if (!$seen_classes{$class});
504         $seen_classes{$class} = 1;
505     }
506     $search_class_re .= '):';
507
508     warn " ** Search class RE: $search_class_re\n" if $self->debug;
509
510     my $required_re = $pkg->operator('required');
511     $required_re = qr/^\s*\Q$required_re\E/;
512     my $and_re = $pkg->operator('and');
513     $and_re = qr/^\s*\Q$and_re\E/;
514
515     my $or_re = $pkg->operator('or');
516     $or_re = qr/^\s*\Q$or_re\E/;
517
518     my $group_start_re = $pkg->operator('group_start');
519     $group_start_re = qr/^\s*\Q$group_start_re\E/;
520
521     my $group_end = $pkg->operator('group_end');
522     my $group_end_re = qr/^\s*\Q$group_end\E/;
523
524     my $modifier_tag_re = $pkg->operator('modifier');
525     $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
526
527
528     # Build the filter and modifier uber-regexps
529     my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
530     warn " Facet RE: $facet_re\n" if $self->debug;
531
532     my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
533     my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
534
535     my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
536     my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
537
538     my $struct = $self->new_plan( level => $recursing );
539     my $remainder = '';
540
541     my $last_type = '';
542     while (!$remainder) {
543         if (/^\s*$/) { # end of an explicit group
544             last;
545         } elsif (/$group_end_re/) { # end of an explicit group
546             warn "Encountered explicit group end\n" if $self->debug;
547
548             $_ = $';
549             $remainder = $struct->top_plan ? '' : $';
550
551             $last_type = '';
552         } elsif ($self->filter_count && /$filter_re/) { # found a filter
553             warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
554
555             my $negate = ($1 eq '-') ? 1 : 0;
556             $_ = $';
557             $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
558
559             $last_type = '';
560         } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
561             warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
562
563             my $negate = ($1 eq '-') ? 1 : 0;
564             $_ = $';
565             $struct->new_filter( $2 => [ split '[,]+', $3 ], $negate );
566
567             $last_type = '';
568         } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
569             warn "Encountered search modifier: $1\n" if $self->debug;
570
571             $_ = $';
572             if (!$struct->top_plan) {
573                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
574             } else {
575                 $struct->new_modifier($1);
576             }
577
578             $last_type = '';
579         } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
580             warn "Encountered search modifier: $1\n" if $self->debug;
581
582             my $mod = $1;
583
584             $_ = $';
585             if (!$struct->top_plan) {
586                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
587             } elsif ($2 =~ /^[ty1]/i) {
588                 $struct->new_modifier($mod);
589             }
590
591             $last_type = '';
592         } elsif (/$group_start_re/) { # start of an explicit group
593             warn "Encountered explicit group start\n" if $self->debug;
594
595             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
596             $struct->add_node( $substruct ) if ($substruct);
597             $_ = $subremainder;
598
599             $last_type = '';
600         } elsif (/$and_re/) { # ANDed expression
601             $_ = $';
602             next if ($last_type eq 'AND');
603             next if ($last_type eq 'OR');
604             warn "Encountered AND\n" if $self->debug;
605
606             $struct->joiner( '&' );
607
608             $last_type = 'AND';
609         } elsif (/$or_re/) { # ORed expression
610             $_ = $';
611             next if ($last_type eq 'AND');
612             next if ($last_type eq 'OR');
613             warn "Encountered OR\n" if $self->debug;
614
615             $struct->joiner( '|' );
616
617             $last_type = 'OR';
618         } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
619             warn "Encountered facet: $1$2 => $3\n" if $self->debug;
620
621             my $negate = ($1 eq '-') ? 1 : 0;
622             my $facet = $2;
623             my $facet_value = [ split '\s*#\s*', $3 ];
624             $struct->new_facet( $facet => $facet_value, $negate );
625             $_ = $';
626
627             $last_type = '';
628         } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
629
630             if ($last_type eq 'CLASS') {
631                 $struct->remove_last_node( $current_class );
632                 warn "Encountered class change with no searches!\n" if $self->debug;
633             }
634
635             warn "Encountered class change: $1\n" if $self->debug;
636
637             $current_class = $1;
638             $struct->classed_node( $current_class );
639             $_ = $';
640
641             $last_type = 'CLASS';
642         } elsif (/^\s*"([^"]+)"/) { # phrase, always anded
643             warn "Encountered phrase: $1\n" if $self->debug;
644
645             $struct->joiner( '&' );
646             my $phrase = $1;
647
648             my $class_node = $struct->classed_node($current_class);
649             $class_node->add_phrase( $phrase );
650             $_ = $phrase . $';
651
652             $last_type = '';
653         } elsif (/$required_re([^\s)]+)/) { # phrase, always anded
654             warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
655
656             my $phrase = $1;
657
658             my $class_node = $struct->classed_node($current_class);
659             $class_node->add_phrase( $phrase );
660             $_ = $phrase . $';
661             $struct->joiner( '&' );
662
663             $last_type = '';
664         } elsif (/^\s*([^$group_end\s]+)/o) { # atom
665             warn "Encountered atom: $1\n" if $self->debug;
666             warn "Remainder: $'\n" if $self->debug;
667
668             my $atom = $1;
669             my $after = $';
670
671             $_ = $after;
672             $last_type = '';
673
674             my $negator = ($atom =~ s/^-//o) ? '!' : '';
675             my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
676
677             if (!grep { $atom eq $_ } ('&','|')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
678                 my $class_node = $struct->classed_node($current_class);
679                 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $negator, node => $class_node );
680                 $struct->joiner( '&' );
681             }
682         } 
683
684         last unless ($_);
685
686     }
687
688     $struct = undef if (scalar(@{$struct->query_nodes}) == 0 && !$struct->top_plan);
689
690     return $struct if !wantarray;
691     return ($struct, $remainder);
692 }
693
694 sub find_class_index {
695     my $class = shift;
696     my $query = shift;
697
698     my ($class_part, @field_parts) = split '\|', $class;
699     $class_part ||= $class;
700
701     for my $idx ( 0 .. scalar(@$query) - 1 ) {
702         next unless ref($$query[$idx]);
703         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
704     }
705
706     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
707     return -1;
708 }
709
710 sub core_limit {
711     my $self = shift;
712     my $l = shift;
713     $self->{core_limit} = $l if ($l);
714     return $self->{core_limit};
715 }
716
717 sub superpage {
718     my $self = shift;
719     my $l = shift;
720     $self->{superpage} = $l if ($l);
721     return $self->{superpage};
722 }
723
724 sub superpage_size {
725     my $self = shift;
726     my $l = shift;
727     $self->{superpage_size} = $l if ($l);
728     return $self->{superpage_size};
729 }
730
731
732 #-------------------------------
733 package QueryParser::query_plan;
734
735 sub QueryParser {
736     my $self = shift;
737     return undef unless ref($self);
738     return $self->{QueryParser};
739 }
740
741 sub new {
742     my $pkg = shift;
743     $pkg = ref($pkg) || $pkg;
744     my %args = (query => [], joiner => '&', @_);
745
746     return bless \%args => $pkg;
747 }
748
749 sub new_node {
750     my $self = shift;
751     my $pkg = ref($self) || $self;
752     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
753     $self->add_node( $node );
754     return $node;
755 }
756
757 sub new_facet {
758     my $self = shift;
759     my $pkg = ref($self) || $self;
760     my $name = shift;
761     my $args = shift;
762     my $negate = shift;
763
764     my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
765     $self->add_node( $node );
766
767     return $node;
768 }
769
770 sub new_filter {
771     my $self = shift;
772     my $pkg = ref($self) || $self;
773     my $name = shift;
774     my $args = shift;
775     my $negate = shift;
776
777     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
778     $self->add_filter( $node );
779
780     return $node;
781 }
782
783 sub find_filter {
784     my $self = shift;
785     my $needle = shift;;
786     return undef unless ($needle);
787     return grep { $_->name eq $needle } @{ $self->filters };
788 }
789
790 sub find_modifier {
791     my $self = shift;
792     my $needle = shift;;
793     return undef unless ($needle);
794     return grep { $_->name eq $needle } @{ $self->modifiers };
795 }
796
797 sub new_modifier {
798     my $self = shift;
799     my $pkg = ref($self) || $self;
800     my $name = shift;
801
802     my $node = do{$pkg.'::modifier'}->new( $name );
803     $self->add_modifier( $node );
804
805     return $node;
806 }
807
808 sub classed_node {
809     my $self = shift;
810     my $requested_class = shift;
811
812     my $node;
813     for my $n (@{$self->{query}}) {
814         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
815         if ($n->requested_class eq $requested_class) {
816             $node = $n;
817             last;
818         }
819     }
820
821     if (!$node) {
822         $node = $self->new_node;
823         $node->requested_class( $requested_class );
824     }
825
826     return $node;
827 }
828
829 sub remove_last_node {
830     my $self = shift;
831     my $requested_class = shift;
832
833     my $old = pop(@{$self->query_nodes});
834     pop(@{$self->query_nodes}) if (@{$self->query_nodes});
835
836     return $old;
837 }
838
839 sub query_nodes {
840     my $self = shift;
841     return $self->{query};
842 }
843
844 sub add_node {
845     my $self = shift;
846     my $node = shift;
847
848     $self->{query} ||= [];
849     push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
850     push(@{$self->{query}}, $node);
851
852     return $self;
853 }
854
855 sub top_plan {
856     my $self = shift;
857
858     return $self->{level} ? 0 : 1;
859 }
860
861 sub plan_level {
862     my $self = shift;
863     return $self->{level};
864 }
865
866 sub joiner {
867     my $self = shift;
868     my $joiner = shift;
869
870     $self->{joiner} = $joiner if ($joiner);
871     return $self->{joiner};
872 }
873
874 sub modifiers {
875     my $self = shift;
876     $self->{modifiers} ||= [];
877     return $self->{modifiers};
878 }
879
880 sub add_modifier {
881     my $self = shift;
882     my $modifier = shift;
883
884     $self->{modifiers} ||= [];
885     return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
886
887     push(@{$self->{modifiers}}, $modifier);
888
889     return $self;
890 }
891
892 sub facets {
893     my $self = shift;
894     $self->{facets} ||= [];
895     return $self->{facets};
896 }
897
898 sub add_facet {
899     my $self = shift;
900     my $facet = shift;
901
902     $self->{facets} ||= [];
903     return $self if (grep {$_->name eq $facet->name} @{$self->{facets}});
904
905     push(@{$self->{facets}}, $facet);
906
907     return $self;
908 }
909
910 sub filters {
911     my $self = shift;
912     $self->{filters} ||= [];
913     return $self->{filters};
914 }
915
916 sub add_filter {
917     my $self = shift;
918     my $filter = shift;
919
920     $self->{filters} ||= [];
921     return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
922
923     push(@{$self->{filters}}, $filter);
924
925     return $self;
926 }
927
928
929 #-------------------------------
930 package QueryParser::query_plan::node;
931
932 sub new {
933     my $pkg = shift;
934     $pkg = ref($pkg) || $pkg;
935     my %args = @_;
936
937     return bless \%args => $pkg;
938 }
939
940 sub new_atom {
941     my $self = shift;
942     my $pkg = ref($self) || $self;
943     return do{$pkg.'::atom'}->new( @_ );
944 }
945
946 sub requested_class { # also split into classname and fields
947     my $self = shift;
948     my $class = shift;
949
950     if ($class) {
951         my ($class_part, @field_parts) = split '\|', $class;
952         $class_part ||= $class;
953
954         $self->{requested_class} = $class;
955         $self->{classname} = $class_part;
956         $self->{fields} = \@field_parts;
957     }
958
959     return $self->{requested_class};
960 }
961
962 sub plan {
963     my $self = shift;
964     my $plan = shift;
965
966     $self->{plan} = $plan if ($plan);
967     return $self->{plan};
968 }
969
970 sub classname {
971     my $self = shift;
972     my $class = shift;
973
974     $self->{classname} = $class if ($class);
975     return $self->{classname};
976 }
977
978 sub fields {
979     my $self = shift;
980     my @fields = @_;
981
982     $self->{fields} ||= [];
983     $self->{fields} = \@fields if (@fields);
984     return $self->{fields};
985 }
986
987 sub phrases {
988     my $self = shift;
989     my @phrases = @_;
990
991     $self->{phrases} ||= [];
992     $self->{phrases} = \@phrases if (@phrases);
993     return $self->{phrases};
994 }
995
996 sub add_phrase {
997     my $self = shift;
998     my $phrase = shift;
999
1000     push(@{$self->phrases}, $phrase);
1001
1002     return $self;
1003 }
1004
1005 sub query_atoms {
1006     my $self = shift;
1007     my @query_atoms = @_;
1008
1009     $self->{query_atoms} ||= [];
1010     $self->{query_atoms} = \@query_atoms if (@query_atoms);
1011     return $self->{query_atoms};
1012 }
1013
1014 sub add_fts_atom {
1015     my $self = shift;
1016     my $atom = shift;
1017
1018     if (!ref($atom)) {
1019         my $content = $atom;
1020         my @parts = @_;
1021
1022         $atom = $self->new_atom( content => $content, @parts );
1023     }
1024
1025     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1026     push(@{$self->query_atoms}, $atom);
1027
1028     return $self;
1029 }
1030
1031 #-------------------------------
1032 package QueryParser::query_plan::node::atom;
1033
1034 sub new {
1035     my $pkg = shift;
1036     $pkg = ref($pkg) || $pkg;
1037     my %args = @_;
1038
1039     return bless \%args => $pkg;
1040 }
1041
1042 sub node {
1043     my $self = shift;
1044     return undef unless (ref $self);
1045     return $self->{node};
1046 }
1047
1048 sub content {
1049     my $self = shift;
1050     return undef unless (ref $self);
1051     return $self->{content};
1052 }
1053
1054 sub prefix {
1055     my $self = shift;
1056     return undef unless (ref $self);
1057     return $self->{prefix};
1058 }
1059
1060 sub suffix {
1061     my $self = shift;
1062     return undef unless (ref $self);
1063     return $self->{suffix};
1064 }
1065
1066 #-------------------------------
1067 package QueryParser::query_plan::filter;
1068
1069 sub new {
1070     my $pkg = shift;
1071     $pkg = ref($pkg) || $pkg;
1072     my %args = @_;
1073
1074     return bless \%args => $pkg;
1075 }
1076
1077 sub plan {
1078     my $self = shift;
1079     return $self->{plan};
1080 }
1081
1082 sub name {
1083     my $self = shift;
1084     return $self->{name};
1085 }
1086
1087 sub negate {
1088     my $self = shift;
1089     return $self->{negate};
1090 }
1091
1092 sub args {
1093     my $self = shift;
1094     return $self->{args};
1095 }
1096
1097 #-------------------------------
1098 package QueryParser::query_plan::facet;
1099
1100 sub new {
1101     my $pkg = shift;
1102     $pkg = ref($pkg) || $pkg;
1103     my %args = @_;
1104
1105     return bless \%args => $pkg;
1106 }
1107
1108 sub plan {
1109     my $self = shift;
1110     return $self->{plan};
1111 }
1112
1113 sub name {
1114     my $self = shift;
1115     return $self->{name};
1116 }
1117
1118 sub negate {
1119     my $self = shift;
1120     return $self->{negate};
1121 }
1122
1123 sub values {
1124     my $self = shift;
1125     return $self->{'values'};
1126 }
1127
1128 #-------------------------------
1129 package QueryParser::query_plan::modifier;
1130
1131 sub new {
1132     my $pkg = shift;
1133     $pkg = ref($pkg) || $pkg;
1134     my $modifier = shift;
1135
1136     return bless \$modifier => $pkg;
1137 }
1138
1139 sub name {
1140     my $self = shift;
1141     return $$self;
1142 }
1143
1144 1;
1145