]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
Merge branch 'opac-tt-poc' of git+ssh://yeti.esilibrary.com/home/evergreen/evergreen...
[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 = $';
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 );
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     return $struct if !wantarray;
689     return ($struct, $remainder);
690 }
691
692 sub find_class_index {
693     my $class = shift;
694     my $query = shift;
695
696     my ($class_part, @field_parts) = split '\|', $class;
697     $class_part ||= $class;
698
699     for my $idx ( 0 .. scalar(@$query) - 1 ) {
700         next unless ref($$query[$idx]);
701         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
702     }
703
704     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
705     return -1;
706 }
707
708 sub core_limit {
709     my $self = shift;
710     my $l = shift;
711     $self->{core_limit} = $l if ($l);
712     return $self->{core_limit};
713 }
714
715 sub superpage {
716     my $self = shift;
717     my $l = shift;
718     $self->{superpage} = $l if ($l);
719     return $self->{superpage};
720 }
721
722 sub superpage_size {
723     my $self = shift;
724     my $l = shift;
725     $self->{superpage_size} = $l if ($l);
726     return $self->{superpage_size};
727 }
728
729
730 #-------------------------------
731 package QueryParser::query_plan;
732
733 sub QueryParser {
734     my $self = shift;
735     return undef unless ref($self);
736     return $self->{QueryParser};
737 }
738
739 sub new {
740     my $pkg = shift;
741     $pkg = ref($pkg) || $pkg;
742     my %args = (query => [], joiner => '&', @_);
743
744     return bless \%args => $pkg;
745 }
746
747 sub new_node {
748     my $self = shift;
749     my $pkg = ref($self) || $self;
750     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
751     $self->add_node( $node );
752     return $node;
753 }
754
755 sub new_facet {
756     my $self = shift;
757     my $pkg = ref($self) || $self;
758     my $name = shift;
759     my $args = shift;
760     my $negate = shift;
761
762     my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
763     $self->add_node( $node );
764
765     return $node;
766 }
767
768 sub new_filter {
769     my $self = shift;
770     my $pkg = ref($self) || $self;
771     my $name = shift;
772     my $args = shift;
773     my $negate = shift;
774
775     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
776     $self->add_filter( $node );
777
778     return $node;
779 }
780
781 sub find_filter {
782     my $self = shift;
783     my $needle = shift;;
784     return undef unless ($needle);
785     return grep { $_->name eq $needle } @{ $self->filters };
786 }
787
788 sub find_modifier {
789     my $self = shift;
790     my $needle = shift;;
791     return undef unless ($needle);
792     return grep { $_->name eq $needle } @{ $self->modifiers };
793 }
794
795 sub new_modifier {
796     my $self = shift;
797     my $pkg = ref($self) || $self;
798     my $name = shift;
799
800     my $node = do{$pkg.'::modifier'}->new( $name );
801     $self->add_modifier( $node );
802
803     return $node;
804 }
805
806 sub classed_node {
807     my $self = shift;
808     my $requested_class = shift;
809
810     my $node;
811     for my $n (@{$self->{query}}) {
812         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
813         if ($n->requested_class eq $requested_class) {
814             $node = $n;
815             last;
816         }
817     }
818
819     if (!$node) {
820         $node = $self->new_node;
821         $node->requested_class( $requested_class );
822     }
823
824     return $node;
825 }
826
827 sub remove_last_node {
828     my $self = shift;
829     my $requested_class = shift;
830
831     my $old = pop(@{$self->query_nodes});
832     pop(@{$self->query_nodes}) if (@{$self->query_nodes});
833
834     return $old;
835 }
836
837 sub query_nodes {
838     my $self = shift;
839     return $self->{query};
840 }
841
842 sub add_node {
843     my $self = shift;
844     my $node = shift;
845
846     $self->{query} ||= [];
847     push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
848     push(@{$self->{query}}, $node);
849
850     return $self;
851 }
852
853 sub top_plan {
854     my $self = shift;
855
856     return $self->{level} ? 0 : 1;
857 }
858
859 sub plan_level {
860     my $self = shift;
861     return $self->{level};
862 }
863
864 sub joiner {
865     my $self = shift;
866     my $joiner = shift;
867
868     $self->{joiner} = $joiner if ($joiner);
869     return $self->{joiner};
870 }
871
872 sub modifiers {
873     my $self = shift;
874     $self->{modifiers} ||= [];
875     return $self->{modifiers};
876 }
877
878 sub add_modifier {
879     my $self = shift;
880     my $modifier = shift;
881
882     $self->{modifiers} ||= [];
883     return $self if (grep {$$_ eq $$modifier} @{$self->{modifiers}});
884
885     push(@{$self->{modifiers}}, $modifier);
886
887     return $self;
888 }
889
890 sub facets {
891     my $self = shift;
892     $self->{facets} ||= [];
893     return $self->{facets};
894 }
895
896 sub add_facet {
897     my $self = shift;
898     my $facet = shift;
899
900     $self->{facets} ||= [];
901     return $self if (grep {$_->name eq $facet->name} @{$self->{facets}});
902
903     push(@{$self->{facets}}, $facet);
904
905     return $self;
906 }
907
908 sub filters {
909     my $self = shift;
910     $self->{filters} ||= [];
911     return $self->{filters};
912 }
913
914 sub add_filter {
915     my $self = shift;
916     my $filter = shift;
917
918     $self->{filters} ||= [];
919     return $self if (grep {$_->name eq $filter->name} @{$self->{filters}});
920
921     push(@{$self->{filters}}, $filter);
922
923     return $self;
924 }
925
926
927 #-------------------------------
928 package QueryParser::query_plan::node;
929
930 sub new {
931     my $pkg = shift;
932     $pkg = ref($pkg) || $pkg;
933     my %args = @_;
934
935     return bless \%args => $pkg;
936 }
937
938 sub new_atom {
939     my $self = shift;
940     my $pkg = ref($self) || $self;
941     return do{$pkg.'::atom'}->new( @_ );
942 }
943
944 sub requested_class { # also split into classname and fields
945     my $self = shift;
946     my $class = shift;
947
948     if ($class) {
949         my ($class_part, @field_parts) = split '\|', $class;
950         $class_part ||= $class;
951
952         $self->{requested_class} = $class;
953         $self->{classname} = $class_part;
954         $self->{fields} = \@field_parts;
955     }
956
957     return $self->{requested_class};
958 }
959
960 sub plan {
961     my $self = shift;
962     my $plan = shift;
963
964     $self->{plan} = $plan if ($plan);
965     return $self->{plan};
966 }
967
968 sub classname {
969     my $self = shift;
970     my $class = shift;
971
972     $self->{classname} = $class if ($class);
973     return $self->{classname};
974 }
975
976 sub fields {
977     my $self = shift;
978     my @fields = @_;
979
980     $self->{fields} ||= [];
981     $self->{fields} = \@fields if (@fields);
982     return $self->{fields};
983 }
984
985 sub phrases {
986     my $self = shift;
987     my @phrases = @_;
988
989     $self->{phrases} ||= [];
990     $self->{phrases} = \@phrases if (@phrases);
991     return $self->{phrases};
992 }
993
994 sub add_phrase {
995     my $self = shift;
996     my $phrase = shift;
997
998     push(@{$self->phrases}, $phrase);
999
1000     return $self;
1001 }
1002
1003 sub query_atoms {
1004     my $self = shift;
1005     my @query_atoms = @_;
1006
1007     $self->{query_atoms} ||= [];
1008     $self->{query_atoms} = \@query_atoms if (@query_atoms);
1009     return $self->{query_atoms};
1010 }
1011
1012 sub add_fts_atom {
1013     my $self = shift;
1014     my $atom = shift;
1015
1016     if (!ref($atom)) {
1017         my $content = $atom;
1018         my @parts = @_;
1019
1020         $atom = $self->new_atom( content => $content, @parts );
1021     }
1022
1023     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1024     push(@{$self->query_atoms}, $atom);
1025
1026     return $self;
1027 }
1028
1029 #-------------------------------
1030 package QueryParser::query_plan::node::atom;
1031
1032 sub new {
1033     my $pkg = shift;
1034     $pkg = ref($pkg) || $pkg;
1035     my %args = @_;
1036
1037     return bless \%args => $pkg;
1038 }
1039
1040 sub node {
1041     my $self = shift;
1042     return undef unless (ref $self);
1043     return $self->{node};
1044 }
1045
1046 sub content {
1047     my $self = shift;
1048     return undef unless (ref $self);
1049     return $self->{content};
1050 }
1051
1052 sub prefix {
1053     my $self = shift;
1054     return undef unless (ref $self);
1055     return $self->{prefix};
1056 }
1057
1058 sub suffix {
1059     my $self = shift;
1060     return undef unless (ref $self);
1061     return $self->{suffix};
1062 }
1063
1064 #-------------------------------
1065 package QueryParser::query_plan::filter;
1066
1067 sub new {
1068     my $pkg = shift;
1069     $pkg = ref($pkg) || $pkg;
1070     my %args = @_;
1071
1072     return bless \%args => $pkg;
1073 }
1074
1075 sub plan {
1076     my $self = shift;
1077     return $self->{plan};
1078 }
1079
1080 sub name {
1081     my $self = shift;
1082     return $self->{name};
1083 }
1084
1085 sub negate {
1086     my $self = shift;
1087     return $self->{negate};
1088 }
1089
1090 sub args {
1091     my $self = shift;
1092     return $self->{args};
1093 }
1094
1095 #-------------------------------
1096 package QueryParser::query_plan::facet;
1097
1098 sub new {
1099     my $pkg = shift;
1100     $pkg = ref($pkg) || $pkg;
1101     my %args = @_;
1102
1103     return bless \%args => $pkg;
1104 }
1105
1106 sub plan {
1107     my $self = shift;
1108     return $self->{plan};
1109 }
1110
1111 sub name {
1112     my $self = shift;
1113     return $self->{name};
1114 }
1115
1116 sub negate {
1117     my $self = shift;
1118     return $self->{negate};
1119 }
1120
1121 sub values {
1122     my $self = shift;
1123     return $self->{'values'};
1124 }
1125
1126 #-------------------------------
1127 package QueryParser::query_plan::modifier;
1128
1129 sub new {
1130     my $pkg = shift;
1131     $pkg = ref($pkg) || $pkg;
1132     my $modifier = shift;
1133
1134     return bless \$modifier => $pkg;
1135 }
1136
1137 sub name {
1138     my $self = shift;
1139     return $$self;
1140 }
1141
1142 1;
1143