File Coverage

blib/lib/Net/LDAP/Class/Iterator.pm
Criterion Covered Total %
statement 74 91 81.3
branch 19 32 59.3
condition 3 5 60.0
subroutine 16 16 100.0
pod 5 5 100.0
total 117 149 78.5


line stmt bran cond sub pod time code
1             package Net::LDAP::Class::Iterator;
2 10     10   67 use strict;
  10         25  
  10         18851  
3 10     10   78 use warnings;
  10         18  
  10         495  
4 10     10   105 use base qw( Rose::Object );
  10         24  
  10         1151  
5 10     10   66 use Carp;
  10         20  
  10         875  
6 10     10   61 use Data::Dump qw( dump );
  10         25  
  10         583  
7             use Net::LDAP::Class::MethodMaker (
8 10     10   57 'scalar' => [qw( ldap base_dn page_size filter class )], );
  10         44  
  10         115  
9 10     10   16667 use Net::LDAP::Control::Paged;
  10         25  
  10         321  
10 10     10   62 use Net::LDAP::Constant qw( LDAP_CONTROL_PAGED );
  10         22  
  10         8306883  
11              
12             our $VERSION = '0.26';
13              
14             =head1 NAME
15              
16             Net::LDAP::Class::Iterator - iterate over Net::LDAP::Class objects
17              
18             =head1 SYNOPSIS
19              
20             my $iterator = $user->groups_iterator;
21             while ( my $group = $iterator->next ) {
22             # $group isa Net::LDAP::Class::Group
23             }
24             printf("%d groups found\n", $iterator->count);
25              
26             =head1 DESCRIPTION
27              
28             Net::LDAP::Class::Iterator handles paged searching using Net::LDAP::Control::Paged.
29              
30             =head1 ITERATORS vs ARRAYS
31              
32             Many of the relationship methods in Net::LDAP::Class get and set arrays
33             or array refs of related objects. For small (<1000) data sets arrays are just
34             fine but as data sets scale, different techniques become necessary. An iterator
35             has a big resource advantage over an array: instead of holding all the related
36             objects in memory at once, as an array does, an iterator reads one
37             object at a time from the LDAP server.
38              
39             For example, if you want to look at all the users who are members of a group,
40             and the number of users is large (>1000), some LDAP servers (Active Directory
41             in particular) won't return all of your user objects in a single query. Instead,
42             the results must be paged using Net::LDAP::Control::Paged. You'll see the
43             evidence of this if you call the following code against Active Directory
44             with a group of more than 1000 users.
45              
46             my $group = MyADGroup->new( cn => 'myBigGroup', ldap => $ldap )->read;
47             my $users = $group->users; # bitten by AD! returns an empty array ref!
48             foreach my $user (@$users) {
49             # nothing here -- the array is empty
50             }
51              
52             The call to $group->users returns an empty array because Active Directory
53             refuses to return more than 1000 results at a time. (NOTE the number 1000
54             is the default maximum; your server may be configured differently.)
55              
56             So an iterator to the rescue!
57              
58             my $users = $group->users_iterator;
59             while ( my $user = $users->next ) {
60             # do something with $user
61             }
62             printf("We saw %d users in group %s\n", $users->count, $group->name);
63              
64             You might ask, why bother with arrays at all if iterators are so great.
65             The answer is convenience.
66             For small data sets, arrays are convenient,
67             especially if you intend to do things with subsets of them at a time.
68             Of course, you could do this:
69              
70             my $users = $group->users_iterator;
71             my @allusers;
72             while ( my $user = $users->next ) {
73             push @allusers, $user;
74             }
75            
76             But then you've negated one of the advantages of the iterator: it is
77             less resource-intensive. But hey, if you've got the memory, honey,
78             Perl's got the time.
79              
80             =head1 METHODS
81              
82             =head2 ldap
83              
84             Accessor for Net::LDAP object. Set in new().
85              
86             =head2 base_dn
87              
88             Required in new().
89              
90             =head2 page_size
91              
92             The size of the Net::LDAP::Control::Paged set. Default is 500. Set in new().
93              
94             =head2 filter
95              
96             The search filter to use. Set in new().
97              
98             =head2 class
99              
100             The class to bless results into. Set in new().
101              
102             =head2 init
103              
104             Checks that alll required params are defined and sets up the pager.
105              
106             =cut
107              
108             sub init {
109 56     56 1 878 my $self = shift;
110 56         312 $self->SUPER::init(@_);
111 56 50       2238 if ( !$self->class ) {
112 0         0 croak "class param required";
113             }
114 56   100     403 my $page_size = $self->page_size || 500;
115 56         646 $self->{_page} = Net::LDAP::Control::Paged->new( size => $page_size, );
116 56         4167 $self->{_count} = 0;
117 56         154 $self->{_cookie} = undef;
118 56         230 $self->_do_search();
119 56         303 return $self;
120             }
121              
122             sub _cookie_check {
123 52     52   121 my $self = shift;
124 52 50       379 my ($resp) = $self->{_ldap_search}->control(LDAP_CONTROL_PAGED)
125             or croak "failed to get PAGED control response";
126              
127 52         7298438 $self->{_cookie} = $resp->cookie;
128 52 50       16017 if ( !$self->{_cookie} ) {
129 0         0 return;
130             }
131              
132             # Set cookie in paged control
133 52         638 $self->{_page}->cookie( $self->{_cookie} );
134              
135 52         707 return $self->{_cookie};
136             }
137              
138             sub _do_search {
139 108     108   355 my $self = shift;
140              
141             # execute the search, stashing the search object
142 108 50       586 my $filter = $self->filter or croak "filter required";
143 108   33     472 my $base_dn = $self->base_dn || $self->class->metadata->base_dn;
144 108         705 my $attributes = $self->class->metadata->attributes;
145 108         817 my @args = (
146             'base' => $base_dn,
147             'filter' => $filter,
148             'attrs' => $attributes,
149             'control' => [ $self->{_page} ],
150             );
151 108 50       543 my $ldap = $self->ldap or croak "need Net::LDAP object";
152              
153             #warn "$self->{_count} : _do_search with args: " . dump( \@args ) . "\n";
154              
155 108         641 my $ldap_search = $ldap->search(@args);
156              
157 108 50       35066610 if ( !$ldap_search ) {
158              
159             # be nice to the server and stop the search
160             # if we still have a cookie
161 0 0       0 if ( $self->{_cookie} ) {
162 0         0 $self->{_page}->size(0);
163 0         0 $self->{_page}->cookie( $self->{_cookie} );
164 0         0 $ldap->search(@args);
165 0         0 croak "LDAP seach ended prematurely.";
166             }
167              
168 0         0 $self->{_exhausted} = 1;
169 0         0 return; # no more entries
170              
171             }
172              
173             # fatal on search error
174 108 50       854 croak "error searching ldap: ",
175             Net::LDAP::Class->get_ldap_error($ldap_search)
176             if ( $ldap_search->code );
177              
178 108         1605 $self->{_current_set} = $ldap_search->count;
179 108         3420 $self->{_ldap_search} = $ldap_search;
180              
181             # if we found nothing.
182 108 100       1439 if ( !$self->{_current_set} ) {
183 56         149 $self->{_exhausted} = 1;
184 56         672 return;
185             }
186              
187 52         431 return $self;
188             }
189              
190             =head2 count
191              
192             Returns the number of iterations performed.
193              
194             =cut
195              
196             sub count {
197 1     1 1 7 return shift->{_count};
198             }
199              
200             =head2 is_exhausted
201              
202             Returns true (1) if all the results for this iterator have
203             been seen, false (0) otherwise.
204              
205             =cut
206              
207             sub is_exhausted {
208 180     180 1 4267 return shift->{_exhausted};
209             }
210              
211             =head2 next
212              
213             Returns the next Net::LDAP::Class object from the pager. Returns
214             undef if no more results are found.
215              
216             =cut
217              
218             sub next {
219 341     341 1 3983 my $self = shift;
220              
221 341 100       1050 return undef if $self->{_exhausted};
222              
223 330         1303 my $ldap_entry = $self->{_ldap_search}->shift_entry;
224              
225 330 100       15876 if ( !defined $ldap_entry ) {
226              
227             #warn "no ldap_entry ... trying next page";
228              
229             # handle next search page
230 51         263 $self->_cookie_check;
231              
232             # if there is no cookie, this was the last page.
233 51 50       238 if ( !$self->{_cookie} ) {
234 0         0 $self->{_exhausted} = 1;
235 0         0 return undef;
236             }
237              
238 51 100       194 $self->_do_search or return undef;
239 12         79 $ldap_entry = $self->{_ldap_search}->shift_entry;
240 12 50       624 if ( !$ldap_entry ) {
241 0         0 $self->{_exhausted} = 1;
242 0         0 return undef;
243             }
244             }
245              
246 291         558 $self->{_count}++;
247              
248 291         2132 return $self->class->new(
249             ldap => $self->ldap,
250             ldap_entry => $ldap_entry
251             );
252              
253             }
254              
255             =head2 finish
256              
257             Tell the server you're done iterating over results.
258             This method is only necessary if you stop before exhausting
259             all the results.
260              
261             =cut
262              
263             sub finish {
264 1     1 1 2 my $self = shift;
265 1         7 $self->{_page}->size(0);
266 1         11 $self->_cookie_check;
267 1 50       3 if ( !$self->_do_search() ) {
268 1         8 return 1;
269             }
270             else {
271 0         0 return 0;
272             }
273             }
274              
275             sub DESTROY {
276 56     56   2186 my $self = shift;
277 56 50       218 if ( !$self->is_exhausted ) {
278 0           carp("non-exhausted iterator DESTROY'd");
279 0           Data::Dump::dump($self);
280 0           $self->finish();
281             }
282             }
283              
284             1;
285              
286             __END__