File Coverage

blib/lib/Data/Toolkit/Map.pm
Criterion Covered Total %
statement 109 126 86.5
branch 66 110 60.0
condition n/a
subroutine 14 16 87.5
pod 9 10 90.0
total 198 262 75.5


line stmt bran cond sub pod time code
1             #!/usr/bin/perl -w
2             #
3             # Data::Toolkit::Map
4             #
5             # Andrew Findlay
6             # Nov 2006
7             # andrew.findlay@skills-1st.co.uk
8             #
9             # $Id: Map.pm 388 2013-08-30 15:19:23Z remotesvn $
10              
11             package Data::Toolkit::Map;
12              
13 4     4   4152 use strict;
  4         11  
  4         214  
14 4     4   21 use Data::Dumper;
  4         8  
  4         292  
15 4     4   44 use Carp;
  4         8  
  4         248  
16 4     4   23 use Clone qw(clone);
  4         6  
  4         241  
17              
18             =head1 NAME
19              
20             Data::Toolkit::Map
21              
22             =head1 DESCRIPTION
23              
24             Data::Toolkit::Map objects implement mapping functions for attribute names
25             and values in Data::Toolkit::Entry objects. This is useful when converting between
26             different data representations in directory-synchronisation projects.
27              
28             =head1 SYNOPSIS
29              
30             my $map = Data::Toolkit::Map->new();
31              
32             $map->set("surname", "sn" );
33              
34             $map->set("objectclass", [ "inetOrgPerson", "organizationalPerson", "person" ] );
35             $map->set("phone","+44 1234 567890");
36             $map->set("address", \&buildAddress);
37             $map->set("fn", sub { return firstValue("fullname", @_) });
38              
39             $arrayRef = $map->outputs();
40              
41             $values = $map->generate('attributeName', $entry [, $entry...]);
42              
43             $newEntry = $map->newEntry($source1, $source2 ...);
44              
45             $result = $map->delete('thisAttribute');
46              
47             my $currentDebugLevel = Data::Toolkit::Map->debug();
48             my $newDebugLevel = Data::Toolkit::Map->debug(1);
49              
50             my $string = $map->dump();
51              
52             =head1 DEPENDENCIES
53              
54             Carp
55             Clone
56             Data::Dumper
57              
58             =cut
59              
60             ########################################################################
61             # Package globals
62             ########################################################################
63              
64 4     4   21 use vars qw($VERSION);
  4         7  
  4         7172  
65             $VERSION = '1.0';
66              
67             # Set this non-zero for debug logging
68             #
69             my $debug = 0;
70              
71             ########################################################################
72             # Constructors and destructors
73             ########################################################################
74              
75             =head1 Constructor
76              
77             =head2 new
78              
79             my $map = Data::Toolkit::Map->new();
80             my $map = Data::Toolkit::Map->new( {configAttrib => value, ....} );
81              
82             Creates an object of type Data::Toolkit::Map
83              
84             Optionally accepts a hash of configuration items chosen from this list:
85              
86             =over
87              
88             =item caseSensitiveNames
89              
90             If this is defined with a true value then attribute names are case-sensitive.
91             By default they are not, so "Surname", "surname", and "SurName" are all the same attribute.
92              
93             =item defaultMissingValueBehaviour
94              
95             This is a hash defining what to do when mapping attributes that do not have values.
96             The keys are:
97              
98             =over
99              
100             =item missing
101              
102             Defines the behaviour when an input attribute is entirely missing.
103              
104             =item noValues
105              
106             Defines the behaviour when an input attribute exists but it has no values.
107              
108             =item nullValue
109              
110             Defines the behaviour when an input attribute exists and its first value is undef.
111              
112             =item emptyString
113              
114             Defines the behaviour when an input attribute has an empty string as its first value.
115              
116             =back
117              
118             The possible values are:
119              
120             =over
121              
122             =item delete
123              
124             Delete the attribute entirely from the output of the map.
125             If the generate method is used on such an attribute, it will return undef.
126              
127             =item noValues
128              
129             The attribute will appear in the output of the map but no values will be defined.
130             If the generate method is used on such an attribute, it will return an empty array.
131              
132             =item nullValue
133              
134             The attribute will appear in the output of the map and its single value will be undef.
135             If the generate method is used on such an attribute, it will return an array containing one undef element.
136              
137             =item emptyString
138              
139             The attribute will appear in the output of the map with a single empty string value.
140              
141             =item A subroutine reference or closure
142              
143             If a pointer to an executable procedure is used as the value, that procedure
144             will be called and its return value used as the value of the attribute.
145             The return value can be undef, scalar, vector, or hash.
146             There is no way for the subroutine to request deletion of the entire attribute.
147              
148             The subroutine is called with the name of the attribute being generated as its first parameter,
149             an indication of whether an array result is wanted as its second parameter,
150             and a reference to the input entry as its third parameter:
151              
152             subroutine( $attributename, $wantarray, $entry);
153              
154             =back
155              
156             The default missing value behaviour is:
157              
158             {
159             missing => 'delete',
160             noValues => 'noValues',
161             nullValue => 'nullValue',
162             emptyString => 'emptyString',
163             };
164              
165             =back
166              
167             =cut
168              
169             sub new {
170 6     6 1 6429 my $class = shift;
171 6         14 my $configParam = shift;
172              
173 6         11 my $self = {};
174 6         23 $self->{mapping} = {};
175              
176             # Take a copy of the config hash
177             # - we don't want to store a ref to the one we were given
178             # in case it is part of another object
179             #
180 6 100       23 if (defined($configParam)) {
181 2 50       14 if ((ref $configParam) ne 'HASH') {
182 0         0 croak "Data::Toolkit::Map->new expects a hash ref but was given something else"
183             }
184              
185 2         43 $self->{config} = clone($configParam);
186             }
187             else {
188             # Start with empty config
189 4         9 $self->{config} = {};
190             # Add the default missing value behaviour
191 4         27 $self->{config}->{defaultMissingValueBehaviour} = {
192             missing => 'delete',
193             noValues => 'noValues',
194             nullValue => 'nullValue',
195             emptyString => 'emptyString',
196             };
197             }
198              
199 6         29 bless ($self, $class);
200              
201 6 50       24 carp "Data::Toolkit::Map->new $self" if $debug;
202 6         47 return $self;
203             }
204              
205             sub DESTROY {
206 6     6   743 my $self = shift;
207 6 50       276 carp "Data::Toolkit::Map Destroying $self" if $debug;
208             }
209              
210             ########################################################################
211             # Methods
212             ########################################################################
213              
214             =head1 Methods
215              
216             =cut
217              
218             ########################################
219              
220             =head2 set
221              
222             Set or replace a mapping.
223              
224             $map->set( outputAttribute, generator )
225              
226             outputAttribute must be a text string. Generator can be of several types:
227              
228             =over
229              
230             =item SCALAR - the value is the name of an attribute in the source entry, which is copied
231             to the outputAttribute
232              
233             =item ARRAY - the value is a fixed array of strings which will be used as the value of
234             the outputAttribute
235              
236             =item CODE - the value is a procedure or closure that is run to generate the value of
237             the outputAttribute. The procedure must return undef or an array reference.
238              
239             =back
240              
241             This is a simple mapping that generates a "surname" attribute by copying
242             the value of the input entry's "sn" attribute:
243              
244             $map->set("surname", "sn" );
245              
246             This is a fixed mapping generating an LDAP objectClass attribute with
247             several values:
248              
249             $map->set("objectclass", [ "inetOrgPerson", "organizationalPerson", "person" ] );
250              
251             This is a fixed mapping generating a single value (note the use of a list
252             to distinguish this from the first case above):
253              
254             $map->set("phone", ["+44 1234 567890"]);
255              
256             This is a dynamic mapping where the attribute is generated by a procedure:
257              
258             $map->set("address", \&buildAddress);
259              
260             When a dynamic mapping is evaluated, it is given the name of the attribute being generated
261             followed by all the parameters that were passed to the "generate" call,
262             so it can refer to entries and other objects.
263              
264             Similarly, closures can be used:
265              
266             $map->set("fn", sub { return firstValue("xyzzy", @_) });
267              
268             In this example, when the firstValue() procedure is called by "generate",
269             it gets one fixed parameter plus anything else that was passed to the "generate" call.
270             Thus the call:
271              
272             $map->generate("fn",$entry)
273              
274             would result in a call like this:
275              
276             firstValue("fn","xyzzy",$entry)
277              
278              
279             =cut
280              
281             sub set {
282 35     35 1 1296 my $self = shift;
283 35         53 my $attrib = shift;
284 35         45 my $values = shift;
285              
286 35 50       77 croak "set requires an attribute name" if (!$attrib);
287 35 50       69 croak "set requires a value" if (!$values);
288              
289             # Lower-case the attribute name if necessary
290 35 50       108 $attrib = "\L$attrib" if (!$self->{config}->{caseSensitiveNames});
291              
292 35 50       86 carp "Data::Toolkit::Map->set attribute '$attrib'" if $debug;
293              
294 35         136 return $self->{mapping}->{$attrib} = $values;
295             }
296              
297              
298             ########################################
299              
300             =head2 unset
301              
302             Removes an attribute from a map.
303             Returns a reference to the deleted value.
304              
305             =cut
306              
307             sub unset {
308 1     1 1 9 my $self = shift;
309 1         2 my $attrib = shift;
310              
311 1 50       4 croak "unset requires an attribute name" if (!$attrib);
312              
313             # Lower-case the attribute name if necessary
314 1 50       6 $attrib = "\L$attrib" if (!$self->{config}->{caseSensitiveNames});
315              
316 1 50       4 carp "Data::Toolkit::Map->unset attribute '$attrib'" if $debug;
317              
318 1         8 return delete $self->{mapping}->{$attrib};
319             }
320              
321              
322             ########################################
323              
324             =head2 outputs
325              
326             Return the list of attributes that the map generates.
327              
328             Returns an empty list if there are no attributes.
329              
330             $arrayRef = $map->outputs();
331              
332             =cut
333              
334             sub outputs {
335 10     10 1 1182 my $self = shift;
336              
337 10         18 my @keys_list = sort(CORE::keys %{$self->{mapping}});
  10         120  
338 10 50       34 carp "Data::Toolkit::Map->outputs are: " . (join ',', @keys_list) if $debug;
339 10 100       60 return( wantarray ? @keys_list : \@keys_list );
340              
341             }
342              
343             ########################################
344             #
345             # generateMissingValue( $attrib, $wantarray, $requiredBehaviour, $entry );
346             #
347             # Internal procedure for handling missing values
348              
349             sub generateMissingValue {
350 15     15 0 34 my ($attrib, $wantarray, $requiredBehaviour, $entry) = @_;
351              
352 15 50       33 croak "generateMissingValue needs a requiredBehaviour parameter" if (!$requiredBehaviour);
353              
354 15 50       32 carp "generateMissingValue '$attrib', $wantarray, '$requiredBehaviour'" if $debug;
355 15 100       283 if ( $requiredBehaviour eq 'delete' ) {
356 7         29 return undef;
357             }
358              
359 8 100       22 if ($requiredBehaviour eq 'noValues') {
360             # Return an empty array
361 1         2 my $res = [];
362 1 50       9 return ($wantarray ? @$res : $res);
363             }
364              
365 7 100       18 if ($requiredBehaviour eq 'nullValue') {
366             # Return an array containing an undef value
367 1         3 my $res = [ undef ];
368 1 50       27 return ($wantarray ? @$res : $res);
369             }
370              
371 6 100       16 if ($requiredBehaviour eq 'emptyString') {
372 1         3 my $res = [''];
373 1 50       9 return ($wantarray ? @$res : $res);
374             }
375              
376 5 50       14 if ((ref $requiredBehaviour) eq 'CODE') {
377             # We have been given some code to run
378 5         13 return &$requiredBehaviour($attrib, $wantarray, $entry);
379             }
380              
381 0         0 croak "generateMissingValue was given an invalid requiredBehaviour parameter ($requiredBehaviour)";
382             }
383              
384              
385             ########################################
386              
387             =head2 generate
388              
389             Generate a list of values for a given attribute.
390              
391             $values = $map->generate('attributeName', $entry );
392              
393             =cut
394              
395             sub generate {
396 37     37 1 52 my $self = shift;
397 37         50 my $attrib = shift;
398              
399 37 50       83 croak "generate requires an attribute name" if (!$attrib);
400              
401             # Lower-case the attribute name if necessary
402 37 50       118 $attrib = "\L$attrib" if (!$self->{config}->{caseSensitiveNames});
403              
404 37 50       74 carp "Data::Toolkit::Map->generate for attribute '$attrib'" if $debug;
405              
406 37         68 my $mapping = $self->{mapping}->{$attrib};
407             # If that is undef or empty, return it immediately
408 37 100       81 return $mapping if !$mapping;
409              
410 36         56 my $refMap = ref $mapping;
411 36 100       90 if (!$refMap) {
    100          
    50          
412             # We have a mapping but it is not a reference
413 24 50       49 carp "Data::Toolkit::Map->generate attribute '$attrib' from source attribute '$mapping'" if $debug;
414              
415             # Must be a simple attribute map so get the source entry
416 24         30 my $entry = shift;
417 24         97 my @values = $entry->get($mapping);
418              
419 24 100       70 if (not $values[0]) {
420             # We may have a missing value or attribute in the source
421             # (or it may just be zero...) so do some checks
422 15         40 my $valref = $entry->get($mapping);
423 15 100       37 if (not defined($valref)) {
424             # The attribute is entirely missing in the source
425 6 50       15 carp "generate attribute '$attrib' from missing source attr" if $debug;
426 6         22 return generateMissingValue( $attrib, wantarray,
427             $self->{config}->{defaultMissingValueBehaviour}->{missing}, $entry );
428             }
429 9 100       23 if ((scalar @$valref) == 0) {
430             # The attribute is present but has no values
431 3 50       9 carp "generate attribute '$attrib' from source attr with no values" if $debug;
432 3         17 return generateMissingValue( $attrib, wantarray,
433             $self->{config}->{defaultMissingValueBehaviour}->{noValues}, $entry );
434             }
435 6 100       18 if (not defined($valref->[0])) {
436             # The attribute is present but has a null value
437 3 50       7 carp "generate attribute '$attrib' from null valued source attr" if $debug;
438 3         12 return generateMissingValue( $attrib, wantarray,
439             $self->{config}->{defaultMissingValueBehaviour}->{nullValue}, $entry );
440             }
441 3 50       10 if ($valref->[0] eq '') {
442             # The attribute is present and the value is an empty string
443 3 50       162 carp "generate attribute '$attrib' from empty string source attr" if $debug;
444 3         12 return generateMissingValue( $attrib, wantarray,
445             $self->{config}->{defaultMissingValueBehaviour}->{emptyString}, $entry );
446             }
447             # In all other cases, just return what was there in the source entry
448             }
449              
450 9 50       44 return wantarray ? @values : \@values;
451             }
452             elsif ($refMap eq 'ARRAY') {
453             # Arrays represent constant data so just return it as-is
454 3 50       9 carp "Data::Toolkit::Map->generate attribute '$attrib' from fixed array" if $debug;
455              
456 3 50       18 return wantarray ? @$mapping : $mapping;
457             }
458             elsif ($refMap eq 'CODE') {
459             # We have been given some code to run
460 9 50       23 carp "Data::Toolkit::Map->generate attribute '$attrib' from supplied code" if $debug;
461              
462 9         33 my $result = &$mapping($attrib, @_);
463             # Do some sanity checking on the result
464 9 100       74 return undef if !defined($result);
465 7         13 my $resType = ref $result;
466 7 50       17 $resType = 'SCALAR' if !$resType;
467 7 50       18 if ($resType ne 'ARRAY') {
468 0         0 croak "mapping procedure returned $resType while mapping for '$attrib' - it should have returned an ARRAY";
469             }
470              
471 7         27 return $result;
472             }
473             else {
474             # Don't know what to do with this!
475 0         0 croak "generate does not know how to handle a $refMap mapping";
476             }
477              
478             }
479              
480             ########################################
481              
482             =head2 newEntry
483              
484             Create a new entry object by applying a map to one or more existing entries
485              
486             $newEntry = $map->newEntry($source1, $source2 ...);
487              
488             The source objects are Data::Toolkit::Entry objects
489              
490             =cut
491              
492             sub newEntry {
493 3     3 1 20 my $self = shift;
494              
495             # If we have been passed any source entries, use the first one as a template
496             # to create the new entry
497 3 50       11 if ($_[0]) {
498 3 50       11 carp "Data::Toolkit::Map->newEntry from template entry" if $debug;
499 3         6 my $template = shift;
500 3         19 return $template->map( $self, @_ );
501             }
502              
503             # Hmm - we seem to be mapping from nothing to create something!
504 0 0       0 carp "Data::Toolkit::Map->newEntry from nothing" if $debug;
505              
506             # Create a new entry with default config to act as source
507 0         0 my $newEntry = Data::Template::Entry->new();
508              
509 0         0 return $newEntry->map( $self );
510             }
511              
512              
513             ########################################
514              
515             =head2 delete
516              
517             Delete an output from a map.
518              
519             $result = $map->delete('thisAttribute');
520              
521             =cut
522              
523             sub delete {
524 0     0 1 0 my $self = shift;
525 0         0 my $attrib = shift;
526              
527 0 0       0 croak "delete requires an attribute name" if (!$attrib);
528              
529             # Lower-case the attribute name if necessary
530 0 0       0 $attrib = "\L$attrib" if (!$self->{config}->{caseSensitiveNames});
531              
532 0 0       0 carp "Data::Toolkit::Map->delete '$attrib'" if $debug;
533              
534 0         0 return delete $self->{$attrib};
535             }
536              
537              
538             ########################################################################
539             # Debugging methods
540             ########################################################################
541              
542             =head1 Debugging methods
543              
544             =head2 debug
545              
546             Set and/or get the debug level for Data::Toolkit::Map
547              
548             my $currentDebugLevel = Data::Toolkit::Map->debug();
549             my $newDebugLevel = Data::Toolkit::Map->debug(1);
550              
551             Any non-zero debug level causes the module to print copious debugging information.
552              
553             Note that this is a package method, not an object method. It should always be
554             called exactly as shown above.
555              
556             All debug information is reported using "carp" from the Carp module, so if
557             you want a full stack backtrace included you can run your program like this:
558              
559             perl -MCarp=verbose myProg
560              
561             =cut
562              
563             # Class method to set and/or get debug level
564             #
565             sub debug {
566 4     4 1 991 my $class = shift;
567 4 50       19 if (ref $class) { croak "Class method 'debug' called as object method" }
  0         0  
568             # print "DEBUG: ", (join '/', @_), "\n";
569 4 100       20 $debug = shift if (@_ == 1);
570 4         21 return $debug
571             }
572              
573             ########################################
574              
575             =head2 dump
576              
577             Returns a text representation of the map.
578              
579             my $string = $map->dump();
580              
581             =cut
582              
583              
584             sub dump {
585 0     0 1   my $self = shift;
586              
587 0           my %hash = $self->{mapping};
588 0           return Dumper(\%hash);
589             }
590              
591              
592             ########################################################################
593             ########################################################################
594              
595             =head1 Error handling
596              
597             If you miss out an essential parameter, the module will throw an exception
598             using "croak" from the Carp module. These exceptions represent programming
599             errors in most cases so there is little point in trapping them with "eval".
600              
601             =head1 Author
602              
603             Andrew Findlay
604              
605             Skills 1st Ltd
606              
607             andrew.findlay@skills-1st.co.uk
608              
609             http://www.skills-1st.co.uk/
610              
611             =cut
612              
613             ########################################################################
614             ########################################################################
615             1;