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   34 use strict;
  10         12  
  10         214  
3 10     10   28 use warnings;
  10         12  
  10         231  
4 10     10   33 use base qw( Rose::Object );
  10         11  
  10         572  
5 10     10   39 use Carp;
  10         10  
  10         464  
6 10     10   38 use Data::Dump qw( dump );
  10         12  
  10         446  
7             use Net::LDAP::Class::MethodMaker (
8 10     10   32 'scalar' => [qw( ldap base_dn page_size filter class )], );
  10         16  
  10         55  
9 10     10   6092 use Net::LDAP::Control::Paged;
  10         11  
  10         200  
10 10     10   28 use Net::LDAP::Constant qw( LDAP_CONTROL_PAGED );
  10         14  
  10         5376  
11              
12             our $VERSION = '0.27';
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 443 my $self = shift;
110 56         198 $self->SUPER::init(@_);
111 56 50       1034 if ( !$self->class ) {
112 0         0 croak "class param required";
113             }
114 56   100     260 my $page_size = $self->page_size || 500;
115 56         328 $self->{_page} = Net::LDAP::Control::Paged->new( size => $page_size, );
116 56         1810 $self->{_count} = 0;
117 56         91 $self->{_cookie} = undef;
118 56         161 $self->_do_search();
119 56         172 return $self;
120             }
121              
122             sub _cookie_check {
123 52     52   79 my $self = shift;
124 52 50       214 my ($resp) = $self->{_ldap_search}->control(LDAP_CONTROL_PAGED)
125             or croak "failed to get PAGED control response";
126              
127 52         7594 $self->{_cookie} = $resp->cookie;
128 52 50       5111 if ( !$self->{_cookie} ) {
129 0         0 return;
130             }
131              
132             # Set cookie in paged control
133 52         156 $self->{_page}->cookie( $self->{_cookie} );
134              
135 52         360 return $self->{_cookie};
136             }
137              
138             sub _do_search {
139 108     108   129 my $self = shift;
140              
141             # execute the search, stashing the search object
142 108 50       333 my $filter = $self->filter or croak "filter required";
143 108   33     297 my $base_dn = $self->base_dn || $self->class->metadata->base_dn;
144 108         362 my $attributes = $self->class->metadata->attributes;
145             my @args = (
146             'base' => $base_dn,
147             'filter' => $filter,
148             'attrs' => $attributes,
149 108         433 'control' => [ $self->{_page} ],
150             );
151 108 50       272 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         313 my $ldap_search = $ldap->search(@args);
156              
157 108 50       3040699 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       352 croak "error searching ldap: ",
175             Net::LDAP::Class->get_ldap_error($ldap_search)
176             if ( $ldap_search->code );
177              
178 108         846 $self->{_current_set} = $ldap_search->count;
179 108         1042 $self->{_ldap_search} = $ldap_search;
180              
181             # if we found nothing.
182 108 100       668 if ( !$self->{_current_set} ) {
183 56         163 $self->{_exhausted} = 1;
184 56         278 return;
185             }
186              
187 52         229 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 5 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 1620 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 944 my $self = shift;
220              
221 341 100       580 return undef if $self->{_exhausted};
222              
223 330         651 my $ldap_entry = $self->{_ldap_search}->shift_entry;
224              
225 330 100       7363 if ( !defined $ldap_entry ) {
226              
227             #warn "no ldap_entry ... trying next page";
228              
229             # handle next search page
230 51         139 $self->_cookie_check;
231              
232             # if there is no cookie, this was the last page.
233 51 50       118 if ( !$self->{_cookie} ) {
234 0         0 $self->{_exhausted} = 1;
235 0         0 return undef;
236             }
237              
238 51 100       121 $self->_do_search or return undef;
239 12         45 $ldap_entry = $self->{_ldap_search}->shift_entry;
240 12 50       341 if ( !$ldap_entry ) {
241 0         0 $self->{_exhausted} = 1;
242 0         0 return undef;
243             }
244             }
245              
246 291         292 $self->{_count}++;
247              
248 291         1089 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         5 $self->{_page}->size(0);
266 1         11 $self->_cookie_check;
267 1 50       3 if ( !$self->_do_search() ) {
268 1         4 return 1;
269             }
270             else {
271 0         0 return 0;
272             }
273             }
274              
275             sub DESTROY {
276 56     56   1717 my $self = shift;
277 56 50       132 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__