]> Sergey Matveev's repositories - public-inbox.git/blob - lib/PublicInbox/SearchThread.pm
thread: remove rootset accessor method
[public-inbox.git] / lib / PublicInbox / SearchThread.pm
1 # This library is free software; you can redistribute it and/or modify
2 # it under the same terms as Perl itself.
3 #
4 # This license differs from the rest of public-inbox
5 #
6 # Our own jwz-style threading class based on Mail::Thread from CPAN.
7 # Mail::Thread is unmaintained and available on some distros.
8 # We also do not want pruning or subject grouping, since we want
9 # to encourage strict threading and hopefully encourage people
10 # to use proper In-Reply-To.
11 #
12 # This includes fixes from several open bugs for Mail::Thread
13 #
14 # Avoid circular references
15 # - https://rt.cpan.org/Public/Bug/Display.html?id=22817
16 #
17 # And avoid recursion in recurse_down:
18 # - https://rt.cpan.org/Ticket/Display.html?id=116727
19 # - http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=833479
20 package PublicInbox::SearchThread;
21 use strict;
22 use warnings;
23
24 sub new {
25         return bless {
26                 messages => $_[1],
27                 id_table => {},
28                 rootset  => []
29         }, $_[0];
30 }
31
32 sub thread {
33         my $self = shift;
34         $self->_setup();
35         $self->{rootset} = [
36                         grep { !$_->{parent} } values %{$self->{id_table}} ];
37         $self->_finish();
38 }
39
40 sub _finish {
41         my $self = shift;
42         delete $self->{id_table};
43         delete $self->{seen};
44 }
45
46 sub _get_cont_for_id ($$) {
47         my ($self, $mid) = @_;
48         $self->{id_table}{$mid} ||= PublicInbox::SearchThread::Msg->new($mid);
49 }
50
51 sub _setup {
52         my ($self) = @_;
53
54         _add_message($self, $_) foreach @{$self->{messages}};
55 }
56
57 sub _add_message ($$) {
58         my ($self, $smsg) = @_;
59
60         # A. if id_table...
61         my $this = _get_cont_for_id($self, $smsg->{mid});
62         $this->{smsg} = $smsg;
63
64         # B. For each element in the message's References field:
65         my $prev;
66         if (defined(my $refs = $smsg->{references})) {
67                 foreach my $ref ($refs =~ m/<([^>]+)>/g) {
68                         # Find a Container object for the given Message-ID
69                         my $cont = _get_cont_for_id($self, $ref);
70
71                         # Link the References field's Containers together in
72                         # the order implied by the References header
73                         #
74                         # * If they are already linked don't change the
75                         #   existing links
76                         # * Do not add a link if adding that link would
77                         #   introduce a loop...
78                         if ($prev &&
79                                 !$cont->{parent} &&  # already linked
80                                 !$cont->has_descendent($prev) # would loop
81                            ) {
82                                 $prev->add_child($cont);
83                         }
84                         $prev = $cont;
85                 }
86         }
87
88         # C. Set the parent of this message to be the last element in
89         # References...
90         if ($prev && !$this->has_descendent($prev)) { # would loop
91                 $prev->add_child($this)
92         }
93 }
94
95 sub order {
96         my $self = shift;
97         my $ordersub = shift;
98
99         # make a fake root
100         my $root = _get_cont_for_id($self, 'fakeroot');
101         $root->add_child( $_ ) for @{ $self->{rootset} };
102
103         # sort it
104         $root->order_children( $ordersub );
105
106         # and untangle it
107         my $kids = $root->children;
108         $self->{rootset} = $kids;
109         $root->remove_child($_) for @$kids;
110 }
111
112 package PublicInbox::SearchThread::Msg;
113 use Carp qw(croak);
114 use Scalar::Util qw(weaken);
115
116 sub new { my $self = shift; bless { id => shift }, $self; }
117
118 sub add_child {
119         my ($self, $child) = @_;
120         croak "Cowardly refusing to become my own parent: $self"
121           if $self == $child;
122
123         if (grep { $_ == $child } @{$self->children}) {
124                 # All is potentially correct with the world
125                 weaken($child->{parent} = $self);
126                 return;
127         }
128
129         my $parent = $child->{parent};
130         remove_child($parent, $child) if $parent;
131
132         $child->{next} = $self->{child};
133         $self->{child} = $child;
134         weaken($child->{parent} = $self);
135 }
136
137 sub remove_child {
138         my ($self, $child) = @_;
139
140         my $x = $self->{child} or return;
141         if ($x == $child) {  # First one's easy.
142                 $self->{child} = $child->{next};
143                 $child->{parent} = $child->{next} = undef;
144                 return;
145         }
146
147         my $prev = $x;
148         while ($x = $x->{next}) {
149                 if ($x == $child) {
150                         $prev->{next} = $x->{next}; # Unlink x
151                         $x->{next} = $x->{parent} = undef; # Deparent it
152                         return;
153                 }
154                 $prev = $x;
155         }
156         # oddly, we can get here
157         $child->{next} = $child->{parent} = undef;
158 }
159
160 sub has_descendent {
161         my $self = shift;
162         my $child = shift;
163         die "Assertion failed: $child" unless eval {$child};
164         my $there = 0;
165         $self->recurse_down(sub { $there = 1 if $_[0] == $child });
166
167         return $there;
168 }
169
170 sub children {
171         my $self = shift;
172         my @children;
173         my $visitor = $self->{child};
174         while ($visitor) {
175                 push @children, $visitor;
176                 $visitor = $visitor->{next};
177         }
178         \@children;
179 }
180
181 sub set_children {
182         my ($self, $children) = @_;
183         my $walk = $self->{child} = shift @$children;
184         do {
185                 $walk = $walk->{next} = shift @$children;
186         } while ($walk);
187 }
188
189 sub order_children {
190         my $self = shift;
191         my $ordersub = shift;
192
193         return unless $ordersub;
194
195         my $sub = sub {
196                 my $cont = shift;
197                 my $children = $cont->children;
198                 return if @$children < 2;
199                 $cont->set_children( $ordersub->( $children ) );
200         };
201         $self->iterate_down( undef, $sub );
202         undef $sub;
203 }
204
205 # non-recursive version of recurse_down to avoid stack depth warnings
206 sub recurse_down {
207         my ($self, $callback) = @_;
208         my %seen;
209         my @q = ($self);
210         while (my $cont = shift @q) {
211                 $seen{$cont}++;
212                 $callback->($cont);
213
214                 if (my $next = $cont->{next}) {
215                         if ($seen{$next}) {
216                                 $cont->{next} = undef;
217                         } else {
218                                 push @q, $next;
219                         }
220                 }
221                 if (my $child = $cont->{child}) {
222                         if ($seen{$child}) {
223                                 $cont->{child} = undef;
224                         } else {
225                                 push @q, $child;
226                         }
227                 }
228         }
229 }
230
231 sub iterate_down {
232         my $self = shift;
233         my ($before, $after) = @_;
234
235         my %seen;
236         my $walk = $self;
237         my $depth = 0;
238         my @visited;
239         while ($walk) {
240                 push @visited, [ $walk, $depth ];
241                 $before->($walk, $depth) if $before;
242
243                 # spot/break loops
244                 $seen{$walk}++;
245
246                 my $child = $walk->{child};
247                 if ($child && $seen{$child}) {
248                         $walk->{child} = $child = undef;
249                 }
250
251                 my $next = $walk->{next};
252                 if ($next && $seen{$next}) {
253                         $walk->{next} = $next = undef;
254                 }
255
256                 # go down, or across
257                 if ($child) {
258                         $next = $child;
259                         ++$depth;
260                 }
261
262                 # no next?  look up
263                 if (!$next) {
264                         my $up = $walk;
265                         while ($up && !$next) {
266                                 $up = $up->{parent};
267                                 --$depth;
268                                 $next = $up->{next} if $up;
269                         }
270                 }
271                 $walk = $next;
272         }
273         return unless $after;
274         while (@visited) { $after->(@{ pop @visited }) }
275 }
276
277 1;