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