File Coverage

blib/lib/Data/Hierarchy.pm
Criterion Covered Total %
statement 175 179 97.7
branch 64 72 88.8
condition 14 17 82.3
subroutine 22 22 100.0
pod 5 9 55.5
total 280 299 93.6


line stmt bran cond sub pod time code
1             package Data::Hierarchy;
2             $VERSION = '0.34';
3 6     6   171860 use strict;
  6         15  
  6         216  
4 6     6   8224 use Storable qw(dclone);
  6         31149  
  6         17517  
5             # XXX consider using Moose
6              
7             =head1 NAME
8              
9             Data::Hierarchy - Handle data in a hierarchical structure
10              
11             =head1 SYNOPSIS
12              
13             my $tree = Data::Hierarchy->new();
14             $tree->store ('/', {access => 'all'});
15             $tree->store ('/private', {access => 'auth',
16             '.note' => 'this is private});
17              
18             $info = $tree->get ('/private/somewhere/deep');
19              
20             # return actual data points in list context
21             ($info, @fromwhere) = $tree->get ('/private/somewhere/deep');
22              
23             my @items = $tree->find ('/', {access => qr/.*/});
24              
25             # override all children
26             $tree->store ('/', {'.note' => undef}, {override_sticky_descendents => 1});
27              
28             =head1 DESCRIPTION
29              
30             L provides a simple interface for manipulating
31             inheritable data attached to a hierarchical environment (like
32             a filesystem).
33              
34             One use of L is to allow an application to annotate
35             paths in a real filesystem in a single compact data
36             structure. However, the hierarchy does not actually need to correspond
37             to an actual filesystem.
38              
39             Paths in a hierarchy are referred to in a Unix-like syntax; C<"/"> is
40             the root "directory". (You can specify a different separator character
41             than the slash when you construct a Data::Hierarchy object.) With the
42             exception of the root path, paths should never contain trailing
43             slashes. You can associate properties, which are arbitrary name/value
44             pairs, with any path. (Properties cannot contain the undefined value.)
45             By default, properties are inherited by child
46             paths: thus, if you store some data at C:
47              
48             $tree->store('/some/path', {color => 'red'});
49              
50             you can fetch it again at a C:
51              
52             print $tree->get('/some/path/below/that')->{'color'};
53             # prints red
54              
55             On the other hand, properties whose names begin with dots are
56             uninherited, or "sticky":
57              
58             $tree->store('/some/path', {'.color' => 'blue'});
59             print $tree->get('/some/path')->{'.color'}; # prints blue
60             print $tree->get('/some/path/below/that')->{'.color'}; # undefined
61              
62             Note that you do not need to (and in fact, cannot) explicitly add
63             "files" or "directories" to the hierarchy; you simply add and delete
64             properties to paths.
65              
66             =cut
67              
68             =head1 CONSTRUCTOR
69              
70             Creates a new hierarchy object. Takes the following options:
71              
72             =over
73              
74             =item sep
75              
76             The string used as a separator between path levels. Defaults to '/'.
77              
78             =back
79              
80             =cut
81              
82             sub new {
83 66     66 0 48762 my $class = shift;
84 66         273 my %args = (
85             sep => '/',
86             @_);
87              
88 66         225 my $self = bless {}, $class;
89 66         196 $self->{sep} = $args{sep};
90 66         144 $self->{hash} = {};
91 66         138 $self->{sticky} = {};
92 66         218 return $self;
93             }
94              
95             =head1 METHODS
96              
97             =head2 Instance Methods
98              
99             =over
100              
101             =cut
102              
103             =item C
104              
105             Given a path and a hash reference of properties, stores the properties
106             at the path.
107              
108             Unless the C option is given with a false value,
109             it eliminates any non-sticky property in a descendent of C<$path> with
110             the same name.
111              
112             If the C option is given with a true
113             value, it eliminates any sticky property in a descendent of C<$path>
114             with the same name. override it.
115              
116             A value of undef removes that value; note, though, that
117             if an ancestor of C<$path> defines that property, the ancestor's value
118             will be inherited there; that is, with:
119              
120             $t->store('/a', {k => 'top'});
121             $t->store('/a/b', {k => 'bottom'});
122             $t->store('/a/b', {k => undef});
123             print $t->get('/a/b')->{'k'};
124              
125             it will print 'top'.
126              
127             =cut
128              
129             sub store {
130 197     197 1 1475 my $self = shift;
131 197         444 $self->_store_no_cleanup(@_);
132 197         532 $self->_remove_redundant_properties_and_undefs($_[0]);
133             }
134              
135             # Internal method.
136             #
137             # Does everything that store does, except for the cleanup at the
138             # end (appropriate for use in e.g. merge, which calls this a bunch of
139             # times and then does cleanup at the end).
140              
141             sub _store_no_cleanup {
142 205     205   245 my $self = shift;
143 205         435 my $path = shift;
144 205         241 my $props = shift;
145 205   100     755 my $opts = shift || {};
146              
147 205         840 $self->_path_safe ($path);
148              
149 205         754 my %args = (
150             override_descendents => 1,
151             override_sticky_descendents => 0,
152             %$opts);
153              
154 205 100       786 $self->_remove_matching_properties_recursively($path, $props, $self->{hash})
155             if $args{override_descendents};
156 205 100       466 $self->_remove_matching_properties_recursively($path, $props, $self->{sticky})
157             if $args{override_sticky_descendents};
158 205         437 $self->_store ($path, $props);
159             }
160              
161             =item C
162              
163             Given a path, looks up all of the properteies (sticky and not) and
164             returns them in a hash reference. The values are clones, unless you
165             pass a true value for C<$dont_clone>.
166              
167             If called in list context, returns that hash reference followed by all
168             of the ancestral paths of C<$path> which contain non-sticky properties
169             (possibly including itself).
170              
171             =cut
172              
173             sub get {
174 453     453 1 1351 my ($self, $path, $dont_clone) = @_;
175 453         991 $self->_path_safe ($path);
176 453         620 my $value = {};
177              
178 453         1229 my @datapoints = $self->_ancestors($self->{hash}, $path);
179              
180 453         842 for (@datapoints) {
181 334         601 my $newv = $self->{hash}{$_};
182 334 100       5184 $newv = dclone $newv unless $dont_clone;
183 334         1959 $value = {%$value, %$newv};
184             }
185 453 100       1240 if (exists $self->{sticky}{$path}) {
186 194         322 my $newv = $self->{sticky}{$path};
187 194 100       1673 $newv = dclone $newv unless $dont_clone;
188 194         920 $value = {%$value, %$newv}
189             }
190 453 100       1938 return wantarray ? ($value, @datapoints) : $value;
191             }
192              
193             =item C
194              
195             Given a path and a hash reference of name/regular expression pairs,
196             returns a list of all paths which are descendents of C<$path>
197             (including itself) and define B (not inherited)
198             all of the properties in the hash with values matching the given
199             regular expressions. (You may want to use C to merely see if
200             it has any value defined there.) Properties can be sticky or not.
201              
202             =cut
203              
204             sub find {
205 2     2 1 7 my ($self, $path, $prop_regexps) = @_;
206 2         7 $self->_path_safe ($path);
207 2         4 my @items;
208 2         11 my @datapoints = $self->_all_descendents($path);
209              
210 2         6 for my $subpath (@datapoints) {
211 8         10 my $matched = 1;
212 8         18 for (keys %$prop_regexps) {
213 8 50       26 my $lookat = (index($_, '.') == 0) ?
214             $self->{sticky}{$subpath} : $self->{hash}{$subpath};
215 8 100 66     62 $matched = 0
216             unless exists $lookat->{$_}
217             && $lookat->{$_} =~ m/$prop_regexps->{$_}/;
218 8 100       24 last unless $matched;
219             }
220 8 100       24 push @items, $subpath
221             if $matched;
222             }
223 2         19 return @items;
224             }
225              
226             =item C
227              
228             Given a second L object and a path, copies all the
229             properties from the other object at C<$path> or below into the
230             corresponding paths in the object this method is invoked on. All
231             properties from the object this is invoked on at C<$path> or below are
232             erased first.
233              
234             =cut
235              
236             sub merge {
237 3     3 1 15 my ($self, $other, $path) = @_;
238 3         12 $self->_path_safe ($path);
239              
240 3         14 my %datapoints = map {$_ => 1} ($self->_all_descendents ($path),
  9         27  
241             $other->_all_descendents ($path));
242 3         16 for my $datapoint (sort keys %datapoints) {
243 8         28 my $my_props = $self->get ($datapoint, 1);
244 8         30 my $other_props = $other->get ($datapoint);
245 8         25 for (keys %$my_props) {
246 18 100       52 $other_props->{$_} = undef
247             unless defined $other_props->{$_};
248             }
249 8         29 $self->_store_no_cleanup ($datapoint, $other_props);
250             }
251              
252 3         14 $self->_remove_redundant_properties_and_undefs;
253             }
254              
255             =item C
256              
257             Given a path which B element of the hierarchy must be contained
258             in, returns a special Data::Hierarchy::Relative object which
259             represents the hierarchy relative that path. The B thing you can
260             do with a Data::Hierarchy::Relative object is call
261             C on it, which returns a new
262             L object at that base path. For example, if
263             everything in the hierarchy is rooted at C and it
264             needs to be moved to C, you can do
265              
266             $hierarchy = $hierarchy->to_relative('/home/super_project')->to_absolute('/home/awesome_project');
267              
268             (Data::Hierarchy::Relative objects may be a more convenient
269             serialization format than Data::Hierarchy objects, if they are
270             tracking the state of some relocatable resource.)
271              
272             =cut
273              
274             sub to_relative {
275 2     2 1 93 my $self = shift;
276 2         5 my $base_path = shift;
277              
278 2         16 return Data::Hierarchy::Relative->new($base_path, %$self);
279             }
280              
281             # Internal method.
282             #
283             # Dies if the given path has a trailing slash and is not the root. If it is root,
284             # destructively changes the path given as argument to the empty string.
285              
286             sub _path_safe {
287             # Have to do this explicitly on the elements of @_ in order to be destructive
288 671 100   671   1785 if ($_[1] eq $_[0]->{sep}) {
289 6         12 $_[1] = '';
290 6         10 return;
291             }
292              
293 665         955 my $self = shift;
294 665         765 my $path = shift;
295              
296 665         1080 my $location_of_last_separator = rindex($path, $self->{sep});
297 665 100       1336 return if $location_of_last_separator == -1;
298              
299 469         733 my $potential_location_of_trailing_separator = (length $path) - (length $self->{sep});
300              
301 469 50       1148 return unless $location_of_last_separator == $potential_location_of_trailing_separator;
302              
303 0         0 require Carp;
304 0         0 Carp::confess('non-root path has a trailing slash!');
305             }
306              
307             # Internal method.
308             #
309             # Actually does property updates (to hash or sticky, depending on name).
310              
311             sub _store {
312 205     205   304 my ($self, $path, $new_props) = @_;
313              
314 205 100       510 my $old_props = exists $self->{hash}{$path} ? $self->{hash}{$path} : undef;
315 205 100       224 my $merged_props = {%{$old_props||{}}, %$new_props};
  205         1149  
316 205         652 for (keys %$merged_props) {
317 371 100       750 if (index($_, '.') == 0) {
318 140 100       514 defined $merged_props->{$_} ?
319             $self->{sticky}{$path}{$_} = $merged_props->{$_} :
320             delete $self->{sticky}{$path}{$_};
321 140         339 delete $merged_props->{$_};
322             }
323             else {
324 231 100       671 delete $merged_props->{$_}
325             unless defined $merged_props->{$_};
326             }
327             }
328              
329 205         884 $self->{hash}{$path} = $merged_props;
330             }
331              
332             # Internal method.
333             #
334             # Given a hash (probably $self->{hash}, $self->{sticky}, or their union),
335             # returns a sorted list of the paths with data that are ancestors of the given
336             # path (including it itself).
337              
338             sub _ancestors {
339 453     453   627 my ($self, $hash, $path) = @_;
340              
341 453         426 my @ancestors;
342 453 100       929 push @ancestors, '' if exists $hash->{''};
343              
344             # Special case the root.
345 453 100       1060 return @ancestors if $path eq '';
346              
347 259         1579 my @parts = split m{\Q$self->{sep}}, $path;
348             # Remove empty string at the front.
349 259         377 my $current = '';
350 259 50       581 unless (length $parts[0]) {
351 259         290 shift @parts;
352 259         478 $current .= $self->{sep};
353             }
354              
355 259         417 for my $part (@parts) {
356 364         582 $current .= $part;
357 364 100       871 push @ancestors, $current if exists $hash->{$current};
358 364         910 $current .= $self->{sep};
359             }
360              
361             # XXX: could build cached pointer for fast traversal
362 259         1014 return @ancestors;
363             }
364              
365             # Internal method.
366             #
367             # Given a hash (probably $self->{hash}, $self->{sticky}, or their union),
368             # returns a sorted list of the paths with data that are descendents of the given
369             # path (including it itself).
370              
371             sub _descendents {
372 214     214   302 my ($self, $hash, $path) = @_;
373              
374             # If finding for everything, don't bother grepping
375 214 100       475 return sort keys %$hash unless length($path);
376              
377 206         499 return sort grep {index($_.$self->{sep}, $path.$self->{sep}) == 0}
  218         928  
378             keys %$hash;
379             }
380              
381             # Internal method.
382             #
383             # Returns a sorted list of all of the paths which currently have any
384             # properties (sticky or not) that are descendents of the given path
385             # (including it itself).
386             #
387             # (Note that an arg of "/f" can return entries "/f" and "/f/g" but not
388             # "/foo".)
389              
390             sub _all_descendents {
391 8     8   17 my ($self, $path) = @_;
392 8         15 $self->_path_safe ($path);
393              
394 8 50       9 my $both = {%{$self->{hash}}, %{$self->{sticky} || {}}};
  8         25  
  8         46  
395              
396 8         26 return $self->_descendents($both, $path);
397             }
398              
399             # Internal method.
400             #
401             # Given a path, a hash reference of properties, and a hash reference
402             # (presumably {hash} or {sticky}), removes all properties from the
403             # hash at the path or its descendents with the same name as a name in
404             # the given property hash. (The values in the property hash are
405             # ignored.)
406              
407             sub _remove_matching_properties_recursively {
408 206     206   329 my ($self, $path, $remove_props, $hash) = @_;
409              
410 206         412 my @datapoints = $self->_descendents ($hash, $path);
411              
412 206         490 for my $datapoint (@datapoints) {
413 113         418 delete $hash->{$datapoint}{$_} for keys %$remove_props;
414 113 100       153 delete $hash->{$datapoint} unless %{$hash->{$datapoint}};
  113         446  
415             }
416             }
417              
418             # Internal method.
419             #
420             # Returns the parent of a path; this is a purely textual operation, and is not necessarily a datapoint.
421             # Do not pass in the root.
422              
423             sub _parent {
424 287     287   536 my $self = shift;
425 287         316 my $path = shift;
426              
427 287 50 33     1246 return if $path eq q{} or $path eq $self->{sep};
428              
429             # For example, say $path is "/foo/bar/baz";
430             # then $last_separator is 8.
431 287         439 my $last_separator = rindex($path, $self->{sep});
432              
433             # This happens if a path is passed in without a leading
434             # slash. This is really a bug, but old version of
435             # SVK::Editor::Status did this, and we might as well make it not
436             # throw unintialized value errors, since it works otherwise. At
437             # some point in the future this should be changed to a plain
438             # "return" or an exception.
439 287 50       521 return '' if $last_separator == -1;
440              
441 287         935 return substr($path, 0, $last_separator);
442             }
443              
444             # Internal method.
445             #
446             # Cleans up the hash and sticky by removing redundant properties,
447             # undef properties, and empty property hashes.
448              
449             sub _remove_redundant_properties_and_undefs {
450 200     200   240 my $self = shift;
451 200         241 my $prefix = shift;
452             # This is not necessarily the most efficient way to implement this
453             # cleanup, but that can be fixed later.
454              
455             # By sorting the keys, we guarantee that we never get to a path
456             # before we've dealt with all of its ancestors.
457 200         800 for my $path (sort keys %{$self->{hash}}) {
  200         687  
458 335 100 100     1819 next if $prefix && index($prefix.$self->{sep}, $path.$self->{sep}) != 0;
459 299         464 my $props = $self->{hash}{$path};
460              
461             # First check for undefs.
462 299         550 for my $name (keys %$props) {
463 315 50       985 if (not defined $props->{$name}) {
464 0         0 delete $props->{$name};
465             }
466             }
467              
468             # Now check for redundancy.
469              
470             # The root can't be redundant.
471 299 100       710 if (length $path) {
472 287         2448 my $parent = $self->_parent($path);
473              
474 287         604 my $parent_props = $self->get($parent, 1);
475              
476 287         837 for my $name (keys %$props) {
477             # We've already dealt with undefs in $props, so we
478             # don't need to check that for defined.
479 301 100 100     1643 if (defined $parent_props->{$name} and
480             $props->{$name} eq $parent_props->{$name}) {
481 15         47 delete $props->{$name};
482             }
483             }
484             }
485              
486             # Clean up empty property hashes.
487 299 100       756 delete $self->{hash}{$path} unless %{ $self->{hash}{$path} };
  299         1547  
488             }
489              
490 200         312 for my $path (sort keys %{$self->{sticky}}) {
  200         796  
491             # We only have to remove undefs from sticky, since there is no
492             # inheritance.
493 295         437 my $props = $self->{sticky}{$path};
494              
495 295         659 for my $name (keys %$props) {
496 289 50       767 if (not defined $props->{$name}) {
497 0         0 delete $props->{$name};
498             }
499             }
500              
501             # Clean up empty property hashes.
502 295 100       362 delete $self->{sticky}{$path} unless %{ $self->{sticky}{$path} };
  295         1390  
503             }
504             }
505              
506             # These are for backwards compatibility only.
507              
508 9     9 0 55 sub store_recursively { my $self = shift; $self->store(@_, {override_sticky_descendents => 1}); }
  9         33  
509 9     9 0 52 sub store_fast { my $self = shift; $self->store(@_, {override_descendents => 0}); }
  9         31  
510 3     3 0 41 sub store_override { my $self = shift; $self->store(@_, {override_descendents => 0}); }
  3         10  
511              
512             package Data::Hierarchy::Relative;
513              
514             sub new {
515 2     2   4 my $class = shift;
516 2         14 my $base_path = shift;
517              
518 2         7 my %args = @_;
519              
520 2         10 my $self = bless { sep => $args{sep} }, $class;
521              
522 2         5 my $base_length = length $base_path;
523              
524 2         4 for my $item (qw/hash sticky/) {
525 3         4 my $original = $args{$item};
526 3         6 my $result = {};
527              
528 3         18 for my $path (sort keys %$original) {
529 4 100 100     361 unless ($path eq $base_path or index($path, $base_path . $self->{sep}) == 0) {
530 1         10 require Carp;
531 1         34 Carp::confess("$path is not a child of $base_path");
532             }
533 3         6 my $relative_path = substr($path, $base_length);
534 3         28 $result->{$relative_path} = $original->{$path};
535             }
536              
537 2         8 $self->{$item} = $result;
538             }
539              
540 1         6 return $self;
541             }
542              
543             sub to_absolute {
544 1     1   7 my $self = shift;
545 1         2 my $base_path = shift;
546              
547 1         3 my $tree = { sep => $self->{sep} };
548              
549 1         2 for my $item (qw/hash sticky/) {
550 2         4 my $original = $self->{$item};
551 2         3 my $result = {};
552              
553 2         6 for my $path (keys %$original) {
554 3         8 $result->{$base_path . $path} = $original->{$path};
555             }
556              
557 2         5 $tree->{$item} = $result;
558             }
559              
560 1         4 bless $tree, 'Data::Hierarchy';
561              
562 1         2 return $tree;
563             }
564              
565             1;
566              
567             =back
568              
569             =head1 AUTHORS
570              
571             Chia-liang Kao Eclkao@clkao.orgE
572             David Glasser Eglasser@mit.eduE
573              
574             =head1 COPYRIGHT
575              
576             Copyright 2003-2006 by Chia-liang Kao Eclkao@clkao.orgE.
577              
578             This program is free software; you can redistribute it and/or modify it
579             under the same terms as Perl itself.
580              
581             See L
582              
583             =cut