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