File Coverage

blib/lib/Tree/Numbered/Tools.pm
Criterion Covered Total %
statement 392 509 77.0
branch 72 158 45.5
condition 6 9 66.6
subroutine 25 39 64.1
pod 23 24 95.8
total 518 739 70.0


line stmt bran cond sub pod time code
1             package Tree::Numbered::Tools;
2              
3 1     1   39733 use 5.006000;
  1         3  
  1         34  
4 1     1   4 use strict;
  1         1  
  1         28  
5 1     1   4 use warnings;
  1         5  
  1         25  
6              
7 1     1   774 use Tree::Numbered;
  1         2059  
  1         29  
8 1     1   763 use Text::ParseWords;
  1         1192  
  1         63  
9 1     1   5 use Carp; # generate better errors with more context
  1         1  
  1         5994  
10              
11             require Exporter;
12              
13             our @ISA = qw(Tree::Numbered);
14              
15             # Items to export into callers namespace by default. Note: do not export
16             # names by default without a very good reason. Use EXPORT_OK instead.
17             # Do not simply export all your public functions/methods/constants.
18              
19             # This allows declaration use Tree::Numbered::Tools ':all';
20             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
21             # will save memory.
22             our %EXPORT_TAGS = ( 'all' => [ qw(
23            
24             ) ] );
25              
26             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
27              
28             our @EXPORT = qw(
29            
30             );
31              
32             our $VERSION = '1.04';
33              
34              
35             # - Generate a tree object from different sources: database table, text file, SQL statement, Perl array.
36             # - Dump a tree object to one of these sources.
37             # - Convert between source formats.
38              
39             # Parse a tree from a text file and convert it to a Tree::Numbered object
40             # Each line in the text file should indent using at least one space for each level
41             # The term 'top level' is used to describe a root child.
42              
43             =head1 NAME
44              
45             Tree::Numbered::Tools - Perl module to create tree objects using different sources.
46              
47             =head1 SYNOPSIS
48              
49             Example 1: Using a text file as a source:
50              
51              
52             Value LastName FirstName
53             # ----- -------- ---------
54             Grandfather Smith Abraham
55             Son1 Smith Bert
56             Son2 Smith 'Clumsy Carl'
57             Grandson1 Jones Dennis
58             Grandson2 Jones Eric
59             Son3 Smith 'Fatty Fred'
60             Grandson3 Smith Greg
61             Grandson4 Smith Huey
62             Grandmother Smith Anna
63             Daughter1 Smith Berta
64             Daughter2 Smith Celine
65              
66             use Tree::Numbered::Tools;
67              
68             # Reads a text file, returns a tree object
69             my $tree = Tree::Numbered::Tools->readFile(
70             filename => $filename,
71             use_column_names => 1,
72             );
73              
74             Example 2: Using an array as a source:
75              
76             use Tree::Numbered::Tools;
77              
78             my $arrayref = [
79             [qw(serial parent name url)],
80             [1, 0, 'ROOT', 'ROOT'],
81             [2, 1, 'File', 'file.pl'],
82             [3, 2, 'New', 'file-new.pl'],
83             [4, 3, 'Window', 'file-new-window.pl'],
84             ];
85             my $tree = Tree::Numbered::Tools->readArray(
86             arrayref => $arrayref,
87             use_column_names => 1,
88             );
89              
90             Example 3: Using a database table as a source, use the SQL 'AS' statement for easy column mapping:
91              
92             use Tree::Numbered::Tools;
93              
94             my $sql = 'SELECT serial, parent AS "Parent", name AS "Name", url AS "URL" FROM mytable ORDER BY serial';
95             my $tree = Tree::Numbered::Tools->readSQL(
96             dbh => $dbh,
97             sql => $sql,
98             );
99              
100             Example 4: Display a tree object in the same format as the text file in example 1:
101              
102             my $output = Tree::Numbered::Tools->outputFile();
103              
104             Example 5: Display a tree object as an array reference, to be used for cut 'n paste in a Perl program.
105              
106             my $output = Tree::Numbered::Tools->outputArray();
107              
108             Example 6: Convert a text file to a database table.
109              
110             my $sql = Tree::Numbered::Tools->convertFile2DB(
111             filename => $filename,
112             use_column_names => 1,
113             dbh => $dbh,
114             table => $table,
115             );
116              
117             Example 7: Convert a text file to SQL 'INSERT INTO' statements.
118              
119             my $sql = Tree::Numbered::Tools->convertFile2SQL(
120             filename => $filename,
121             use_column_names => 1,
122             );
123              
124              
125             =head1 DESCRIPTION
126              
127             Tree::Numbered::Tools is a child class of Tree::Numbered.
128             Its purpouse is to easily create a tree object from different sources.
129              
130             The most useful source is probably a text file (see SYNOPSIS, example 1).
131             The text file visualizes the tree structure as well as node names in the first column.
132             Any other columns represent each node's properties.
133             The format is easy to read and understand, even for a non-programmer.
134             Besides, editing a text file is normally far more easy than editing records in a database table.
135             Anyhow, at run time, reading from/writing to a database outperformances a text file.
136             This module is intented to be used as a tool to create database tables using text files, not to replace tables with text files (even if the module permits you to use the text file as a source without dealing with a database).
137             The format of the first column in the text file only requires that each tree level should be indented using one or more spaces (or tabs). It is recommended to be consistent and use the same number of spaces to indent all tree levels, even if the readFile() method tries to determine each node's level even if the indenting isn't consistent. To get each node's properties, the readFile() method parses each line in the text file using the Text::ParseWords module, so any property value containg a space must be quoted. If the last column or columns in the text file for a node are omitted, the corresponding property value is assigned the empty string.
138              
139             Programmers who prefer not using an external source when creating a tree may use an array reference.
140             Being a programmer, it is probably easier to edit an array than database records.
141             See SYNOPSIS, example 2.
142              
143             The purpouse of the SQL statement as a source for the tree object is the more straightforward way to map column names using Tree::Numbered::Tools->readSQL() than the Tree::Numbered::DB->read() method.
144             See SYNOPSIS, example 3.
145              
146             =head1 NOTES ABOUT THE ROOT NODE
147              
148             Using a text file as a source, the text file does not contain the root node itself. This is on purpouse. In daily life, describing a tree, frequently there is not one single root node, but two or many 'top level nodes' as the 'Grandfather' and 'Grandmother' nodes in SYNOPSIS, example 1.
149             To manage all the nodes as a single tree, a single root node named 'ROOT' will always be created.
150             In tree terminology, a 'top level node' is the same as a root child.
151             Anyway, using any other source, the 'ROOT' node should be included.
152             See SYNOPSIS, example 2, how to create the 'ROOT' node with an array.
153              
154             =head1 NOTES ABOUT FIELDS AND COLUMNS
155              
156             A Tree::Numbered object uses the term 'fields' for each node's properties.
157             A Tree::Numbered::Tools object uses the term 'columns'.
158             Shortly, 'columns' are 'fields' in a specified order.
159             The Tree::Numbered->getFieldNames() method uses a hash internally to get field names.
160             This means there is no way to guarantee a specific order in obtaining the field names. The field order doesn't matter for an abstract tree object, but it does when printing a tree structure, for example.
161             The Tree::Numbered::Tools->getColumnNames method uses an array internally to guarantee the specified order.
162              
163             The column order is only an issue when working with a tree object created by a source not specifying columns, for example creating a new tree using the Tree::Numbered->new() method.
164             When creating a tree using the readSQL() method, the column names will be obtained from the DBI::$sth->{NAME} method, i.e. the SQL statement, and thus listed in a known order.
165             When creating a tree using the readFile()/readArray() method, the column names can be obtained using the getColumnNames() method, if the source file/array was specified with column names on its first line/row, and use_column_names is set to true.
166              
167             There is no way to 'map' column names from a file/SQL/array to field names in the tree object using distinct names, as it is in Tree::Numbered::DB, for example.
168             Instead of mapping, modify the column names in your text file or array row, or use the SQL 'AS' statement, depending on which method you use to create the tree.
169              
170             =head1 METHODS SUMMARY
171              
172             Methods to create a tree object reading from a source:
173             readFile() - read from a text file
174             readArray() - read from an array
175             readSQL() - read from an SQL statement
176             readDB() - read from a database table
177              
178             Methods to output the contents of a tree object:
179             outputFile() - output in text file format
180             outputArray() - output in array format (Perl code)
181             outputSQL() - output as SQL statements
182             outputDB() - output to (creates) a database table
183              
184             Methods to convert from one source format to another:
185             convertFile2Array()
186             convertFile2SQL()
187             convertFile2DB()
188             convertArray2File()
189             convertArray2SQL()
190             convertArray2DB()
191             convertSQL2File()
192             convertSQL2Array()
193             convertSQL2DB()
194             convertDB2File()
195             convertDB2Array()
196             convertDB2SQL()
197              
198             Using convertX2Y() is practically the same as calling readX() followed by outputY().
199              
200             Other Methods:
201             getColumnNames - see NOTES ABOUT FIELDS AND COLUMNS
202             getSourceType - File, Array, SQL, DB
203             getSourceName - file name, database table name
204              
205             =head1 METHODS
206              
207             =head2 readFile()
208              
209             readFile(
210             filename => $filename,
211             use_column_names => $use_column_names,
212             );
213              
214             Reads $filename, returns a tree object.
215             $use_column_names is a boolean, if set (default), assumes that the first (non-comment, non-blank) line contains column names.
216              
217             =cut
218              
219              
220             sub readFile {
221 32     32 1 512 my $self = shift;
222             # Get args
223 32         537 my %args = (
224             filename => '',
225             use_column_names => 1, # Require column names by default, as we create them by default in outputFile
226             @_, # argument pair list goes here
227             );
228             # Die on missing filename
229 32 50       154 my $filename = $args{filename} or croak "Missing filename";
230 32         68 my $use_column_names = $args{use_column_names};
231              
232             # Get the file contents into an array
233 32 50       2965 open FH, "<$filename" or croak "Cannot open $filename: $!";
234 32         2139 chomp(my @lines = );
235 32         1177 close FH;
236              
237             # Weed out comments and blank lines
238 32         1403 @lines = grep(!/^\s*\#/, @lines);
239 32         730 @lines = grep(!/^\s*$/, @lines);
240              
241             # Default root value:
242 32         227 my $root_value = 'ROOT';
243              
244             # Optionally, get column names from first line, pass column names to Tree::Numbered->new method
245             # Column names cannot have spaces.
246 32         96 my %args_hash = ();
247 32         56 my @column_names;
248              
249 32         238 my $first_line = $self->_trim($lines[0]);
250             # Initiate the column names array if asked for
251 32 50       76 if ($use_column_names) {
252             # Shift off first line (column names) from contents
253 32         56 shift(@lines);
254             # Get column names from first line
255 32         250 my $column_names_ref = $self->_getColumnNamesFile($first_line);
256 32         250 @column_names = @$column_names_ref;
257             }
258             # When not using column names, we have to scan all lines in the text file to get the one with most columns, as some lines, including the first, may have omitted the last column(s).
259             # The line with most columns will decide the number of columns used.
260             else {
261             # Get max columns
262 0         0 my $max_cols = $self->_getMaxColumnsFile(\@lines);
263             # Use default column names ('serial', 'parent', 'Value', 'Value2', 'Value3', etc) if no column names were given
264 0         0 @column_names[0..2] = ('serial', 'parent', 'Value');
265 0         0 for (my $i = 3; $i < $max_cols; $i++) {
266 0         0 $column_names[$i] = 'Value'.($i-1);
267             }
268             }
269             # The argument hash for the root node
270 32         111 foreach my $column_name (@column_names) {
271 64         189 $args_hash{$column_name} = $root_value;
272             }
273              
274             # Create a root node to tie all top level nodes
275 32         369 $self = $self->new(%args_hash);
276              
277             # Assume that first line is a top level node
278             # Use a hash, where the key is the indentation and the value is the level
279 32         3613 my %level_indent = ();
280             # Use first top level node as start values
281 32         195 my $current_indent = $self->_indented($lines[0]);
282 32         79 my $previous_indent = $self->_indented($lines[0]);
283 32         52 my $current_level = 0;
284 32         174 $level_indent{$current_indent} = 0;
285              
286 32         83 my $node = $self;
287              
288             # Loop through lines
289 32         144 for (my $i = 0; $i < @lines; $i++) {
290 736         1792 my $line = $self->_trim($lines[$i]);
291             # Split possible line fields, keep quotes, Text::ParseWords for details
292 736         2132 my @line_fields = &parse_line('\s+', 1, $line);
293 736         47220 @line_fields = $self->_strip_quotes(@line_fields);
294 736         1249 my $value = $line_fields[0];
295              
296 736         1689 $current_indent = $self->_indented($lines[$i]);
297 736 100       2521 $previous_indent = ($i > 0) ? $self->_indented($lines[$i-1]) : $self->_indented($lines[0]);
298              
299             # Down one level ?
300 736 100       2006 if ($current_indent > $previous_indent) {
    100          
301             # We never go down more than one level at a time
302 224         291 $self = $node;
303 224         272 $current_level++;
304             }
305              
306             # Up one or more levels ?
307             elsif ($current_indent < $previous_indent) {
308             # We may go up one or more levels at a time
309              
310             # BUGFIX - BEGIN
311             # Bug in Tree-Numbered-Tools-1.01:
312             # Warning message "Use of uninitialized value in subtraction (-)" when nodes at the first line or lines use a higher indent level than following lines.
313             # The warning message is caused by $current_indent having a undefined value.
314             # The solution is to set $current_indent to 0 and show a customized warning message.
315             # (NOT reported in bug ticket http://rt.cpan.org/Public/Bug/Display.html?id=48068)
316             # Bugfix added in 1.02 (2009-07-25).
317 144 100       471 if (!defined $level_indent{$current_indent})
318             {
319 16         47 $level_indent{$current_indent} = 0;
320 16 50       113 my $warn_lines = $i ? "'$lines[$i-1]'\n'$lines[$i]'\n'$lines[$i+1]'\n" : "'$lines[0]'\n'$lines[1]'\n'$lines[2]'\n";
321 16         291 warn "WARNING: One or more of the following line seems to be incorrectly indented:\n$warn_lines";
322             }
323             # BUGFIX - END
324 144         797 my $up_levels = $level_indent{$previous_indent} - $level_indent{$current_indent};
325 144         187 $current_level = $current_level - $up_levels;
326 144         317 foreach (1..$up_levels) {
327 128         456 $self = $self->getParentRef;
328             }
329             }
330              
331             # Determine fields used
332 736 50       1979 if ($use_column_names) {
333 736         943 my $j = 0;
334 736         1075 foreach my $column_name (@column_names) {
335 1472         3551 $args_hash{$column_name} = $line_fields[$j++];
336             }
337              
338             }
339             else {
340             # Default field 'Value' if no column names
341 0         0 $args_hash{Value} = $value;
342             }
343             # Append node
344 736         3042 $node = $self->append(%args_hash);
345              
346             # Save current level state
347 736         81343 $level_indent{$current_indent} = $current_level;
348             }
349              
350             # Up to top level, to get the entire tree object
351 32         160 while ($self->getNumber != 1) {
352 96         700 $self = $self->getParentRef;
353             }
354              
355             # Set object properties
356             # Initiate the column names variable so the outside world can use getColumnNames
357             # (will return undef if use_column_names was set to false)
358 32         274 $self->{COLUMN_NAMES_REF} = \@column_names;
359 32         98 $self->{SOURCE_TYPE} = 'File';
360 32         81 $self->{SOURCE_NAME} = $filename;
361              
362             # Return the tree object
363 32         301 return $self;
364             }
365              
366             =cut
367              
368             =head2 readArray()
369              
370             readArray(
371             arrayref => $arrayref,
372             use_column_names => $use_column_names,
373             );
374              
375             Reads $arrayref, returns a Tree::Numbered object.
376             $use_column_names is a boolean, if set (default), assumes that the first array row contains column names.
377              
378             =cut
379              
380             sub readArray {
381 49     49 1 462 my $self = shift;
382             # Get args
383 49         280 my %args = (
384             arrayref => '',
385             use_column_names => 1, # Assume column names by default
386             @_, # argument pair list goes here
387             );
388 49 50       200 my $arrayref = $args{arrayref} or croak "Missing array";
389 49         122 my $use_column_names = $args{use_column_names};
390              
391             # Get the array
392 49         182 my @array = @$arrayref;
393              
394             # Get first element
395 49         67 my @first_element = @{$array[0]};
  49         161  
396 49 50       110 croak "The array must have at least three columns: 'serial', 'parent', and 'Value'" if (@first_element < 3);
397              
398 49         75 my @column_names = ();
399              
400             # Shift off first element (column names) from array if we are using column names.
401 49 100       87 if ($use_column_names) {
402 48         82 @column_names = @first_element;
403 48         61 shift @array;
404             }
405             # use default column names ('serial', 'parent', 'Value', 'Value2', 'Value3', etc) if no column names were given
406             else {
407 1         3 @column_names[0..2] = ('serial', 'parent', 'Value');
408 1         5 for (my $i = 3; $i < @first_element; $i++) {
409 4         15 $column_names[$i] = 'Value'.($i-1);
410             }
411             }
412              
413             # BUGFIX BEGIN
414             # First column's name must be 'serial' (lower case).
415 49 50       113 croak "The first column's name must be 'serial' (lower case)" if ($column_names[0] ne 'serial');
416             # BUGFIX END
417              
418             # BUGFIX - BEGIN
419             # Bug in Tree-Numbered-Tools-1.01:
420             # http://rt.cpan.org/Public/Bug/Display.html?id=48068
421             # Bugfix added in 1.02 (2009-07-24), suggested by Daniel Higgins:
422             # Check column 'serial' and 'parent', both must be numeric integer values.
423             # Then sort array numerically by 'parent' column to avoid append() error later, bug occurs with unsorted arrays.
424             # Check for valid integers.
425 49         124 for (my $i = 0; $i < @array; $i++)
426             {
427             # Get element and it fields
428 451         397 my @element_fields = @{$array[$i]};
  451         949  
429             # Get current node and parent node numbers
430 451         523 my $serial = $element_fields[0];
431 451         376 my $parent = $element_fields[1];
432 451 50       657 croak "The 'serial' element '$serial' in row $i isn't an integer'" if (!_isInteger($serial));
433 451 50       680 croak "The 'parent' element '$parent' in row $i isn't an integer'" if (!_isInteger($parent));
434             }
435             # Sort array.
436 1123         1246 @array = sort {
437 49         231 ($a->[1] <=> $b->[1]) } @array;
438             # BUGFIX - END
439              
440             # Get root node
441 49         58 my @root_node = @{$array[0]};
  49         117  
442              
443             # Create argument hash using column names as keys and root node as values
444 49         107 my %args_hash = ();
445 49         115 for (my $i = 0; $i < @column_names; $i++) {
446 199         570 $args_hash{$column_names[$i]} = $root_node[$i];
447             }
448              
449             # Shift off the root node from the array
450 49         54 shift @array;
451              
452             # Create a root node to tie all top level nodes
453 49         234 $self = $self->new(
454             %args_hash
455             );
456              
457             # Loop through elements
458 49         5667 for (my $i = 0; $i < @array; $i++) {
459             # Get element and it fields
460 402         47405 my @element_fields = @{$array[$i]};
  402         939  
461              
462             # Get current node and parent node numbers
463 402         450 my $serial = $element_fields[0];
464 402         411 my $parent = $element_fields[1];
465              
466             # Determine fields used
467 402         581 my $j = 0;
468 402         551 foreach my $column_name (@column_names) {
469 1614         2594 $args_hash{$column_name} = $element_fields[$j++];
470             }
471              
472             # BUGFIX - BEGIN
473             # Bug in Tree-Numbered-Tools-1.01:
474             # http://rt.cpan.org/Public/Bug/Display.html?id=48068
475             # Bugfix added in 1.02 (2009-07-24), suggested by Daniel Higgins:
476 402         631 our $parentnode=undef;
477             $self->allProcess( sub {
478 3795     3795   29034 my ($self,$parent) = @_;
479 3795         3570 our $parentnode;
480 3795         14062 $_ = $self->getserial ;
481 3795 100       120915 $parentnode = $self if $_ == $parent ;
482             },
483 402         1966 $parent );
484              
485             # Add current node to its parent
486 402         10735 my $node = $parentnode ;
487 402         1354 $node = $node->append(%args_hash);
488             # BUGFIX - END
489             }
490              
491             # Set object properties
492             # Initiate the column names variable so the outside world can use getColumnNames
493             # (will return undef if use_column_names was set to false)
494             # Column names serial and parent should not be included in column names list, shift them off
495 49 50       6102 shift @column_names if @column_names;
496 49 50       331 shift @column_names if @column_names;
497 49         90 $self->{COLUMN_NAMES_REF} = \@column_names;
498 49         80 $self->{SOURCE_TYPE} = 'Array';
499 49         169 $self->{SOURCE_NAME} = undef;
500              
501             # Return the tree object
502 49         267 return $self;
503             }
504              
505              
506             =head2 readSQL()
507              
508             readSQL(
509             dbh => $dbh,
510             sql => $sql,
511             );
512              
513             Fetches an array using the database handle $dbh and the SQL statement $sql, returns a tree object.
514             Uses readArray() internally to create the tree object.
515             To map column names in the database table to tree column names, use the SQL 'AS' statement.
516             Always get used to double quote the alias name, to make the SQL statement database type independent.
517             Without alias quotes, reserved SQL words such as 'AS' will work as an alias on MySQL but not on PgSQL (PgSQL returns lower case aliases unless double quoted).
518             Remember that aliases cannot contain spaces, as they reflect the column names, which in turn are used for methods getting a column's value. For example, to obtain a value for a column created from an alias called 'MyColumn', the method getMyColumn() will be used. An alias called 'My Column' will try to call the method getMy Column(), which of course will cause a run-time syntax error.
519              
520             Example 1:
521             # GOOD, works on both MySQL and PgSQL
522             my $sql = 'SELECT serial AS "Serial", parent AS "Parent", name AS "Name", url AS "URL" FROM mytable ORDER BY Serial';
523              
524             Example 2:
525             # BAD, works on MySQL but not on PgSQL
526             my $sql = 'SELECT serial AS Serial, parent AS Parent, name AS Name, url AS URL FROM mytable ORDER BY Serial';
527              
528             Example 3:
529             # BAD, single quotes will not do on PgSQL
530             my $sql = "SELECT serial AS 'Serial', parent AS 'Parent', name AS 'Name', url AS 'URL' FROM mytable ORDER BY Serial";
531              
532             Well, if you forgot to quote the aliases, readSQL() adds the quotes for you.
533             You should just be aware of that unquoted aliases doesn't always work as expected in your daily SQL life. :-)
534              
535             =cut
536              
537             sub readSQL {
538 0     0 1 0 my $self = shift;
539             # Get args
540 0         0 my %args = (
541             dbh => '',
542             sql => '',
543             @_, # argument pair list goes here
544             );
545 0 0       0 my $dbh = $args{dbh} or croak "Missing DB handle";
546 0 0       0 my $sql = $args{sql} or croak "Missing SQL statement" ;
547              
548             # Quote any SQL aliases
549 0         0 $sql = $self->_sql_alias_quoted($sql);
550              
551             # Get array reference
552             # Column names are always used, named after the SQL columns
553 0 0       0 my $sth = $dbh->prepare($sql) or croak $dbh->errstr;
554 0 0       0 $sth->execute or croak $dbh->errstr;
555             # Get column names
556 0         0 my $colnamesref = $sth->{'NAME'};
557 0 0       0 my $arrayref = $sth->fetchall_arrayref or croak $dbh->errstr;
558              
559             # Insert column names as first element into the retreived array
560 0         0 unshift @$arrayref, $colnamesref;
561              
562             # Use readArray to create the tree
563 0         0 my $use_column_names = 1;
564 0         0 $self = $self->readArray(
565             arrayref => $arrayref,
566             use_column_names => 1,
567             );
568              
569             # Set object properties
570             # Initiate the column names variable so the outside world can use getColumnNames
571             # $self->{COLUMN_NAMES_REF} = $colnamesref; # Already set from readArray
572 0         0 $self->{SOURCE_TYPE} = 'SQL';
573 0         0 $self->{SOURCE_NAME} = undef;
574              
575             # Return the tree object
576 0         0 return $self;
577             }
578              
579             =head2 readDB()
580              
581             readDB(
582             dbh => $dbh,
583             table => $table,
584             );
585              
586             Fetches an array using the database handle $dbh from the table $table, returns a Tree::Numbered object.
587             This is a wrapper for the readSQL() mehod using the SQL statement 'SELECT * from $table'.
588             It is recommended to use the more flexible readSQL() instead, as you can map names using the 'AS' statement.
589              
590             =cut
591              
592             sub readDB {
593 0     0 1 0 my $self = shift;
594             # Get args
595 0         0 my %args = (
596             dbh => '',
597             table => '',
598             @_, # argument pair list goes here
599             );
600 0 0       0 my $dbh = $args{dbh} or croak "Missing DB handle";
601 0 0       0 my $table = $args{table} or croak "Missing database table name" ;
602 0         0 my $sql = "SELECT * FROM $table";
603              
604             # Use readSQL to create the tree
605 0         0 $self = $self->readSQL(
606             dbh => $dbh,
607             sql => $sql,
608             );
609              
610             # Set object properties
611             # Initiate the column names variable so the outside world can use getColumnNames
612             # $self->{COLUMN_NAMES_REF} = $colnamesref; # Already set from readSQL
613 0         0 $self->{SOURCE_TYPE} = 'DB';
614 0         0 $self->{SOURCE_NAME} = $table;
615              
616             # Return the tree object
617 0         0 return $self;
618             }
619              
620              
621             =head2 outputFile()
622              
623             outputFile(
624             first_indent => $first_indent,
625             level_indent => $level_indent,
626             column_indent => $column_indent,
627             );
628              
629             The ouputFile() method returns the tree structure as used in the file format.
630             The purpouse of this method is to display/create an overview of a tree object, both the tree hierarchy and each node's properties, which easily can be modified with a text editor to create a new tree object using the readFile() method.
631              
632             All arguments are optional.
633             Formatting arguments:
634             $first_indent decides the position of the first column.
635             $level_indent decides the indenting for each node level.
636             $column_indent decides the number of spaces to separate columns.
637              
638             =cut
639              
640             sub outputFile {
641 20     20 1 7596 my $self = shift;
642 20         91 my %args = (
643             first_indent => 2,
644             level_indent => 2,
645             column_indent => 2,
646             @_, # argument pair list goes here
647             );
648              
649             # Get list of column names
650 20         83 my $column_names_ref = $self->getColumnNames();
651             # If column names are defined, compare number of columns with number of fields in tree
652 20         43 my @column_names = @$column_names_ref;
653 20         74 my @field_names = $self->getFieldNames;
654              
655             # If column names aren't defined ($column_names_ref returns undef), use tree field names (arbitrary order).
656 20 50       162 @column_names = @field_names if (!$column_names_ref);
657              
658             # Create the indented tree structure and optional additional columns
659 20         30 my $first_indent = $args{first_indent};
660 20         22 my $level_indent = $args{level_indent};
661 20         25 my $column_indent = $args{column_indent};
662 20         34 my $first_indent_string = ' ' x $first_indent;
663 20         25 my $indent_string = ' ' x $level_indent;;
664 20         25 my $tree_structure = '';
665             # Use a copy of the array, as it will be modified.
666 20         33 my @extra_column_names = @column_names;
667 20         27 my $first_column_name = shift(@extra_column_names);
668              
669             # Calculate each node value's string length, needed for pretty printing.
670             # The longest string in each column will decide each column's position.
671             # The first column's value will be indented according to its tree level.
672             # Thus, the indenting has to be included when calculate the string length for the first column.
673              
674             # Array to store each column's position, needed for pretty printing
675             # Initiate the @column_pos array with the length of each column_name, in case that the column name is longer than any of its values.
676 20         27 my @column_pos = ();
677 20         35 foreach (@column_names) {
678 40         62 push @column_pos, length($_);
679             }
680              
681             # Calculate first column's position, including indenting
682 20         64 foreach my $node_number ($self->listChildNumbers) {
683 215         4781 my @values = $self->follow($node_number, $first_column_name);
684             # Calculate spaces before this node's string
685 215         49447 my $node_indent_length = $first_indent + (scalar(@values) - 1) * $level_indent;
686             # Add this node's string length
687 215         915 my $first_value = pop(@values);
688 215         275 my $first_value_string_length = length($first_value);
689             # Add 2 to string length if quoting is needed
690 215 50       517 $first_value_string_length = $first_value_string_length + 2 if ($first_value =~ m/\s+/);
691             # Caclulate entire length for first column
692 215         266 my $first_column_string_length = $node_indent_length + $first_value_string_length + 1;
693 215 100       385 $column_pos[0] = $first_column_string_length if ($first_column_string_length > $column_pos[0]);
694             # Calculate extra columns' positions
695 215         414 for (my $i = 1; $i < @column_names; $i++) {
696             # Last value in array contains this node's value
697 215         664 my @values = $self->follow($node_number, $column_names[$i]);
698 215         55661 my $value = pop(@values);
699             # If no $value (last column may be blank, which returns undef), ignore.
700 215 50       405 if ($value) {
701 215 50       337 my $column_string_length = ($value) ? length($value) : 0;
702             # Add 2 to length if quoting is needed
703 215 50       617 $column_string_length = $column_string_length + 2 if ($value =~ m/\s+/);
704 215 100       1089 $column_pos[$i] = $column_string_length if ($column_string_length > $column_pos[$i]);
705             }
706             }
707             }
708              
709             # Create contents string
710 20         68 foreach my $node_number ($self->listChildNumbers) {
711 215         4593 my $line = $first_indent_string;
712             # The array contains a list of all the node's parent values as well as its own value
713 215         531 my @values = $self->follow($node_number, $first_column_name);
714             # The scalar contains only the node's own value
715 215         47426 my $value = pop(@values);
716 215         355 $line .= $indent_string x scalar(@values);
717 215         217 $line .= $value;
718             # Add any necessary spaces after the value
719 215         361 $line .= " " x ($column_pos[0] - length($line) + $column_indent - 1);
720             # Loop through all other columns but the first
721 215         524 for (my $i = 1; $i < @column_names; $i++) {
722 215         581 my @values = $self->follow($node_number, $column_names[$i]);
723 215         50316 my $column_value = pop(@values);
724             # If no $value (last column may be blank, which returns undef), ignore.
725 215 50       427 if ($column_value) {
726             # Quote if necessary
727 215 50       511 $column_value = "'".$column_value."'" if ($column_value =~ m/\s+/);
728             # Pretty printing
729 215         221 $line .= $column_value;
730 215         1055 $line .= " " x ($column_pos[$i] - length($column_value) + $column_indent);
731             }
732             }
733 215         463 $tree_structure .= "$line\n";
734             }
735              
736             # Insert columns at top of tree contents
737 20         39 my $header = $first_indent_string;
738 20         48 for (my $i = 0; $i < @column_names; $i++) {
739 40         48 $header .= $column_names[$i];
740             # Dirty hack
741 40 100       66 if ($i == 0) {
742 20         88 $header .= " " x ($column_pos[$i] - length($column_names[$i]) - $first_indent + $column_indent - 1);
743             }
744             else {
745 20         62 $header .= " " x ($column_pos[$i] - length($column_names[$i]) + $column_indent);
746             }
747             }
748 20         30 $header .= "\n";
749             # Add underscore to columns, replace all non-space characters with '-'
750 20         190 (my $underscore = $header) =~ s/\S/-/g;
751             # Replace first character with a comment sign
752 20         78 $underscore =~ s/^./\#/g;
753              
754             # Insert comments at top
755 20         28 my $package = __PACKAGE__ || '';
756 20   50     220 my $method = (caller(0))[3] || '';
757             # Replace last :: with ->
758 20         172 $method =~ s/$package\:\:/->/;
759 20 50       45 $method .= '()' if $method;
760 20         61 my $comments = <
761             # Tree contents generated by $package$method.
762             # Redirect this output to a file called for example 'tree.txt'.
763             # To create a tree object, use the $package->readFile() method with 'tree.txt' as the filename argument.
764             # For details, check the $package documentation.
765             #
766             COMMENT
767              
768             # Return the entire output
769 20         86 my $output = $comments.$header.$underscore.$tree_structure;
770 20         162 return $output;
771             }
772              
773             =cut
774              
775             =head2 outputArray()
776              
777             outputArray();
778              
779             The outputArray() method returns a Perl code snippet for creating a new tree object based on the current tree object, using an array reference and the readArray() method.
780             The purpouse of this method is to easily create Perl code from whatever tree source, possibly modify/add/delete elements (nodes) in the array reference, and then use the readArray() method to create a new tree object.
781              
782             =cut
783              
784             sub outputArray {
785              
786 15     15 1 8801 my $self = shift;
787             # my %args = (
788             # @_, # argument pair list goes here
789             # );
790              
791             # Get list of column names
792 15         118 my $column_names_ref = $self->getColumnNames();
793 15         49 my @column_names = @$column_names_ref;
794              
795             # If column names aren't defined ($column_names_ref returns undef), use tree field names (arbitrary order).
796 15         91 my @field_names = $self->getFieldNames;
797 15 50       174 if (!$column_names_ref) {
798 0         0 @column_names = @field_names
799             }
800             # Insert required columns:
801 15         125 my @required_column_names = ('serial', 'parent');
802              
803 15         154 my $arrayref_code =
804             'my $arrayref = [
805             [qw('.join(' ', @required_column_names, @column_names).')],
806             [1, 0, ' . "'ROOT', " x @column_names . '],
807             ';
808              
809 15         137 foreach my $node_number ($self->listChildNumbers) {
810 301         7876 my $node = $self->getSubTree($node_number);
811 301         119712 my $parent_node = $node->getParentRef;
812 301         1486 my $parent_number = $parent_node->getNumber;
813 301         1300 my $value_code = '';
814 301         479 foreach my $column_name (@column_names) {
815             # Last value in array contains this node's value
816 602         1738 my @values = $self->follow($node_number, $column_name);
817 602         158515 my $value = pop(@values);
818             # Set value to empty string if undefined
819 602 50       1449 $value = '' if !$value;
820             # Escape possible quote characters in values
821 602         871 $value =~ s/\'/\\\'/g;
822             # Add quotes and comma
823 602         1759 $value_code .= "'$value', ";
824             }
825 301         1079 $arrayref_code .= " [$node_number, $parent_number, $value_code],\n";
826             }
827             $arrayref_code .=
828 15         192 ' ];
829             ';
830              
831 15         32 my $extra_code = '# Create a new tree object using the array above
832             my $use_column_names = 1;
833             my $tree = Tree::Numbered::Tools->readArray(
834             arrayref => $arrayref,
835             use_column_names => $use_column_names,
836             );
837             # Display the Perl code for the created object
838             print $tree->outputArray();
839             ';
840              
841             # Insert comments at top
842 15         25 my $package = __PACKAGE__ || '';
843 15   50     383 my $method = (caller(0))[3] || '';
844             # Replace last :: with ->
845 15         185 $method =~ s/$package\:\:/->/;
846 15 50       117 $method .= '()' if $method;
847 15         52 my $comments = <
848             #
849             # Perl code generated by $package$method.
850             # Redirect this output to a file called for example 'tree.pl'.
851             # The run from the command line:
852             # perl -w tree.pl
853             # For details, check the $package documentation.
854             #
855             COMMENT
856              
857             # Insert program header
858             ### my $perl_binary = $^X; # BUGFIX: Normally shows just 'perl' instead of '/usr/bin/perl'
859 15         86133 my $perl_binary = `which perl`;
860 15         207 chomp $perl_binary;
861 15         162 my $header = '#!' . $perl_binary . " -w\n";
862 15         32 $header .= "use strict;\n";
863 15         55 $header .= "use $package;\n";
864              
865             # Return the entire output (complete program snippet)
866 15         160 my $output = $header.$comments.$arrayref_code.$extra_code;
867 15         1844 return $output;
868             }
869              
870             =cut
871              
872             =head2 outputSQL()
873              
874             outputSQL(
875             table => $table,
876             dbs => $dbs,
877             drop => $drop,
878             );
879              
880             The outputSQL() method returns SQL statements for creating records in the database table $table.
881             The purpouse of this method is to create SQL statements for later use.
882             If you want to create the records instead of the SQL stataments, use the outputDB() method instead.
883              
884             The $dbs argument is optional, sets the database server type, defaults to 'mysql'.
885             Currently supported database server types are MySQL and PostgreSQL.
886             Due to inconsistent naming convention for PostgreSQL ($dbh->{Driver}->{Name} returns 'Pg' while $dbh->get_info( SQL_DBMS_NAME ) returns 'PostgreSQL'), valid 'dbs' values when using PostgreSQL are: 'postgres', 'PostgreSQL', 'PgSQL', and 'Pg'.
887             The 'dbs' argument is case-insensitive.
888             The generated SQL code has been tested with MySQL 5.0.77 and PostgreSQL 8.2.13 on FreeBSD 7.2, but may need modification for use with other database servers/versions/platforms.
889              
890             The $drop argument is optional, if true (false by default), inserts a DROP TABLE statement before the CREATE TABLE statement.
891             If false, the DROP TABLE statement will be left outcommented.
892              
893             =cut
894              
895             sub outputSQL {
896              
897 60     60 1 7836 my $self = shift;
898 60         520 my %args = (
899             table => '',
900             dbs => 'mysql',
901             drop => '',
902             @_, # argument pair list goes here
903             );
904              
905             # Die on missing table name
906 60 50       186 my $table = $args{table} or croak "Missing table name";
907 60         100 my $dbs = $args{dbs};
908 60         110 my $drop = $args{drop};
909              
910             # Get all SQL statements into array refs
911 60         288 my $sql_statements_ref = $self->_sql_statements(%args);
912 60         198 my @sql_statements = @$sql_statements_ref;
913 60         136 my ($sql_header_ref, $drop_table_ref, $create_table_ref, $insert_into_ref, $create_index_ref, $comments_ref) = @sql_statements;
914              
915             # Format SQL statements and comments for string output
916 60         91 my $comments_header = $comments_ref->[0];
917 60         107 my $comments1 = $comments_ref->[1];
918 60 100       188 $comments1 .= "\n" if $comments1;
919 60         95 my $comments2 = $comments_ref->[2];
920 60 100       112 $comments2 .= "\n" if $comments2;
921 60         190 my $sql_header = join("\n", @$sql_header_ref);
922 60 100       129 $sql_header .= "\n" if $sql_header;
923 60         148 my $drop_table = join("\n", @$drop_table_ref);
924 60 50       138 $drop_table .= "\n" if $drop_table;
925 60         113 my $create_table = join("\n", @$create_table_ref);
926 60 50       153 $create_table .= "\n" if $create_table;
927 60         361 my $insert_into = join("\n", @$insert_into_ref);
928 60 50       140 $insert_into .= "\n" if $insert_into;
929 60         112 my $create_index = join("\n", @$create_index_ref);
930 60 100       123 $create_index .= "\n" if $create_index;
931              
932             # Return the entire output (SQL statements and comments)
933 60         341 my $output = $comments_header.$sql_header.$comments1.$drop_table.$create_table.$insert_into.$comments2.$create_index;
934 60         1449 return $output;
935              
936             }
937              
938             =cut
939              
940             =head2 outputDB()
941              
942             outputDB(
943             dbh => $dbh,
944             table => $table,
945             drop => $drop,
946             );
947              
948             The outputDB() method creates a database table $table using the database handle $dbh, and insert tree nodes as table records.
949             The purpouse of this method is to store a tree in a table. The tree object can be recreated by using one of the readSQL() or readDB methods.
950             This method uses outputSQL() internally to get the SQL statements, and executes them.
951             If you want to tie a tree object to a database table in "real time", first use this method with an existing tree object to create the database table. Then create a tree object using the Tree::Numbered::DB module by Yosef Meller, which will reflect changes in the database table as you modify the tree nodes.
952              
953             The $dbh is a database handle.
954             The $table and $drop arguments are the same as for outputSQL().
955             There is no $dbs argument, as the database server type is determined by the $dbh argument ($dbh->{Driver}->{Name} more exactly).
956              
957             =cut
958              
959             sub outputDB {
960              
961 0     0 1 0 my $self = shift;
962 0         0 my %args = (
963             dbh => '',
964             table => '',
965             drop => '',
966             @_, # argument pair list goes here
967             );
968              
969             # Die on missing DB handle and/or table name
970 0 0       0 my $dbh = $args{dbh} or croak "Missing DB handle";
971 0 0       0 my $table = $args{table} or croak "Missing table name";
972 0         0 $args{dbs} = $dbh->{Driver}->{Name};
973 0         0 my $dbs = $args{dbs};
974 0         0 my $drop = $args{drop};
975              
976             # Get all SQL statements into array refs
977 0         0 my $sql_statements_ref = $self->_sql_statements(%args);
978 0         0 my @sql_statements = @$sql_statements_ref;
979 0         0 my ($sql_header_ref, $drop_table_ref, $create_table_ref, $insert_into_ref, $create_index_ref, $comments_ref) = @sql_statements;
980              
981 0         0 my $sql = '';
982              
983             # We will not execute comments nor empty strings in array elements.
984             # Execute SQL headers, if any
985 0         0 foreach (@$sql_header_ref) {
986 0         0 $sql = $_;
987 0 0       0 if ($sql) {
988 0 0       0 $dbh->do($sql) or croak $dbh->errstr;
989             }
990             }
991             # Execute DROP TABLE, if $drop
992 0 0       0 if ($drop) {
993             ### $sql = $drop_table_ref->[0];
994 0         0 $sql = $drop_table_ref->[1];
995 0 0       0 if ($sql) {
996 0 0       0 $dbh->do($sql) or croak $dbh->errstr;
997             }
998             }
999             # Execute CREATE TABLE
1000 0         0 $sql = $create_table_ref->[0];
1001 0 0       0 if ($sql) {
1002 0 0       0 $dbh->do($sql) or croak $dbh->errstr;
1003             }
1004             # Execute INSERT INTO statements
1005 0         0 foreach (@$insert_into_ref) {
1006 0         0 $sql = $_;
1007 0 0       0 if ($sql) {
1008 0 0       0 $dbh->do($sql) or croak $dbh->errstr;
1009             }
1010             }
1011             # Execute CREATE INDEX statements, if any
1012 0         0 foreach (@$create_index_ref) {
1013 0         0 $sql = $_;
1014 0 0       0 if ($sql) {
1015 0 0       0 $dbh->do($sql) or croak $dbh->errstr;
1016             }
1017             }
1018              
1019 0         0 return 1;
1020              
1021             }
1022              
1023             =cut
1024              
1025             =head2 convertFile2Array()
1026              
1027             convertFile2Array(
1028             filename => $filename,
1029             use_column_names => $use_column_names,
1030             );
1031              
1032             Calls readFile() followed by outputArray().
1033              
1034             =cut
1035              
1036             sub convertFile2Array {
1037 10     10 1 8309 my $self = shift;
1038             ### my $tree = $self->readFile(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1039 10         322 my $tree = Tree::Numbered::Tools->readFile(@_); # SOLUTION: Always use a new tree object.
1040 10         53 return $tree->outputArray();
1041             }
1042              
1043             =cut
1044              
1045             =head2 convertFile2SQL()
1046              
1047             convertFile2SQL(
1048             filename => $filename,
1049             use_column_names => $use_column_names,
1050             table => $table,
1051             dbs => $dbs,
1052             drop => $drop,
1053             );
1054              
1055             Calls readFile() followed by outputSQL().
1056              
1057             =cut
1058              
1059             sub convertFile2SQL {
1060 20     20 1 6459 my $self = shift;
1061             ### my $tree = $self->readFile(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1062 20         183 my $tree = Tree::Numbered::Tools->readFile(@_); # SOLUTION: Always use a new tree object.
1063 20         150 return $tree->outputSQL(@_);
1064             }
1065              
1066             =cut
1067              
1068             =head2 convertFile2DB()
1069              
1070             convertFile2DB(
1071             filename => $filename,
1072             use_column_names => $use_column_names,
1073             dbh => $dbh,
1074             table => $table,
1075             drop => $drop,
1076             );
1077              
1078             Calls readFile() followed by outputDB().
1079              
1080             =cut
1081              
1082             sub convertFile2DB {
1083 0     0 1 0 my $self = shift;
1084             ### my $tree = $self->readFile(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1085 0         0 my $tree = Tree::Numbered::Tools->readFile(@_); # SOLUTION: Always use a new tree object.
1086 0         0 return $tree->outputDB(@_);
1087             }
1088              
1089             =cut
1090              
1091             =head2 convertArray2File()
1092              
1093             convertArray2File(
1094             arrayref => $arrayref,
1095             use_column_names => $use_column_names,
1096             first_indent => $first_indent,
1097             level_indent => $level_indent,
1098             column_indent => $column_indent,
1099             );
1100              
1101             Calls readArray() followed by outputFile().
1102              
1103             =cut
1104              
1105             sub convertArray2File {
1106 15     15 1 2583 my $self = shift;
1107             ### my $tree = $self->readArray(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1108 15         58 my $tree = Tree::Numbered::Tools->readArray(@_); # SOLUTION: Always use a new tree object.
1109 15         57 return $tree->outputFile(@_);
1110             }
1111              
1112             =cut
1113              
1114             =head2 convertArray2SQL()
1115              
1116             convertArray2SQL(
1117             arrayref => $arrayref,
1118             use_column_names => $use_column_names,
1119             table => $table,
1120             dbs => $dbs,
1121             drop => $drop,
1122             );
1123              
1124             Calls readArray() followed by outputSQL().
1125              
1126             =cut
1127              
1128             sub convertArray2SQL {
1129 30     30 1 3839 my $self = shift;
1130             ### my $tree = $self->readArray(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1131 30         108 my $tree = Tree::Numbered::Tools->readArray(@_); # SOLUTION: Always use a new tree object.
1132 30         96 return $tree->outputSQL(@_);
1133             }
1134              
1135             =cut
1136              
1137             =head2 convertArray2DB()
1138              
1139             convertArray2DB(
1140             arrayref => $arrayref,
1141             use_column_names => $use_column_names,
1142             dbh => $dbh,
1143             table => $table,
1144             drop => $drop,
1145             );
1146              
1147             Calls readArray() followed by outputDB().
1148              
1149             =cut
1150              
1151             sub convertArray2DB {
1152 0     0 1 0 my $self = shift;
1153             ### my $tree = $self->readArray(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1154 0         0 my $tree = Tree::Numbered::Tools->readArray(@_); # SOLUTION: Always use a new tree object.
1155 0         0 return $tree->outputDB(@_);
1156             }
1157              
1158             =cut
1159              
1160             =head2 convertSQL2File()
1161              
1162             convertSQL2File(
1163             dbh => $dbh,
1164             sql => $sql,
1165             first_indent => $first_indent,
1166             level_indent => $level_indent,
1167             column_indent => $column_indent,
1168             );
1169              
1170             Calls readSQL() followed by outputFile().
1171              
1172             =cut
1173              
1174             sub convertSQL2File {
1175 0     0 1 0 my $self = shift;
1176             ### my $tree = $self->readSQL(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1177 0         0 my $tree = Tree::Numbered::Tools->readSQL(@_); # SOLUTION: Always use a new tree object.
1178 0         0 return $tree->outputFile(@_);
1179             }
1180              
1181             =cut
1182              
1183             =head2 convertSQL2Array()
1184              
1185             convertSQL2Array(
1186             dbh => $dbh,
1187             sql => $sql,
1188             );
1189              
1190             Calls readSQL() followed by outputArray().
1191              
1192             =cut
1193              
1194             sub convertSQL2Array {
1195 0     0 1 0 my $self = shift;
1196             ### my $tree = $self->readSQL(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1197 0         0 my $tree = Tree::Numbered::Tools->readSQL(@_); # SOLUTION: Always use a new tree object.
1198 0         0 return $tree->outputArray(@_);
1199             }
1200              
1201             =cut
1202              
1203             =head2 convertSQL2DB()
1204              
1205             convertSQL2DB(
1206             dbh => $dbh,
1207             sql => $sql,
1208             dbh_dest => $dbh_dest,
1209             table => $table,
1210             drop => $drop,
1211             );
1212              
1213             Calls readSQL() followed by outputDB().
1214              
1215             NOTE: There are two database handles, $dbh and $dbh_dest, in case you use one database as a source and another as destination. The argument $dbh_dest is optional, defaults to $dbh, assumes using the same database handle for both source and destination.
1216             Using different database handles, this method can be useful to migrate a tree table from MySQL to PostgreSQL, for example.
1217              
1218             =cut
1219              
1220             sub convertSQL2DB {
1221 0     0 1 0 my $self = shift;
1222 0         0 my %args_sql = (
1223             dbh => '',
1224             sql => '',
1225             @_, # argument pair list goes here
1226             );
1227 0         0 my %args_db = (
1228             dbh_dest => $args_sql{dbh},
1229             table => '',
1230             drop => '',
1231             @_, # argument pair list goes here
1232             );
1233 0         0 $args_db{dbh} = $args_db{dbh_dest};
1234             ### my $tree = $self->readSQL(%args_sql); # BUG: Using an existing tree object, the tree nodes are not replaced.
1235 0         0 my $tree = Tree::Numbered::Tools->readSQL(%args_sql); # SOLUTION: Always use a new tree object.
1236 0         0 return $tree->outputDB(%args_db);
1237             }
1238              
1239             =cut
1240              
1241             =head2 convertDB2File()
1242              
1243             convertDB2File(
1244             dbh => $dbh,
1245             table => $table,
1246             first_indent => $first_indent,
1247             level_indent => $level_indent,
1248             column_indent => $column_indent,
1249             );
1250              
1251             Calls readDB() followed by outputFile().
1252              
1253             =cut
1254              
1255             sub convertDB2File {
1256 0     0 1 0 my $self = shift;
1257             ### my $tree = $self->readDB(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1258 0         0 my $tree = Tree::Numbered::Tools->readDB(@_); # SOLUTION: Always use a new tree object.
1259 0         0 return $tree->outputFile(@_);
1260             }
1261              
1262             =cut
1263              
1264             =head2 convertDB2Array()
1265              
1266             convertDB2Array(
1267             dbh => $dbh,
1268             table => $table,
1269             );
1270              
1271             Calls readDB() followed by outputArray().
1272              
1273             =cut
1274              
1275             sub convertDB2Array {
1276 0     0 1 0 my $self = shift;
1277             ### my $tree = $self->readDB(@_); # BUG: Using an existing tree object, the tree nodes are not replaced.
1278 0         0 my $tree = Tree::Numbered::Tools->readDB(@_); # SOLUTION: Always use a new tree object.
1279 0         0 return $tree->outputArray(@_);
1280             }
1281              
1282             =cut
1283              
1284             =head2 convertDB2SQL()
1285              
1286             convertDB2SQL(
1287             dbh => $dbh,
1288             sql => $sql,
1289             table => $table,
1290             table_dest => $table_dest,
1291             dbs => $dbs,
1292             drop => $drop,
1293             );
1294              
1295             Calls readDB() followed by outputSQL().
1296             NOTE: $table is the source table, $table_dest is the table name used in the generated SQL statements.
1297              
1298             =cut
1299              
1300             sub convertDB2SQL {
1301 0     0 1 0 my $self = shift;
1302 0         0 my %args_db = (
1303             dbh => '',
1304             sql => '',
1305             table => '',
1306             @_, # argument pair list goes here
1307             );
1308 0         0 my %args_sql = (
1309             table_dest => '',
1310             dbs => 'mysql',
1311             drop => '',
1312             @_, # argument pair list goes here
1313             );
1314 0         0 $args_sql{table} = $args_db{table_dest};
1315             ### my $tree = $self->readDB(%args_db); # BUG: Using an existing tree object, the tree nodes are not replaced.
1316 0         0 my $tree = Tree::Numbered::Tools->readDB(%args_db); # SOLUTION: Always use a new tree object.
1317 0         0 return $tree->outputSQL(%args_sql);
1318             }
1319              
1320             =cut
1321              
1322             =head2 getColumnNames()
1323              
1324             Returns a list (in array context) or a ref to a list (in scalar context) of the column names.
1325             The list corresponds to:
1326             Using a file - the words on the first non-comment or blank line.
1327             Using an array - the first array row.
1328             Using an SQL statement - the SQL field names
1329             Using a database table - the table column names
1330              
1331             Using this method on a tree created using with use_column_names set to 0 returns the default column names: 'Value', 'Value2', 'Value3', etc.
1332              
1333             =cut
1334              
1335             sub getColumnNames {
1336 101     101 1 4374 my $self = shift;
1337 101         184 my $ary_ref = $self->{COLUMN_NAMES_REF};
1338 101         315 my @ary = @$ary_ref;
1339 101 100       429 return (wantarray) ? @ary : $ary_ref;
1340             }
1341              
1342             =head2 getSourceType()
1343              
1344             Returns one of the strings 'File', 'Array', 'SQL', 'DB' depending on which source was used to create the tree object.
1345              
1346             =cut
1347              
1348             sub getSourceType {
1349 5     5 1 1652 my $self = shift;
1350 5         13 return $self->{SOURCE_TYPE};
1351             }
1352              
1353             =head2 getSourceName()
1354              
1355             Returns the file name if the source type is 'File', or the database table name if the source type is 'DB'.
1356             Returns undef if source type is 'Array' or 'SQL'.
1357              
1358             =cut
1359              
1360             sub getSourceName {
1361 5     5 1 2023 my $self = shift;
1362 5         17 return $self->{SOURCE_NAME};
1363             }
1364              
1365             # version
1366             sub version{
1367 0     0 0 0 my $self = shift;
1368 0         0 return $VERSION;
1369             }
1370              
1371             #----------- Internal subs -------------
1372              
1373             # Get column names from file (internal use only)
1374             # Use getColumnNames from the outside world
1375             sub _getColumnNamesFile {
1376 32     32   55 my $self = shift;
1377 32         98 my $first_line = shift;
1378 32         299 my @column_names = &parse_line('\s+', 0, $first_line);
1379 32         3888 return \@column_names;
1380             }
1381              
1382             # Get the max number of columns in a file contents, passed as an array of lines.
1383             sub _getMaxColumnsFile {
1384 0     0   0 my $self = shift;
1385 0         0 my $lines_ref = shift;
1386 0         0 my @lines = @$lines_ref;
1387 0         0 my $max_cols = 0;
1388 0         0 foreach my $line (@lines) {
1389 0         0 my @columns = &parse_line('\s+', 0, $line);
1390 0 0       0 $max_cols = scalar(@columns) if (scalar(@columns) > $max_cols);
1391             }
1392 0         0 return $max_cols;
1393             }
1394              
1395             sub _trim {
1396 768     768   1039 my $self = shift;
1397 768         1509 my @s = @_;
1398 768         1350 for (@s) {
1399 768         2853 s/^\s+//;
1400 768         3901 s/\s+$//;
1401             }
1402 768 50       2645 return wantarray ? @s : $s[0];
1403             }
1404              
1405             sub _strip_quotes {
1406 736     736   995 my $self = shift;
1407 736         1688 my @s = @_;
1408 736         1275 for (@s) {
1409 1472         2175 s/^\'(.*)\'$/$1/;
1410 1472         2970 s/^\"(.*)\"$/$1/;
1411             # s/^[\'|\"]//;
1412             # s/[\'|\"]$//;
1413             }
1414 736 50       9744 return wantarray ? @s : $s[0];
1415             }
1416              
1417             sub _indented {
1418 1536     1536   1769 my $self = shift;
1419 1536         2019 my $s = shift;
1420 1536         5363 $s =~ s/^(\s*).*/$1/;
1421 1536         3996 return length($s);
1422             }
1423              
1424             sub _isInteger {
1425 902     902   783 my $string = shift;
1426 902 50       4059 return ($string =~ /^[+-]?\d+$/) ? 1 : 0;
1427             }
1428              
1429             # Quotes SQL aliases (the word that follows 'AS' in an SQL statement).
1430             # Used by readSQL() to ensure all aliases are quoted.
1431             # Unquoted aliases works on MySQL but not on PgSQL.
1432             sub _sql_alias_quoted {
1433 0     0   0 my $self = shift;
1434 0         0 my $sql = shift;
1435             # Split the SQL statement into an array of words.
1436              
1437             # When found the word 'AS' (without quotes, case insensitive), the following word is an alias.
1438             # If the following word (the alias) isn't double quoted, double quote it.
1439             # It is possible to use a double quote character as part of the alias, escaping it with an extra double quote:
1440             # SELECT serial as """SERIAL""" FROM treetest
1441             # will create the alias "SERIAL", including the double quotes.
1442             # This means, if the alias was quoted with 1, 3, 5, or any odd number of double quotes, there is no need to quote the alias, as it will work any way.
1443             # If the alias was quoted with 2, 4, 6, or any even number of double quotes, there is no need to quote the alias, as the SQL statement was invalid anyway. ;-)
1444             # Summary: never double quote an already double quoted alias.
1445              
1446             # It is possible to use a reserved SQL word as an alias, as long as it is quoted:
1447             # SELECT serial AS "AS" from treetest
1448             # On PgSQL, it even works without quotes:
1449             # SELECT serial AS AS FROM treetest
1450             # This could cause a parsing error, as the second AS could try to quote the following word ('FROM' in the example above).
1451             # To avoid this, test exactly on the word 'AS' (without quotes).
1452             # When found, the following word in the array will be double quoted.
1453             # When testing the next element ('"AS"' in the example above) for the word 'AS', it will not match.
1454              
1455             # Quoted aliases may have spaces:
1456             # SELECT serial AS "My Serial" FROM treetest
1457             # This means that we can't just split on \s+
1458             # Solution: Text::ParseWords takes care of not splitting quoted words. Nevertheless, quotes have to be added, as Text::ParseWords removes them.
1459             # The concern about aliases with spaces is to make this sub generic.
1460             # Aliases with spaces will never occur generating a tree, as the aliases corresponds to the field names, which can contain spaces, so aliases with spaces will not work with trees.
1461              
1462             # Bugfix in 1.03:
1463             # Warning message when SQL string contains trailing newline(s)
1464             ### chomp $sql; BAD SOLUTION: works ONLY for ones single trailing newline, not for two newlines.
1465             # Better solution: trim leading and trailing whitespace characters [ \t\n\r\f];
1466 0         0 $sql =~ s/^\s+//;
1467 0         0 $sql =~ s/\s+$//;
1468              
1469 0         0 my @words = &parse_line('\s+', 0, $sql);
1470 0         0 for (my $i = 0; $i < @words; $i++) {
1471             # If reserved word AS, quote the following word
1472 0 0       0 if (uc($words[$i]) eq 'AS') {
1473             # Check for existing array element
1474 0 0       0 if ($words[$i+1]) {
1475             # The alias may include the following comma, which must follow the quote.
1476 0 0       0 if ($words[$i+1] =~ m/\,$/) {
1477 0         0 $words[$i+1] =~ s/\,$/\"\,/;
1478             }
1479             else {
1480 0         0 $words[$i+1] .= '"';
1481             }
1482 0         0 $words[$i+1] = '"'.$words[$i+1];
1483             }
1484             }
1485             #print $words[$i], "\n";
1486             }
1487 0         0 $sql = join(' ', @words);
1488 0         0 return $sql;
1489             }
1490              
1491             # Returns the SQL statements as an reference to a list of arrays references, where each element is one statement.
1492             # The statements are separated by type: the CREATE TABLE statement goes in one array, all INSERT INTO statements in another array, etc.
1493             sub _sql_statements {
1494 60     60   84 my $self = shift;
1495              
1496 60         277 my %args = (
1497             table => '',
1498             dbs => 'mysql',
1499             drop => '',
1500             @_, # argument pair list goes here
1501             );
1502              
1503             # Die on missing table name
1504 60 50       173 my $table = $args{table} or croak "Missing table name";
1505 60         83 my $dbs = $args{dbs};
1506 60         104 my $drop = $args{drop};
1507              
1508 60         108 my @sql_header = ();
1509 60         78 my @drop_index_and_table = ();
1510 60         80 my @create_table = ();
1511 60         65 my @insert_into = ();
1512 60         87 my @create_index = ();
1513 60         95 my @comments = ();
1514              
1515             # Get list of column names
1516 60         186 my $column_names_ref = $self->getColumnNames();
1517 60         129 my @column_names = @$column_names_ref;
1518              
1519             # If column names aren't defined ($column_names_ref returns undef), use tree field names (arbitrary order).
1520 60         225 my @field_names = $self->getFieldNames;
1521 60 50       564 if (!$column_names_ref) {
1522 0         0 @column_names = @field_names;
1523             }
1524              
1525             # Insert required columns:
1526 60         118 my @required_column_names = ('serial', 'parent');
1527              
1528             # Variables for the SQL statements
1529 60         71 my $sql_header = '';
1530 60         74 my $example_output_file = 'insert-into.sql';
1531 60         68 my $drop_index = '';
1532 60         68 my $drop_table = '';
1533 60         70 my $create_table = '';
1534 60         110 my $create_table_last_line = '';
1535 60         89 my $insert_into = '';
1536 60         108 my $create_index = '';
1537 60         68 my $field_type = '';
1538 60         57 my $qc = '';
1539 60         152 my $sql_comment = '';
1540 60         64 my $command_line = '';
1541 60         101 my $comments = '';
1542             # Use only lower case letters for columns names in SQL statements, even if column names may be mixed or upper case letters.
1543 60         189 my @column_names_sql = @column_names;
1544 60         491 @column_names_sql = grep(s/^(.+$)/lc($1)/e, @column_names_sql);
  120         772  
1545              
1546             # Database dependent SQL syntax
1547 60         119 SWITCH: for ($dbs) {
1548             # MySQL
1549 60 100       223 /^mysql$/i && do {
1550             # No SQL header for MySQL
1551             # $sql_header = '';
1552             # DROP TABLE statement for MySQL (outcommented if $drop is not set)
1553 30         73 $drop_table = "DROP TABLE IF EXISTS $table;";
1554 30         37 $sql_comment = "#";
1555             # CREATE TABLE statement for MySQL ('serial' and 'parent' columns only)
1556 30         61 $create_table =
1557             'CREATE TABLE '. $table . ' (
1558             `serial` int(11) NOT NULL auto_increment,
1559             `parent` int(11) NOT NULL default \'0\',
1560             ';
1561             # CREATE TABLE statement (last line) for MySQL
1562 30         35 $create_table_last_line =
1563             ' PRIMARY KEY (serial)
1564             ) TYPE=MyISAM;';
1565             # No separate 'CREATE INDEX' for MySQL
1566             # $create_index = '';
1567             # Field type for MySQL
1568 30         35 $field_type = 'varchar(255) default NULL';
1569             # Quote character for MySQL
1570 30         40 $qc = '`';
1571             # Command line for MySQL
1572 30         29 $example_output_file = 'insert-into-mysql.sql';
1573 30         40 $command_line = "mysql -u root -pmysqlpassword test < $example_output_file";
1574             # Push dummy empty string comments
1575 30         53 push @comments, '', '';
1576 30         70 last SWITCH;
1577             };
1578             # PgSQL
1579 30 50       153 /^postgres$|^PostgreSQL$|^pgsql$|^pg$/i && do {
1580             # SQL header for PostgresSQL
1581 30         58 $sql_header =
1582             'SET SESSION AUTHORIZATION \'pgsql\';';
1583 30         51 push @sql_header, $sql_header;
1584 30         34 $sql_header =
1585             'SET search_path = "public", pg_catalog;';
1586 30         42 push @sql_header, $sql_header;
1587 30         42 $comments =
1588             '-- Definition';
1589 30         43 push @comments, $comments;
1590              
1591             # DROP INDEX statement for PostgresSQL (outcommented if $drop is not set)
1592 30         61 $drop_index = 'DROP INDEX IF EXISTS "'. $table .'_serial_index"'.";";
1593             # DROP TABLE statement for PostgresSQL (outcommented if $drop is not set)
1594 30         48 $drop_table = 'DROP TABLE IF EXISTS "'. $table .'"'.";";
1595 30         40 $sql_comment = "--";
1596             # CREATE TABLE statement for PostgresSQL ('serial' and 'parent' columns only)
1597 30         51 $create_table =
1598             'CREATE TABLE "'. $table .'" (
1599             "serial" integer,
1600             "parent" integer,
1601             ';
1602             # CREATE TABLE statement (last line) for PostgresSQL
1603 30         35 $create_table_last_line =
1604             ') WITH OIDS;';
1605             # 'CREATE INDEX' for PostgresSQL
1606 30         83 $comments =
1607             '-- Indexes';
1608 30         37 push @comments, $comments;
1609 30         55 $create_index =
1610             'CREATE UNIQUE INDEX '.$table.'_serial_index ON '.$table.' USING btree (serial);';
1611 30         41 push @create_index, $create_index;
1612             # Field type for PostgresSQL
1613 30         37 $field_type = 'text';
1614             # Quote character for PostgresSQL
1615 30         28 $qc = '"';
1616             # Command line for PostgresSQL
1617 30         40 $example_output_file = 'insert-into-pgsql.sql';
1618 30         36 $command_line = "psql -q -U pgsql -d test -f $example_output_file";
1619 30         74 last SWITCH;
1620             };
1621             # DEFAULT
1622 0         0 croak "Database server type '$dbs' is not supported.";
1623             }
1624              
1625             # DROP TABLE statement (outcommented if $drop is not set)
1626 60 50       163 $drop_index = $sql_comment.' '.$drop_index if !$drop;
1627 60         175 push @drop_index_and_table, $drop_index;
1628 60 50       106 $drop_table = $sql_comment.' '.$drop_table if !$drop;
1629 60         87 push @drop_index_and_table, $drop_table;
1630              
1631             # CREATE TABLE statement
1632              
1633             # MySQL
1634             # DROP TABLE IF EXISTS junk;
1635             # CREATE TABLE junk (
1636             # serial int(11) NOT NULL auto_increment,
1637             # parent int(11) NOT NULL default '0',
1638             # name varchar(255) default NULL,
1639             # url varchar(255) default NULL,
1640             # color varchar(255) default NULL,
1641             # permission varchar(255) default NULL,
1642             # visible varchar(255) default NULL,
1643             # PRIMARY KEY (serial)
1644             # ) TYPE=MyISAM;
1645              
1646              
1647             # PostgreSQL
1648             # SET SESSION AUTHORIZATION 'postgres';
1649             # SET search_path = "public", pg_catalog;
1650             # -- Definition
1651             # DROP TABLE "public"."menu";
1652             # CREATE TABLE "menu" (
1653             # "serial" integer,
1654             # "parent" integer,
1655             # "name" text,
1656             # "url" text,
1657             # "color" text,
1658             # "permission" text,
1659             # "visible" text
1660             # ) WITH OIDS;
1661             # -- Indexes
1662             # CREATE UNIQUE INDEX serial ON menu USING btree (serial);
1663              
1664 60         177 for (my $i = 0; $i < @column_names_sql; $i++) {
1665             # Add quotes
1666 120         294 $create_table .= " ".$qc.$column_names_sql[$i].$qc." $field_type";
1667             # Add comma for all but last value or if MySQL
1668 120 100 100     502 $create_table .= "," if (($i < @column_names_sql - 1) || lc($dbs) eq 'mysql') ;
1669             # Add newline
1670 120         257 $create_table .= "\n";
1671             }
1672 60         94 $create_table .= $create_table_last_line;
1673              
1674 60         113 push @create_table, $create_table;
1675              
1676             # INSERT INTO statements
1677              
1678             # INSERT INTO `junk2` ( `serial` , `parent` , `name` , `url` , `color` , `permission` , `visible` )
1679             # VALUES (
1680             # '1', '0', 'ROOT', 'ROOT', 'ROOT', 'ROOT', 'ROOT'
1681             # );
1682              
1683 60         370 $insert_into =
1684             "INSERT INTO $qc". $table ."$qc ( $qc".join("$qc, $qc", @required_column_names, @column_names_sql)."$qc )\n".
1685             "VALUES (\n".
1686             " 1, 0, " . "'ROOT', " x (@column_names_sql - 1). "'ROOT'\n".
1687             ');';
1688 60         93 push @insert_into, $insert_into;
1689              
1690 60         243 foreach my $node_number ($self->listChildNumbers) {
1691 852         21315 my $node = $self->getSubTree($node_number);
1692 852         311567 my $parent_node = $node->getParentRef;
1693 852         3912 my $parent_number = $parent_node->getNumber;
1694 852         2800 my $value_code = '';
1695 852         2285 for (my $i = 0; $i < @column_names; $i++) {
1696 1704         2160 my $column_name = $column_names[$i];
1697             # Last value in array contains this node's value
1698 1704         4395 my @values = $self->follow($node_number, $column_name);
1699 1704         423529 my $value = pop(@values);
1700             # Set value to empty string if undefined
1701 1704 50       3223 $value = '' if !$value;
1702             # Escape possible quote characters in values
1703             ### $value =~ s/\'/\\\'/g; # BUGFIX: double quote instead of escape quotes to avoid warning message on PgSQL 8.2.
1704 1704         2476 $value =~ s/\'/\'\'/g; # BUGFIX: double quote instead of escape quotes to avoid warning message on PgSQL 8.2.
1705             # Add quotes
1706 1704         2479 $value_code .= "'$value'";
1707             # Add comma for all but last value
1708 1704 100       6802 $value_code .= ", " if ($i < @column_names - 1);
1709             }
1710 852         5240 $insert_into =
1711             "INSERT INTO $qc". $table ."$qc ( $qc".join("$qc, $qc", @required_column_names, @column_names_sql)."$qc )\n".
1712             "VALUES (\n".
1713             " $node_number, $parent_number, $value_code\n".
1714             ');';
1715 852         2216 push @insert_into, $insert_into;
1716             }
1717              
1718             # Insert comments at top
1719 60         188 my $package = __PACKAGE__ || '';
1720 60   50     863 my $method = (caller(1))[3] || '';
1721             # Replace last :: with ->
1722 60         517 $method =~ s/$package\:\:/->/;
1723 60 50       214 $method .= '()' if $method;
1724             # Supress the following comment if $drop.
1725 60 50       138 my $uncomment_drop = ($drop) ? "Comment out the 'DROP TABLE ...' statement if you don't want to delete an existing table." : "Uncomment the 'DROP TABLE ...' statement if you want to delete an existing table.";
1726 60         713 $comments = <
1727             $sql_comment SQL statements for $dbs generated by $package$method.
1728             $sql_comment For details, check the $package documentation.
1729             $sql_comment $uncomment_drop
1730             $sql_comment Usage of this output:
1731             $sql_comment Redirect this output to a file called, for example, '$example_output_file':
1732             $sql_comment $0 @ARGV > $example_output_file
1733             $sql_comment Then run from the command line (assumes that the database 'test' already exists):
1734             $sql_comment $command_line
1735             $sql_comment
1736             COMMENT
1737 60         177 unshift @comments, $comments;
1738              
1739             # Return a reference to all array references.
1740 60         207 my @list = (\@sql_header, \@drop_index_and_table, \@create_table, \@insert_into, \@create_index, \@comments);
1741 60         610 return \@list;
1742             }
1743              
1744              
1745             =cut
1746              
1747             =head1 BUGS AND OTHER ISSUES
1748              
1749             There may be bugs in the code.
1750             The code was written more to be useful as a tool, rather than to be compact, fast and clean.
1751             Please report through CPAN:
1752             http://rt.cpan.org/NoAuth/Bugs.html?Dist=Tree-Numbered-Tools
1753             or send mail to bug-Tree-Numbered-Tools@rt.cpan.org
1754              
1755             Incorrectly using $use_column_names=1 together with a source where column names are *not* specified will cause unpredictable results, probably a run-time error.
1756             The same is true for incorrect usage of $use_column_names=0 together with a source where column names *are* specified.
1757             This module doesn't try to determine incorrect usage as described above.
1758             The possible incorrect usage applies to files and arrays, which may or may not use column names.
1759             SQL expressions and DB tables always use column names by nature.
1760             Always use $use_column_names=1 (set by default using any method) and always specify column names in the source text file or array.
1761              
1762             For suggestions, questions and such, email me directly.
1763              
1764             =head1 EXAMPLES
1765              
1766             To see working examples, see the 'examples' directory in the distribution.
1767              
1768             =head1 SEE ALSO
1769              
1770             Tree::Numbered, Tree::Numbered::DB by Yosef Meller
1771              
1772             =head1 AUTHOR
1773              
1774             Johan Kuuse, Ejohan@kuu.seE
1775              
1776             =head1 COPYRIGHT AND LICENSE
1777              
1778             Copyright (C) 2004-2009 by Johan Kuuse
1779              
1780             This library is free software; you can redistribute it and/or modify
1781             it under the same terms as Perl itself, either Perl version 5.8.9 or,
1782             at your option, any later version of Perl 5 you may have available.
1783              
1784             =cut
1785             1;