]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Storage/QueryParser.pm
QP: OO-ize canonicalizer; remove extra nesting from canonicalized query; repair neste...
[working/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             disallowed => '-',
17             modifier => '#'
18         }
19     }
20 );
21
22 sub canonicalize {
23     my $self = shift;
24     return QueryParser::Canonicalize::abstract_query2str_impl(
25         $self->parse_tree->to_abstract_query(@_)
26     );
27 }
28
29
30 sub facet_class_count {
31     my $self = shift;
32     return @{$self->facet_classes};
33 }
34
35 sub search_class_count {
36     my $self = shift;
37     return @{$self->search_classes};
38 }
39
40 sub filter_count {
41     my $self = shift;
42     return @{$self->filters};
43 }
44
45 sub modifier_count {
46     my $self = shift;
47     return @{$self->modifiers};
48 }
49
50 sub custom_data {
51     my $class = shift;
52     $class = ref($class) || $class;
53
54     $parser_config{$class}{custom_data} ||= {};
55     return $parser_config{$class}{custom_data};
56 }
57
58 sub operators {
59     my $class = shift;
60     $class = ref($class) || $class;
61
62     $parser_config{$class}{operators} ||= {};
63     return $parser_config{$class}{operators};
64 }
65
66 sub filters {
67     my $class = shift;
68     $class = ref($class) || $class;
69
70     $parser_config{$class}{filters} ||= [];
71     return $parser_config{$class}{filters};
72 }
73
74 sub filter_callbacks {
75     my $class = shift;
76     $class = ref($class) || $class;
77
78     $parser_config{$class}{filter_callbacks} ||= {};
79     return $parser_config{$class}{filter_callbacks};
80 }
81
82 sub modifiers {
83     my $class = shift;
84     $class = ref($class) || $class;
85
86     $parser_config{$class}{modifiers} ||= [];
87     return $parser_config{$class}{modifiers};
88 }
89
90 sub new {
91     my $class = shift;
92     $class = ref($class) || $class;
93
94     my %opts = @_;
95
96     my $self = bless {} => $class;
97
98     for my $o (keys %{QueryParser->operators}) {
99         $class->operator($o => QueryParser->operator($o)) unless ($class->operator($o));
100     }
101
102     for my $opt ( keys %opts) {
103         $self->$opt( $opts{$opt} ) if ($self->can($opt));
104     }
105
106     return $self;
107 }
108
109 sub new_plan {
110     my $self = shift;
111     my $pkg = ref($self) || $self;
112     return do{$pkg.'::query_plan'}->new( QueryParser => $self, @_ );
113 }
114
115 sub add_search_filter {
116     my $pkg = shift;
117     $pkg = ref($pkg) || $pkg;
118     my $filter = shift;
119     my $callback = shift;
120
121     return $filter if (grep { $_ eq $filter } @{$pkg->filters});
122     push @{$pkg->filters}, $filter;
123     $pkg->filter_callbacks->{$filter} = $callback if ($callback);
124     return $filter;
125 }
126
127 sub add_search_modifier {
128     my $pkg = shift;
129     $pkg = ref($pkg) || $pkg;
130     my $modifier = shift;
131
132     return $modifier if (grep { $_ eq $modifier } @{$pkg->modifiers});
133     push @{$pkg->modifiers}, $modifier;
134     return $modifier;
135 }
136
137 sub add_facet_class {
138     my $pkg = shift;
139     $pkg = ref($pkg) || $pkg;
140     my $class = shift;
141
142     return $class if (grep { $_ eq $class } @{$pkg->facet_classes});
143
144     push @{$pkg->facet_classes}, $class;
145     $pkg->facet_fields->{$class} = [];
146
147     return $class;
148 }
149
150 sub add_search_class {
151     my $pkg = shift;
152     $pkg = ref($pkg) || $pkg;
153     my $class = shift;
154
155     return $class if (grep { $_ eq $class } @{$pkg->search_classes});
156
157     push @{$pkg->search_classes}, $class;
158     $pkg->search_fields->{$class} = [];
159     $pkg->default_search_class( $pkg->search_classes->[0] ) if (@{$pkg->search_classes} == 1);
160
161     return $class;
162 }
163
164 sub operator {
165     my $class = shift;
166     $class = ref($class) || $class;
167     my $opname = shift;
168     my $op = shift;
169
170     return undef unless ($opname);
171
172     $parser_config{$class}{operators} ||= {};
173     $parser_config{$class}{operators}{$opname} = $op if ($op);
174
175     return $parser_config{$class}{operators}{$opname};
176 }
177
178 sub facet_classes {
179     my $class = shift;
180     $class = ref($class) || $class;
181     my $classes = shift;
182
183     $parser_config{$class}{facet_classes} ||= [];
184     $parser_config{$class}{facet_classes} = $classes if (ref($classes) && @$classes);
185     return $parser_config{$class}{facet_classes};
186 }
187
188 sub search_classes {
189     my $class = shift;
190     $class = ref($class) || $class;
191     my $classes = shift;
192
193     $parser_config{$class}{classes} ||= [];
194     $parser_config{$class}{classes} = $classes if (ref($classes) && @$classes);
195     return $parser_config{$class}{classes};
196 }
197
198 sub add_query_normalizer {
199     my $pkg = shift;
200     $pkg = ref($pkg) || $pkg;
201     my $class = shift;
202     my $field = shift;
203     my $func = shift;
204     my $params = shift || [];
205
206     # do not add if function AND params are identical to existing member
207     return $func if (grep {
208         $_->{function} eq $func and 
209         OpenSRF::Utils::JSON->perl2JSON($_->{params}) eq OpenSRF::Utils::JSON->perl2JSON($params)
210     } @{$pkg->query_normalizers->{$class}->{$field}});
211
212     push(@{$pkg->query_normalizers->{$class}->{$field}}, { function => $func, params => $params });
213
214     return $func;
215 }
216
217 sub query_normalizers {
218     my $pkg = shift;
219     $pkg = ref($pkg) || $pkg;
220
221     my $class = shift;
222     my $field = shift;
223
224     $parser_config{$pkg}{normalizers} ||= {};
225     if ($class) {
226         if ($field) {
227             $parser_config{$pkg}{normalizers}{$class}{$field} ||= [];
228             return $parser_config{$pkg}{normalizers}{$class}{$field};
229         } else {
230             return $parser_config{$pkg}{normalizers}{$class};
231         }
232     }
233
234     return $parser_config{$pkg}{normalizers};
235 }
236
237 sub add_filter_normalizer {
238     my $pkg = shift;
239     $pkg = ref($pkg) || $pkg;
240     my $filter = shift;
241     my $func = shift;
242     my $params = shift || [];
243
244     return $func if (grep { $_ eq $func } @{$pkg->filter_normalizers->{$filter}});
245
246     push(@{$pkg->filter_normalizers->{$filter}}, { function => $func, params => $params });
247
248     return $func;
249 }
250
251 sub filter_normalizers {
252     my $pkg = shift;
253     $pkg = ref($pkg) || $pkg;
254
255     my $filter = shift;
256
257     $parser_config{$pkg}{filter_normalizers} ||= {};
258     if ($filter) {
259         $parser_config{$pkg}{filter_normalizers}{$filter} ||= [];
260         return $parser_config{$pkg}{filter_normalizers}{$filter};
261     }
262
263     return $parser_config{$pkg}{filter_normalizers};
264 }
265
266 sub default_search_class {
267     my $pkg = shift;
268     $pkg = ref($pkg) || $pkg;
269     my $class = shift;
270     $QueryParser::parser_config{$pkg}{default_class} = $pkg->add_search_class( $class ) if $class;
271
272     return $QueryParser::parser_config{$pkg}{default_class};
273 }
274
275 sub remove_facet_class {
276     my $pkg = shift;
277     $pkg = ref($pkg) || $pkg;
278     my $class = shift;
279
280     return $class if (!grep { $_ eq $class } @{$pkg->facet_classes});
281
282     $pkg->facet_classes( [ grep { $_ ne $class } @{$pkg->facet_classes} ] );
283     delete $QueryParser::parser_config{$pkg}{facet_fields}{$class};
284
285     return $class;
286 }
287
288 sub remove_search_class {
289     my $pkg = shift;
290     $pkg = ref($pkg) || $pkg;
291     my $class = shift;
292
293     return $class if (!grep { $_ eq $class } @{$pkg->search_classes});
294
295     $pkg->search_classes( [ grep { $_ ne $class } @{$pkg->search_classes} ] );
296     delete $QueryParser::parser_config{$pkg}{fields}{$class};
297
298     return $class;
299 }
300
301 sub add_facet_field {
302     my $pkg = shift;
303     $pkg = ref($pkg) || $pkg;
304     my $class = shift;
305     my $field = shift;
306
307     $pkg->add_facet_class( $class );
308
309     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
310
311     push @{$pkg->facet_fields->{$class}}, $field;
312
313     return { $class => $field };
314 }
315
316 sub facet_fields {
317     my $class = shift;
318     $class = ref($class) || $class;
319
320     $parser_config{$class}{facet_fields} ||= {};
321     return $parser_config{$class}{facet_fields};
322 }
323
324 sub add_search_field {
325     my $pkg = shift;
326     $pkg = ref($pkg) || $pkg;
327     my $class = shift;
328     my $field = shift;
329
330     $pkg->add_search_class( $class );
331
332     return { $class => $field }  if (grep { $_ eq $field } @{$pkg->search_fields->{$class}});
333
334     push @{$pkg->search_fields->{$class}}, $field;
335
336     return { $class => $field };
337 }
338
339 sub search_fields {
340     my $class = shift;
341     $class = ref($class) || $class;
342
343     $parser_config{$class}{fields} ||= {};
344     return $parser_config{$class}{fields};
345 }
346
347 sub add_search_class_alias {
348     my $pkg = shift;
349     $pkg = ref($pkg) || $pkg;
350     my $class = shift;
351     my $alias = shift;
352
353     $pkg->add_search_class( $class );
354
355     return { $class => $alias }  if (grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
356
357     push @{$pkg->search_class_aliases->{$class}}, $alias;
358
359     return { $class => $alias };
360 }
361
362 sub search_class_aliases {
363     my $class = shift;
364     $class = ref($class) || $class;
365
366     $parser_config{$class}{class_map} ||= {};
367     return $parser_config{$class}{class_map};
368 }
369
370 sub add_search_field_alias {
371     my $pkg = shift;
372     $pkg = ref($pkg) || $pkg;
373     my $class = shift;
374     my $field = shift;
375     my $alias = shift;
376
377     return { $class => { $field => $alias } }  if (grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
378
379     push @{$pkg->search_field_aliases->{$class}{$field}}, $alias;
380
381     return { $class => { $field => $alias } };
382 }
383
384 sub search_field_aliases {
385     my $class = shift;
386     $class = ref($class) || $class;
387
388     $parser_config{$class}{field_alias_map} ||= {};
389     return $parser_config{$class}{field_alias_map};
390 }
391
392 sub remove_facet_field {
393     my $pkg = shift;
394     $pkg = ref($pkg) || $pkg;
395     my $class = shift;
396     my $field = shift;
397
398     return { $class => $field }  if (!$pkg->facet_fields->{$class} || !grep { $_ eq $field } @{$pkg->facet_fields->{$class}});
399
400     $pkg->facet_fields->{$class} = [ grep { $_ ne $field } @{$pkg->facet_fields->{$class}} ];
401
402     return { $class => $field };
403 }
404
405 sub remove_search_field {
406     my $pkg = shift;
407     $pkg = ref($pkg) || $pkg;
408     my $class = shift;
409     my $field = shift;
410
411     return { $class => $field }  if (!$pkg->search_fields->{$class} || !grep { $_ eq $field } @{$pkg->search_fields->{$class}});
412
413     $pkg->search_fields->{$class} = [ grep { $_ ne $field } @{$pkg->search_fields->{$class}} ];
414
415     return { $class => $field };
416 }
417
418 sub remove_search_field_alias {
419     my $pkg = shift;
420     $pkg = ref($pkg) || $pkg;
421     my $class = shift;
422     my $field = shift;
423     my $alias = shift;
424
425     return { $class => { $field => $alias } }  if (!$pkg->search_field_aliases->{$class}{$field} || !grep { $_ eq $alias } @{$pkg->search_field_aliases->{$class}{$field}});
426
427     $pkg->search_field_aliases->{$class}{$field} = [ grep { $_ ne $alias } @{$pkg->search_field_aliases->{$class}{$field}} ];
428
429     return { $class => { $field => $alias } };
430 }
431
432 sub remove_search_class_alias {
433     my $pkg = shift;
434     $pkg = ref($pkg) || $pkg;
435     my $class = shift;
436     my $alias = shift;
437
438     return { $class => $alias }  if (!$pkg->search_class_aliases->{$class} || !grep { $_ eq $alias } @{$pkg->search_class_aliases->{$class}});
439
440     $pkg->search_class_aliases->{$class} = [ grep { $_ ne $alias } @{$pkg->search_class_aliases->{$class}} ];
441
442     return { $class => $alias };
443 }
444
445 sub debug {
446     my $self = shift;
447     my $q = shift;
448     $self->{_debug} = $q if (defined $q);
449     return $self->{_debug};
450 }
451
452 sub query {
453     my $self = shift;
454     my $q = shift;
455     $self->{_query} = $q if (defined $q);
456     return $self->{_query};
457 }
458
459 sub parse_tree {
460     my $self = shift;
461     my $q = shift;
462     $self->{_parse_tree} = $q if (defined $q);
463     return $self->{_parse_tree};
464 }
465
466 sub parse {
467     my $self = shift;
468     my $pkg = ref($self) || $self;
469     warn " ** parse package is $pkg\n" if $self->debug;
470     $self->parse_tree(
471         $self->decompose(
472             $self->query( shift() )
473         )
474     );
475
476     return $self;
477 }
478
479 sub decompose {
480     my $self = shift;
481     my $pkg = ref($self) || $self;
482
483     warn " ** decompose package is $pkg\n" if $self->debug;
484
485     $_ = shift;
486     my $current_class = shift || $self->default_search_class;
487
488     my $recursing = shift || 0;
489     my $phrase_helper = shift || 0;
490
491     # Build the search class+field uber-regexp
492     my $search_class_re = '^\s*(';
493     my $first_class = 1;
494
495     my %seen_classes;
496     for my $class ( keys %{$pkg->search_field_aliases} ) {
497         warn " *** ... Looking for search fields in $class\n" if $self->debug;
498
499         for my $field ( keys %{$pkg->search_field_aliases->{$class}} ) {
500             warn " *** ... Looking for aliases of $field\n" if $self->debug;
501
502             for my $alias ( @{$pkg->search_field_aliases->{$class}{$field}} ) {
503                 my $aliasr = qr/$alias/;
504                 s/(^|\s+)$aliasr\|/$1$class\|$field#$alias\|/g;
505                 s/(^|\s+)$aliasr[:=]/$1$class\|$field#$alias:/g;
506                 warn " *** Rewriting: $alias ($aliasr) as $class\|$field\n" if $self->debug;
507             }
508         }
509
510         $search_class_re .= '|' unless ($first_class);
511         $first_class = 0;
512         $search_class_re .= $class . '(?:[|#][^:|]+)*';
513         $seen_classes{$class} = 1;
514     }
515
516     for my $class ( keys %{$pkg->search_class_aliases} ) {
517
518         for my $alias ( @{$pkg->search_class_aliases->{$class}} ) {
519             my $aliasr = qr/$alias/;
520             s/(^|[^|])\b$aliasr\|/$1$class#$alias\|/g;
521             s/(^|[^|])\b$aliasr[:=]/$1$class#$alias:/g;
522             warn " *** Rewriting: $alias ($aliasr) as $class\n" if $self->debug;
523         }
524
525         if (!$seen_classes{$class}) {
526             $search_class_re .= '|' unless ($first_class);
527             $first_class = 0;
528
529             $search_class_re .= $class . '(?:[|#][^:|]+)*';
530             $seen_classes{$class} = 1;
531         }
532     }
533     $search_class_re .= '):';
534
535     warn " ** Rewritten query: $_\n" if $self->debug;
536     warn " ** Search class RE: $search_class_re\n" if $self->debug;
537
538     my $required_re = $pkg->operator('required');
539     $required_re = qr/\Q$required_re\E/;
540
541     my $disallowed_re = $pkg->operator('disallowed');
542     $disallowed_re = qr/\Q$disallowed_re\E/;
543
544     my $and_re = $pkg->operator('and');
545     $and_re = qr/^\s*\Q$and_re\E/;
546
547     my $or_re = $pkg->operator('or');
548     $or_re = qr/^\s*\Q$or_re\E/;
549
550     my $group_start_re = $pkg->operator('group_start');
551     $group_start_re = qr/^\s*\Q$group_start_re\E/;
552
553     my $group_end = $pkg->operator('group_end');
554     my $group_end_re = qr/^\s*\Q$group_end\E/;
555
556     my $modifier_tag_re = $pkg->operator('modifier');
557     $modifier_tag_re = qr/^\s*\Q$modifier_tag_re\E/;
558
559
560     # Build the filter and modifier uber-regexps
561     my $facet_re = '^\s*(-?)((?:' . join( '|', @{$pkg->facet_classes}) . ')(?:\|\w+)*)\[(.+?)\]';
562     warn " ** Facet RE: $facet_re\n" if $self->debug;
563
564     my $filter_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . ')\(([^()]+)\)';
565     my $filter_as_class_re = '^\s*(-?)(' . join( '|', @{$pkg->filters}) . '):\s*(\S+)';
566
567     my $modifier_re = '^\s*'.$modifier_tag_re.'(' . join( '|', @{$pkg->modifiers}) . ')\b';
568     my $modifier_as_class_re = '^\s*(' . join( '|', @{$pkg->modifiers}) . '):\s*(\S+)';
569
570     my $struct = $self->new_plan( level => $recursing );
571     my $remainder = '';
572
573     my $last_type = '';
574     while (!$remainder) {
575         if (/^\s*$/) { # end of an explicit group
576             last;
577         } elsif (/$group_end_re/) { # end of an explicit group
578             warn "Encountered explicit group end\n" if $self->debug;
579
580             $_ = $';
581             $remainder = $struct->top_plan ? '' : $';
582
583             $last_type = '';
584         } elsif ($self->filter_count && /$filter_re/) { # found a filter
585             warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
586
587             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
588             $_ = $';
589
590             my $filter = $2;
591             my $params = [ split '[,]+', $3 ];
592
593             if ($pkg->filter_callbacks->{$filter}) {
594                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
595                 $_ = "$replacement $_" if ($replacement);
596             } else {
597                 $struct->new_filter( $filter => $params, $negate );
598             }
599
600
601             $last_type = '';
602         } elsif ($self->filter_count && /$filter_as_class_re/) { # found a filter
603             warn "Encountered search filter: $1$2 set to $3\n" if $self->debug;
604
605             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
606             $_ = $';
607
608             my $filter = $2;
609             my $params = [ split '[,]+', $3 ];
610
611             if ($pkg->filter_callbacks->{$filter}) {
612                 my $replacement = $pkg->filter_callbacks->{$filter}->($self, $struct, $filter, $params, $negate);
613                 $_ = "$replacement $_" if ($replacement);
614             } else {
615                 $struct->new_filter( $filter => $params, $negate );
616             }
617
618             $last_type = '';
619         } elsif ($self->modifier_count && /$modifier_re/) { # found a modifier
620             warn "Encountered search modifier: $1\n" if $self->debug;
621
622             $_ = $';
623             if (!$struct->top_plan) {
624                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
625             } else {
626                 $struct->new_modifier($1);
627             }
628
629             $last_type = '';
630         } elsif ($self->modifier_count && /$modifier_as_class_re/) { # found a modifier
631             warn "Encountered search modifier: $1\n" if $self->debug;
632
633             my $mod = $1;
634
635             $_ = $';
636             if (!$struct->top_plan) {
637                 warn "  Search modifiers only allowed at the top level of the query\n" if $self->debug;
638             } elsif ($2 =~ /^[ty1]/i) {
639                 $struct->new_modifier($mod);
640             }
641
642             $last_type = '';
643         } elsif (/$group_start_re/) { # start of an explicit group
644             warn "Encountered explicit group start\n" if $self->debug;
645
646             my ($substruct, $subremainder) = $self->decompose( $', $current_class, $recursing + 1 );
647             $struct->add_node( $substruct ) if ($substruct);
648             $_ = $subremainder;
649
650             $last_type = '';
651         } elsif (/$and_re/) { # ANDed expression
652             $_ = $';
653             next if ($last_type eq 'AND');
654             next if ($last_type eq 'OR');
655             warn "Encountered AND\n" if $self->debug;
656
657             my $LHS = $struct;
658             my ($RHS, $subremainder) = $self->decompose( '('.$_.')', $current_class, $recursing + 1 );
659             $_ = $subremainder;
660
661             $struct = $self->new_plan( level => $recursing, joiner => '&' );
662             $struct->add_node($_) for ($LHS, $RHS);
663
664             $last_type = 'AND';
665         } elsif (/$or_re/) { # ORed expression
666             $_ = $';
667             next if ($last_type eq 'AND');
668             next if ($last_type eq 'OR');
669             warn "Encountered OR\n" if $self->debug;
670
671             my $LHS = $struct;
672             my ($RHS, $subremainder) = $self->decompose( '('.$_.')', $current_class, $recursing + 1 );
673             $_ = $subremainder;
674
675             $struct = $self->new_plan( level => $recursing, joiner => '|' );
676             $struct->add_node($_) for ($LHS, $RHS);
677
678             $last_type = 'OR';
679         } elsif ($self->facet_class_count && /$facet_re/) { # changing current class
680             warn "Encountered facet: $1$2 => $3\n" if $self->debug;
681
682             my $negate = ($1 eq $pkg->operator('disallowed')) ? 1 : 0;
683             my $facet = $2;
684             my $facet_value = [ split '\s*#\s*', $3 ];
685             $struct->new_facet( $facet => $facet_value, $negate );
686             $_ = $';
687
688             $last_type = '';
689         } elsif ($self->search_class_count && /$search_class_re/) { # changing current class
690
691             if ($last_type eq 'CLASS') {
692                 $struct->remove_last_node( $current_class );
693                 warn "Encountered class change with no searches!\n" if $self->debug;
694             }
695
696             warn "Encountered class change: $1\n" if $self->debug;
697
698             $current_class = $struct->classed_node( $1 )->requested_class();
699             $_ = $';
700
701             $last_type = 'CLASS';
702         } elsif (/^\s*($required_re|$disallowed_re)?"([^"]+)"/) { # phrase, always anded
703             warn 'Encountered' . ($1 ? " ['$1' modified]" : '') . " phrase: $2\n" if $self->debug;
704
705             my $req_ness = $1 || '';
706             my $phrase = $2;
707
708             if (!$phrase_helper) {
709                 warn "Recursing into decompose with the phrase as a subquery\n" if $self->debug;
710                 my $after = $';
711                 my ($substruct, $subremainder) = $self->decompose( qq/$req_ness"$phrase"/, $current_class, $recursing + 1, 1 );
712                 $struct->add_node( $substruct ) if ($substruct);
713                 $_ = $after;
714             } else {
715                 warn "Directly parsing the phrase subquery\n" if $self->debug;
716                 $struct->joiner( '&' );
717
718                 my $class_node = $struct->classed_node($current_class);
719
720                 if ($req_ness eq $pkg->operator('disallowed')) {
721                     $class_node->add_dummy_atom( node => $class_node );
722                     $class_node->add_unphrase( $phrase );
723                     $phrase = '';
724                     #$phrase =~ s/(^|\s)\b/$1-/g;
725                 } else { 
726                     $class_node->add_phrase( $phrase );
727                 }
728                 $_ = $phrase . $';
729
730             }
731
732             $last_type = '';
733
734 #        } elsif (/^\s*$required_re([^\s"]+)/) { # phrase, always anded
735 #            warn "Encountered required atom (mini phrase): $1\n" if $self->debug;
736 #
737 #            my $phrase = $1;
738 #
739 #            my $class_node = $struct->classed_node($current_class);
740 #            $class_node->add_phrase( $phrase );
741 #            $_ = $phrase . $';
742 #            $struct->joiner( '&' );
743 #
744 #            $last_type = '';
745         } elsif (/^\s*([^$group_end\s]+)/o) { # atom
746             warn "Encountered atom: $1\n" if $self->debug;
747             warn "Remainder: $'\n" if $self->debug;
748
749             my $atom = $1;
750             my $after = $';
751
752             $_ = $after;
753             $last_type = '';
754
755             my $class_node = $struct->classed_node($current_class);
756
757             my $prefix = ($atom =~ s/^$disallowed_re//o) ? '!' : '';
758             my $truncate = ($atom =~ s/\*$//o) ? '*' : '';
759
760             if ($atom ne '' and !grep { $atom =~ /^\Q$_\E+$/ } ('&','|','-','+')) { # throw away & and |, not allowed in tsquery, and not really useful anyway
761 #                $class_node->add_phrase( $atom ) if ($atom =~ s/^$required_re//o);
762 #                $class_node->add_unphrase( $atom ) if ($prefix eq '!');
763
764                 $class_node->add_fts_atom( $atom, suffix => $truncate, prefix => $prefix, node => $class_node );
765                 $struct->joiner( '&' );
766             }
767         } 
768
769         last unless ($_);
770
771     }
772
773     $struct = undef if 
774         scalar(@{$struct->query_nodes}) == 0 &&
775         scalar(@{$struct->filters}) == 0 &&
776         !$struct->top_plan;
777
778     return $struct if !wantarray;
779     return ($struct, $remainder);
780 }
781
782 sub find_class_index {
783     my $class = shift;
784     my $query = shift;
785
786     my ($class_part, @field_parts) = split '\|', $class;
787     $class_part ||= $class;
788
789     for my $idx ( 0 .. scalar(@$query) - 1 ) {
790         next unless ref($$query[$idx]);
791         return $idx if ( $$query[$idx]{requested_class} && $class eq $$query[$idx]{requested_class} );
792     }
793
794     push(@$query, { classname => $class_part, (@field_parts ? (fields => \@field_parts) : ()), requested_class => $class, ftsquery => [], phrases => [] });
795     return -1;
796 }
797
798 sub core_limit {
799     my $self = shift;
800     my $l = shift;
801     $self->{core_limit} = $l if ($l);
802     return $self->{core_limit};
803 }
804
805 sub superpage {
806     my $self = shift;
807     my $l = shift;
808     $self->{superpage} = $l if ($l);
809     return $self->{superpage};
810 }
811
812 sub superpage_size {
813     my $self = shift;
814     my $l = shift;
815     $self->{superpage_size} = $l if ($l);
816     return $self->{superpage_size};
817 }
818
819
820 #-------------------------------
821 package QueryParser::_util;
822
823 # At this level, joiners are always & or |.  This is not
824 # the external, configurable representation of joiners that
825 # defaults to # && and ||.
826 sub is_joiner {
827     my $str = shift;
828
829     return (not ref $str and ($str eq '&' or $str eq '|'));
830 }
831
832 sub default_joiner { '&' }
833
834 # 0 for different, 1 for the same.
835 sub compare_abstract_atoms {
836     my ($left, $right) = @_;
837
838     foreach (qw/prefix suffix content/) {
839         no warnings;    # undef can stand in for '' here
840         return 0 unless $left->{$_} eq $right->{$_};
841     }
842
843     return 1;
844 }
845
846 sub fake_abstract_atom_from_phrase {
847     my $phrase = shift;
848     my $neg = shift;
849     my $qp_class = shift || 'QueryParser';
850
851     my $prefix = '"';
852     if ($neg) {
853         $prefix =
854             $QueryParser::parser_config{$qp_class}{operators}{disallowed} .
855             $prefix;
856     }
857
858     return {
859         "type" => "atom", "prefix" => $prefix, "suffix" => '"',
860         "content" => $phrase
861     }
862 }
863
864 sub find_arrays_in_abstract {
865     my ($hash) = @_;
866
867     my @arrays;
868     foreach my $key (keys %$hash) {
869         if (ref $hash->{$key} eq "ARRAY") {
870             push @arrays, $hash->{$key};
871             foreach (@{$hash->{$key}}) {
872                 push @arrays, find_arrays_in_abstract($_);
873             }
874         }
875     }
876
877     return @arrays;
878 }
879
880 #-------------------------------
881 package QueryParser::Canonicalize;  # not OO
882
883 sub _abstract_query2str_filter {
884     my $f = shift;
885     my $qp_class = shift || 'QueryParser';
886     my $qpconfig = $QueryParser::parser_config{$qp_class};
887
888     return sprintf(
889         '%s%s(%s)',
890         $f->{negate} ? $qpconfig->{operators}{disallowed} : "",
891         $f->{name},
892         join(",", @{$f->{args}})
893     );
894 }
895
896 sub _abstract_query2str_modifier {
897     my $f = shift;
898     my $qp_class = shift || 'QueryParser';
899     my $qpconfig = $QueryParser::parser_config{$qp_class};
900
901     return $qpconfig->{operators}{modifier} . $f;
902 }
903
904 # This should produce an equivalent query to the original, given an
905 # abstract_query.
906 sub abstract_query2str_impl {
907     my $abstract_query  = shift;
908     my $depth = shift || 0;
909
910     my $qp_class ||= shift || 'QueryParser';
911     my $qpconfig = $QueryParser::parser_config{$qp_class};
912
913     my $gs = $qpconfig->{operators}{group_start};
914     my $ge = $qpconfig->{operators}{group_end};
915     my $and = $qpconfig->{operators}{and};
916     my $or = $qpconfig->{operators}{or};
917
918     my $needs_group = 0;
919     my $q = "";
920
921     if (exists $abstract_query->{type}) {
922         if ($abstract_query->{type} eq 'query_plan') {
923             $q .= join(" ", map { _abstract_query2str_filter($_, $qp_class) } @{$abstract_query->{filters}}) if
924                 exists $abstract_query->{filters};
925             $needs_group += scalar(@{$abstract_query->{filters}}) if exists $abstract_query->{filters};
926
927             $q .= " ";
928
929             $q .= join(" ", map { _abstract_query2str_modifier($_, $qp_class) } @{$abstract_query->{modifiers}}) if
930                 exists $abstract_query->{modifiers};
931             $needs_group += scalar(@{$abstract_query->{modifiers}}) if exists $abstract_query->{modifiers};
932         } elsif ($abstract_query->{type} eq 'node') {
933             if ($abstract_query->{alias}) {
934                 $q .= " " . $abstract_query->{alias};
935                 $q .= "|$_" foreach @{$abstract_query->{alias_fields}};
936             } else {
937                 $q .= " " . $abstract_query->{class};
938                 $q .= "|$_" foreach @{$abstract_query->{fields}};
939             }
940             $q .= ":";
941         } elsif ($abstract_query->{type} eq 'atom') {
942             my $prefix = $abstract_query->{prefix} || '';
943             $prefix = $qpconfig->{operators}{disallowed} if $prefix eq '!';
944             $q .= $prefix .
945                 ($abstract_query->{content} || '') .
946                 ($abstract_query->{suffix} || '');
947             $needs_group += 1;
948         } elsif ($abstract_query->{type} eq 'facet') {
949             # facet syntax [ # ] is hardcoded I guess?
950             my $prefix = $abstract_query->{negate} ? $qpconfig->{operators}{disallowed} : '';
951             $q .= $prefix . $abstract_query->{name} . "[" .
952                 join(" # ", @{$abstract_query->{values}}) . "]";
953             $needs_group += 1;
954         }
955     }
956
957     if (exists $abstract_query->{children}) {
958         my $op = (keys(%{$abstract_query->{children}}))[0];
959         $q .= join(
960             " " . ($op eq '&' ? '' : $or) . " ",
961             map {
962                 abstract_query2str_impl($_, $depth + 1, $qp_class)
963             } @{$abstract_query->{children}{$op}}
964         );
965         $needs_group += scalar(@{$abstract_query->{children}{$op}});
966     } elsif ($abstract_query->{'&'} or $abstract_query->{'|'}) {
967         my $op = (keys(%{$abstract_query}))[0];
968         $q .= join(
969             " " . ($op eq '&' ? $and : $or) . " ",
970             map {
971                 abstract_query2str_impl($_, $depth + 1, $qp_class)
972             } @{$abstract_query->{$op}}
973         );
974         $needs_group += scalar(@{$abstract_query->{$op}});
975     }
976     $q .= " ";
977
978     $q = $gs . $q . $ge if ($needs_group > 1 and $depth);
979
980     return $q;
981 }
982
983 #-------------------------------
984 package QueryParser::query_plan;
985
986 sub QueryParser {
987     my $self = shift;
988     return undef unless ref($self);
989     return $self->{QueryParser};
990 }
991
992 sub new {
993     my $pkg = shift;
994     $pkg = ref($pkg) || $pkg;
995     my %args = (query => [], joiner => '&', @_);
996
997     return bless \%args => $pkg;
998 }
999
1000 sub new_node {
1001     my $self = shift;
1002     my $pkg = ref($self) || $self;
1003     my $node = do{$pkg.'::node'}->new( plan => $self, @_ );
1004     $self->add_node( $node );
1005     return $node;
1006 }
1007
1008 sub new_facet {
1009     my $self = shift;
1010     my $pkg = ref($self) || $self;
1011     my $name = shift;
1012     my $args = shift;
1013     my $negate = shift;
1014
1015     my $node = do{$pkg.'::facet'}->new( plan => $self, name => $name, 'values' => $args, negate => $negate );
1016     $self->add_node( $node );
1017
1018     return $node;
1019 }
1020
1021 sub new_filter {
1022     my $self = shift;
1023     my $pkg = ref($self) || $self;
1024     my $name = shift;
1025     my $args = shift;
1026     my $negate = shift;
1027
1028     my $node = do{$pkg.'::filter'}->new( plan => $self, name => $name, args => $args, negate => $negate );
1029     $self->add_filter( $node );
1030
1031     return $node;
1032 }
1033
1034
1035 sub _merge_filters {
1036     my $left_filter = shift;
1037     my $right_filter = shift;
1038     my $join = shift;
1039
1040     return undef unless $left_filter or $right_filter;
1041     return $right_filter unless $left_filter;
1042     return $left_filter unless $right_filter;
1043
1044     my $args = $left_filter->{args} || [];
1045
1046     if ($join eq '|') {
1047         push(@$args, @{$right_filter->{args}});
1048
1049     } else {
1050         # find the intersect values
1051         my %new_vals;
1052         map { $new_vals{$_} = 1 } @{$right_filter->{args} || []};
1053         $args = [ grep { $new_vals{$_} } @$args ];
1054     }
1055
1056     $left_filter->{args} = $args;
1057     return $left_filter;
1058 }
1059
1060 sub collapse_filters {
1061     my $self = shift;
1062     my $name = shift;
1063
1064     # start by merging any filters at this level.
1065     # like-level filters are always ORed together
1066
1067     my $cur_filter;
1068     my @cur_filters = grep {$_->name eq $name } @{ $self->filters };
1069     if (@cur_filters) {
1070         $cur_filter = shift @cur_filters;
1071         my $args = $cur_filter->{args} || [];
1072         $cur_filter = _merge_filters($cur_filter, $_, '|') for @cur_filters;
1073     }
1074
1075     # next gather the collapsed filters from sub-plans and 
1076     # merge them with our own
1077
1078     my @subquery = @{$self->{query}};
1079
1080     while (@subquery) {
1081         my $blob = shift @subquery;
1082         shift @subquery; # joiner
1083         next unless $blob->isa('QueryParser::query_plan');
1084         my $sub_filter = $blob->collapse_filters($name);
1085         $cur_filter = _merge_filters($cur_filter, $sub_filter, $self->joiner);
1086     }
1087
1088     if ($self->QueryParser->debug) {
1089         my @args = ($cur_filter and $cur_filter->{args}) ? @{$cur_filter->{args}} : ();
1090         warn "collapse_filters($name) => [@args]\n";
1091     }
1092
1093     return $cur_filter;
1094 }
1095
1096 sub find_filter {
1097     my $self = shift;
1098     my $needle = shift;;
1099     return undef unless ($needle);
1100
1101     my $filter = $self->collapse_filters($needle);
1102
1103     warn "find_filter($needle) => " . 
1104         (($filter and $filter->{args}) ? "@{$filter->{args}}" : '[]') . "\n" 
1105         if $self->QueryParser->debug;
1106
1107     return $filter ? ($filter) : ();
1108 }
1109
1110 sub find_modifier {
1111     my $self = shift;
1112     my $needle = shift;;
1113     return undef unless ($needle);
1114     return grep { $_->name eq $needle } @{ $self->modifiers };
1115 }
1116
1117 sub new_modifier {
1118     my $self = shift;
1119     my $pkg = ref($self) || $self;
1120     my $name = shift;
1121
1122     my $node = do{$pkg.'::modifier'}->new( $name );
1123     $self->add_modifier( $node );
1124
1125     return $node;
1126 }
1127
1128 sub classed_node {
1129     my $self = shift;
1130     my $requested_class = shift;
1131
1132     my $node;
1133     for my $n (@{$self->{query}}) {
1134         next unless (ref($n) && $n->isa( 'QueryParser::query_plan::node' ));
1135         if ($n->requested_class eq $requested_class) {
1136             $node = $n;
1137             last;
1138         }
1139     }
1140
1141     if (!$node) {
1142         $node = $self->new_node;
1143         $node->requested_class( $requested_class );
1144     }
1145
1146     return $node;
1147 }
1148
1149 sub remove_last_node {
1150     my $self = shift;
1151     my $requested_class = shift;
1152
1153     my $old = pop(@{$self->query_nodes});
1154     pop(@{$self->query_nodes}) if (@{$self->query_nodes});
1155
1156     return $old;
1157 }
1158
1159 sub query_nodes {
1160     my $self = shift;
1161     return $self->{query};
1162 }
1163
1164 sub add_node {
1165     my $self = shift;
1166     my $node = shift;
1167
1168     $self->{query} ||= [];
1169     push(@{$self->{query}}, $self->joiner) if (@{$self->{query}});
1170     push(@{$self->{query}}, $node);
1171
1172     return $self;
1173 }
1174
1175 sub top_plan {
1176     my $self = shift;
1177
1178     return $self->{level} ? 0 : 1;
1179 }
1180
1181 sub plan_level {
1182     my $self = shift;
1183     return $self->{level};
1184 }
1185
1186 sub joiner {
1187     my $self = shift;
1188     my $joiner = shift;
1189
1190     $self->{joiner} = $joiner if ($joiner);
1191     return $self->{joiner};
1192 }
1193
1194 sub modifiers {
1195     my $self = shift;
1196     $self->{modifiers} ||= [];
1197     return $self->{modifiers};
1198 }
1199
1200 sub add_modifier {
1201     my $self = shift;
1202     my $modifier = shift;
1203
1204     $self->{modifiers} ||= [];
1205     $self->{modifiers} = [ grep {$_->name ne $modifier->name} @{$self->{modifiers}} ];
1206
1207     push(@{$self->{modifiers}}, $modifier);
1208
1209     return $self;
1210 }
1211
1212 sub facets {
1213     my $self = shift;
1214     $self->{facets} ||= [];
1215     return $self->{facets};
1216 }
1217
1218 sub add_facet {
1219     my $self = shift;
1220     my $facet = shift;
1221
1222     $self->{facets} ||= [];
1223     $self->{facets} = [ grep {$_->name ne $facet->name} @{$self->{facets}} ];
1224
1225     push(@{$self->{facets}}, $facet);
1226
1227     return $self;
1228 }
1229
1230 sub filters {
1231     my $self = shift;
1232     $self->{filters} ||= [];
1233     return $self->{filters};
1234 }
1235
1236 sub add_filter {
1237     my $self = shift;
1238     my $filter = shift;
1239
1240     $self->{filters} ||= [];
1241
1242     push(@{$self->{filters}}, $filter);
1243
1244     return $self;
1245 }
1246
1247 # %opts supports two options at this time:
1248 #   no_phrases :
1249 #       If true, do not do anything to the phrases and unphrases
1250 #       fields on any discovered nodes.
1251 #   with_config :
1252 #       If true, also return the query parser config as part of the blob.
1253 #       This will get set back to 0 before recursion to avoid repetition.
1254 sub to_abstract_query {
1255     my $self = shift;
1256     my %opts = @_;
1257
1258     my $pkg = ref $self->QueryParser || $self->QueryParser;
1259
1260     my $abstract_query = {
1261         type => "query_plan",
1262         filters => [map { $_->to_abstract_query } @{$self->filters}],
1263         modifiers => [map { $_->to_abstract_query } @{$self->modifiers}]
1264     };
1265
1266     if ($opts{with_config}) {
1267         $opts{with_config} = 0;
1268         $abstract_query->{config} = $QueryParser::parser_config{$pkg};
1269     }
1270
1271     my $kids = [];
1272
1273     for my $qnode (@{$self->query_nodes}) {
1274         # Remember: qnode can be a joiner string, a node, or another query_plan
1275
1276         if (QueryParser::_util::is_joiner($qnode)) {
1277             if ($abstract_query->{children}) {
1278                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1279                 next if $open_joiner eq $qnode;
1280
1281                 my $oldroot = $abstract_query->{children};
1282                 $kids = [$oldroot];
1283                 $abstract_query->{children} = {$qnode => $kids};
1284             } else {
1285                 $abstract_query->{children} = {$qnode => $kids};
1286             }
1287         } else {
1288             push @$kids, $qnode->to_abstract_query(%opts);
1289         }
1290     }
1291
1292     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1293     return $abstract_query;
1294 }
1295
1296
1297 #-------------------------------
1298 package QueryParser::query_plan::node;
1299 use Data::Dumper;
1300 $Data::Dumper::Indent = 0;
1301
1302 sub new {
1303     my $pkg = shift;
1304     $pkg = ref($pkg) || $pkg;
1305     my %args = @_;
1306
1307     return bless \%args => $pkg;
1308 }
1309
1310 sub new_atom {
1311     my $self = shift;
1312     my $pkg = ref($self) || $self;
1313     return do{$pkg.'::atom'}->new( @_ );
1314 }
1315
1316 sub requested_class { # also split into classname, fields and alias
1317     my $self = shift;
1318     my $class = shift;
1319
1320     if ($class) {
1321         my @afields;
1322         my (undef, $alias) = split '#', $class;
1323         if ($alias) {
1324             $class =~ s/#[^|]+//;
1325             ($alias, @afields) = split '\|', $alias;
1326         }
1327
1328         my @fields = @afields;
1329         my ($class_part, @field_parts) = split '\|', $class;
1330         for my $f (@field_parts) {
1331              push(@fields, $f) unless (grep { $f eq $_ } @fields);
1332         }
1333
1334         $class_part ||= $class;
1335
1336         $self->{requested_class} = $class;
1337         $self->{alias} = $alias if $alias;
1338         $self->{alias_fields} = \@afields if $alias;
1339         $self->{classname} = $class_part;
1340         $self->{fields} = \@fields;
1341     }
1342
1343     return $self->{requested_class};
1344 }
1345
1346 sub plan {
1347     my $self = shift;
1348     my $plan = shift;
1349
1350     $self->{plan} = $plan if ($plan);
1351     return $self->{plan};
1352 }
1353
1354 sub alias {
1355     my $self = shift;
1356     my $alias = shift;
1357
1358     $self->{alias} = $alias if ($alias);
1359     return $self->{alias};
1360 }
1361
1362 sub alias_fields {
1363     my $self = shift;
1364     my $alias = shift;
1365
1366     $self->{alias_fields} = $alias if ($alias);
1367     return $self->{alias_fields};
1368 }
1369
1370 sub classname {
1371     my $self = shift;
1372     my $class = shift;
1373
1374     $self->{classname} = $class if ($class);
1375     return $self->{classname};
1376 }
1377
1378 sub fields {
1379     my $self = shift;
1380     my @fields = @_;
1381
1382     $self->{fields} ||= [];
1383     $self->{fields} = \@fields if (@fields);
1384     return $self->{fields};
1385 }
1386
1387 sub phrases {
1388     my $self = shift;
1389     my @phrases = @_;
1390
1391     $self->{phrases} ||= [];
1392     $self->{phrases} = \@phrases if (@phrases);
1393     return $self->{phrases};
1394 }
1395
1396 sub unphrases {
1397     my $self = shift;
1398     my @phrases = @_;
1399
1400     $self->{unphrases} ||= [];
1401     $self->{unphrases} = \@phrases if (@phrases);
1402     return $self->{unphrases};
1403 }
1404
1405 sub add_phrase {
1406     my $self = shift;
1407     my $phrase = shift;
1408
1409     push(@{$self->phrases}, $phrase);
1410
1411     return $self;
1412 }
1413
1414 sub add_unphrase {
1415     my $self = shift;
1416     my $phrase = shift;
1417
1418     push(@{$self->unphrases}, $phrase);
1419
1420     return $self;
1421 }
1422
1423 sub query_atoms {
1424     my $self = shift;
1425     my @query_atoms = @_;
1426
1427     $self->{query_atoms} ||= [];
1428     $self->{query_atoms} = \@query_atoms if (@query_atoms);
1429     return $self->{query_atoms};
1430 }
1431
1432 sub add_fts_atom {
1433     my $self = shift;
1434     my $atom = shift;
1435
1436     if (!ref($atom)) {
1437         my $content = $atom;
1438         my @parts = @_;
1439
1440         $atom = $self->new_atom( content => $content, @parts );
1441     }
1442
1443     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1444     push(@{$self->query_atoms}, $atom);
1445
1446     return $self;
1447 }
1448
1449 sub add_dummy_atom {
1450     my $self = shift;
1451     my @parts = @_;
1452
1453     my $atom = $self->new_atom( @parts, dummy => 1 );
1454
1455     push(@{$self->query_atoms}, $self->plan->joiner) if (@{$self->query_atoms});
1456     push(@{$self->query_atoms}, $atom);
1457
1458     return $self;
1459 }
1460
1461 # This will find up to one occurence of @$short_list within @$long_list, and
1462 # replace it with the single atom $replacement.
1463 sub replace_phrase_in_abstract_query {
1464     my ($self, $short_list, $long_list, $replacement) = @_;
1465
1466     my $success = 0;
1467     my @already = ();
1468     my $goal = scalar @$short_list;
1469
1470     for (my $i = 0; $i < scalar (@$long_list); $i++) {
1471         my $right = $long_list->[$i];
1472
1473         if (QueryParser::_util::compare_abstract_atoms(
1474             $short_list->[scalar @already], $right
1475         )) {
1476             push @already, $i;
1477         } elsif (scalar @already) {
1478             @already = ();
1479             next;
1480         }
1481
1482         if (scalar @already == $goal) {
1483             splice @$long_list, $already[0], scalar(@already), $replacement;
1484             $success = 1;
1485             last;
1486         }
1487     }
1488
1489     return $success;
1490 }
1491
1492 sub to_abstract_query {
1493     my $self = shift;
1494     my %opts = @_;
1495
1496     my $pkg = ref $self->plan->QueryParser || $self->plan->QueryParser;
1497
1498     my $abstract_query = {
1499         "type" => "node",
1500         "alias" => $self->alias,
1501         "alias_fields" => $self->alias_fields,
1502         "class" => $self->classname,
1503         "fields" => $self->fields
1504     };
1505
1506     my $kids = [];
1507
1508     for my $qatom (@{$self->query_atoms}) {
1509         if (QueryParser::_util::is_joiner($qatom)) {
1510             if ($abstract_query->{children}) {
1511                 my $open_joiner = (keys(%{$abstract_query->{children}}))[0];
1512                 next if $open_joiner eq $qatom;
1513
1514                 my $oldroot = $abstract_query->{children};
1515                 $kids = [$oldroot];
1516                 $abstract_query->{children} = {$qatom => $kids};
1517             } else {
1518                 $abstract_query->{children} = {$qatom => $kids};
1519             }
1520         } else {
1521             push @$kids, $qatom->to_abstract_query;
1522         }
1523     }
1524
1525     if ($self->{phrases} and not $opts{no_phrases}) {
1526         for my $phrase (@{$self->{phrases}}) {
1527             # Phrases appear duplication in a real QP tree, and we don't want
1528             # that duplication in our abstract query.  So for all our phrases,
1529             # break them into atoms as QP would, and remove any matching
1530             # sequences of atoms from our abstract query.
1531
1532             my $tmptree = $self->{plan}->{QueryParser}->new(query => '"'.$phrase.'"')->parse->parse_tree;
1533             if ($tmptree) {
1534                 # For a well-behaved phrase, we should now have only one node
1535                 # in the $tmptree query plan, and that node should have an
1536                 # orderly list of atoms and joiners.
1537
1538                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1539                     my $tmplist;
1540
1541                     eval {
1542                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1543                             no_phrases => 1
1544                         )->{children}->{'&'}->[0]->{children}->{'&'};
1545                     };
1546                     next if $@;
1547
1548                     foreach (
1549                         QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
1550                     ) {
1551                         last if $self->replace_phrase_in_abstract_query(
1552                             $tmplist,
1553                             $_,
1554                             QueryParser::_util::fake_abstract_atom_from_phrase($phrase, undef, $pkg)
1555                         );
1556                     }
1557                 }
1558             }
1559         }
1560     }
1561
1562     # Do the same as the preceding block for unphrases (negated phrases).
1563     if ($self->{unphrases} and not $opts{no_phrases}) {
1564         for my $phrase (@{$self->{unphrases}}) {
1565             my $tmptree = $self->{plan}->{QueryParser}->new(
1566                 query => $QueryParser::parser_config{$pkg}{operators}{disallowed}.
1567                     '"' . $phrase . '"'
1568             )->parse->parse_tree;
1569
1570             if ($tmptree) {
1571                 if ($tmptree->{query} and scalar(@{$tmptree->{query}}) == 1) {
1572                     my $tmplist;
1573
1574                     eval {
1575                         $tmplist = $tmptree->{query}->[0]->to_abstract_query(
1576                             no_phrases => 1
1577                         )->{children}->{'&'}->[0]->{children}->{'&'};
1578                     };
1579                     next if $@;
1580
1581                     foreach (
1582                         QueryParser::_util::find_arrays_in_abstract($abstract_query->{children})
1583                     ) {
1584                         last if $self->replace_phrase_in_abstract_query(
1585                             $tmplist,
1586                             $_,
1587                             QueryParser::_util::fake_abstract_atom_from_phrase($phrase, 1, $pkg)
1588                         );
1589                     }
1590                 }
1591             }
1592         }
1593     }
1594
1595     $abstract_query->{children} ||= { QueryParser::_util::default_joiner() => $kids };
1596     return $abstract_query;
1597 }
1598
1599 #-------------------------------
1600 package QueryParser::query_plan::node::atom;
1601
1602 sub new {
1603     my $pkg = shift;
1604     $pkg = ref($pkg) || $pkg;
1605     my %args = @_;
1606
1607     return bless \%args => $pkg;
1608 }
1609
1610 sub node {
1611     my $self = shift;
1612     return undef unless (ref $self);
1613     return $self->{node};
1614 }
1615
1616 sub content {
1617     my $self = shift;
1618     return undef unless (ref $self);
1619     return $self->{content};
1620 }
1621
1622 sub prefix {
1623     my $self = shift;
1624     return undef unless (ref $self);
1625     return $self->{prefix};
1626 }
1627
1628 sub suffix {
1629     my $self = shift;
1630     return undef unless (ref $self);
1631     return $self->{suffix};
1632 }
1633
1634 sub to_abstract_query {
1635     my ($self) = @_;
1636     
1637     return {
1638         (map { $_ => $self->$_ } qw/prefix suffix content/),
1639         "type" => "atom"
1640     };
1641 }
1642 #-------------------------------
1643 package QueryParser::query_plan::filter;
1644
1645 sub new {
1646     my $pkg = shift;
1647     $pkg = ref($pkg) || $pkg;
1648     my %args = @_;
1649
1650     return bless \%args => $pkg;
1651 }
1652
1653 sub plan {
1654     my $self = shift;
1655     return $self->{plan};
1656 }
1657
1658 sub name {
1659     my $self = shift;
1660     return $self->{name};
1661 }
1662
1663 sub negate {
1664     my $self = shift;
1665     return $self->{negate};
1666 }
1667
1668 sub args {
1669     my $self = shift;
1670     return $self->{args};
1671 }
1672
1673 sub to_abstract_query {
1674     my ($self) = @_;
1675     
1676     return {
1677         map { $_ => $self->$_ } qw/name negate args/
1678     };
1679 }
1680
1681 #-------------------------------
1682 package QueryParser::query_plan::facet;
1683
1684 sub new {
1685     my $pkg = shift;
1686     $pkg = ref($pkg) || $pkg;
1687     my %args = @_;
1688
1689     return bless \%args => $pkg;
1690 }
1691
1692 sub plan {
1693     my $self = shift;
1694     return $self->{plan};
1695 }
1696
1697 sub name {
1698     my $self = shift;
1699     return $self->{name};
1700 }
1701
1702 sub negate {
1703     my $self = shift;
1704     return $self->{negate};
1705 }
1706
1707 sub values {
1708     my $self = shift;
1709     return $self->{'values'};
1710 }
1711
1712 sub to_abstract_query {
1713     my ($self) = @_;
1714
1715     return {
1716         (map { $_ => $self->$_ } qw/name negate values/),
1717         "type" => "facet"
1718     };
1719 }
1720
1721 #-------------------------------
1722 package QueryParser::query_plan::modifier;
1723
1724 sub new {
1725     my $pkg = shift;
1726     $pkg = ref($pkg) || $pkg;
1727     my $modifier = shift;
1728     my $negate = shift;
1729
1730     return bless { name => $modifier, negate => $negate } => $pkg;
1731 }
1732
1733 sub name {
1734     my $self = shift;
1735     return $self->{name};
1736 }
1737
1738 sub negate {
1739     my $self = shift;
1740     return $self->{negate};
1741 }
1742
1743 sub to_abstract_query {
1744     my ($self) = @_;
1745     
1746     return $self->name;
1747 }
1748 1;
1749