/usr/local/CPAN/XML-RDB/XML/RDB/PopulateTables.pm


###
#
# $Id: PopulateTables.pm,v 1.2 2003/04/19 04:17:48 trostler Exp $
#
# COPYRIGHT AND LICENSE
# Copyright (c) 2001, 2003, Juniper Networks, Inc.  
# All rights reserved.
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
# 	1.	Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer. 
# 	2.	Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following disclaimer
# in the documentation and/or other materials provided with the
# distribution. 
# 	3.	The name of the copyright owner may not be used to 
# endorse or promote products derived from this software without specific 
# prior written permission. 
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
###

package XML::RDB::PopulateTables;
use vars qw($VERSION);
$VERSION = '1.2';

###
#
# Pull XML data out of file & put into RDB tables created by make_tables.pl
# 
####
use strict;
# use XML::DOM;
use DBIx::Recordset;
# use DBIx::Sequence;

sub new {
  my ($class, $rdb, $doc, $head) = @_;

  my $self = bless { 
    rdb => $rdb,
    doc => $doc,
    head => $head,
#    sequence => new DBIx::Sequence({dbh => $rdb->{DBH}}),
    one_to_n => $rdb->get_one_to_n_db(),
   }, $class;

  $self;
}

sub go {
  my ($self) = @_;

  # Get to work!
  my $root_pk = $self->_populate_table($self->{head});

  # Tell them what they've won...
  my $root_table_name = $self->{rdb}->mtn($self->{head}->getNodeName);
  
  # Store root_table_name and $root_pk for XML unpopulate
  $self->_populate_root_n_pk($root_table_name, $root_pk);

  return ($root_table_name, $root_pk);
}

sub _populate_root_n_pk {
  my $self = shift;
  my ($root_table_name, $root_pk) = @_;
  use vars qw(*insert);   # DBIx::Recordset deals with GLOBs
  *insert = DBIx::Recordset->Setup({
                            '!DataSource' => $self->{rdb}->{DBH},
                            '!Table' => $self->{rdb}->{ROOT_TABLE_N_PK_TABLE},
                                  });
  $insert->Insert({ root => $root_table_name, pk => $root_pk });

  $insert->Flush(); 
  DBIx::Recordset::Undef ('*insert');
  return $self;
}

# 
# The recursive work-horse - pulls out XML data & puts into RDB
#
# So the strategy here is to build up a 'values' hash who's keys
#   are column names & values are column values.  Plain text columns
#   are what they are - 1:1 relationship columns are recursively 
#   determined and 1:N columns are recursively filled out & then stored
#   and then output once this table is completely filled - since we
#   can't fill out the 'N' tables until we know this table's primary key value,
#   which we don't know until we insert it & it gets generated!
sub _populate_table {
    my($self, $head) = @_;
    my $rdb = $self->{rdb};
    my(@stack, $values, $set_our_pk_in_table, $nodes, $sub_table_index, $db_st_name, $sub_table);
    use vars qw(*insert);   # DBIx::Recordset deals with GLOBs
    
    # NOTE : Replaced the recursive sub loop with goto's & lifo @stack,
    TOP_TBL:
    ($values, $set_our_pk_in_table) = ();

    # Get 'real' Database names for this element
    my $db_table_name = $rdb->mtn($head->getNodeName);

    # Check for attributes - easy plain text columns
    if (my $attributes = $head->getAttributes) {
        for(my $i = 0 ; $i < $attributes->getLength ; $i++) {
            my $attr = $attributes->item($i);
            my $name = XML::RDB::normalize($attr->getName);
            my $value = $attr->getValue;
            $values->{"${db_table_name}_${name}_attribute"} = $value;
        }
    }
    
    $nodes = [ $head->getChildNodes ];

    # Now created each sub-element of this element
    while ( scalar(@{$nodes})) {
        $sub_table = shift(@{$nodes});
        $db_st_name = XML::RDB::normalize($sub_table->getNodeName);

        # Text node - just a '_value' in this table
        if ($sub_table->getNodeType == XML::DOM::TEXT_NODE) {
            next if (!defined $sub_table->getNodeValue || 
                        $sub_table->getNodeValue =~ /^\s*$/);
            $values->{"${db_table_name}_value"} = $sub_table->getNodeValue;
            next;
        }

        # Note this 'if' statement is EXACTLY the same one as in MakeTables.pm
        #   used to determine what's a text element & what isn't - otherwise
        #   carnage would ensue
        if (($sub_table->getAttributes && !$sub_table->getAttributes->getLength) && 
            (!$sub_table->getChildNodes 
             || ($#{$sub_table->getChildNodes} == 1 
                 && $sub_table->getChildNodes->[0]->getNodeType == XML::DOM::TEXT_NODE))) {
            # This subtable's value is in this table for one various
            #   reason or another...

            my($val, $parent);
            if ($sub_table->hasChildNodes) {
                # if this guy has child nodes & it's in this table it
                #   must be 'cuz it only has one child node & it's
                #   a TEXT node
                $val = $sub_table->getChildNodes->[0]->getNodeValue || 'null';
            }
            else {
                # This sub table don't got no child nodes so we're
                #   only interested in if this tag is there or not
                #   & since we're here it must be here!
                $val = 'present';
            }

            # Now figure out what the name of this field is
            $parent = XML::RDB::normalize($sub_table->getNodeName);

            # We've hit bottom!
            $values->{"${db_table_name}_${parent}_value"} = $val;
        }
        else {
            # At this point we're dealing with either a 1:1 or 1:N relationship

            # XML comments also fall to here - maybe one day we'll keep 'em
            next if ($sub_table->getNodeName eq '#comment');

            # Get PK of sub table, lifo @stack replacement for recursive sub.
            #  my $sub_table_index = $self->_populate_table($sub_table);
            push(@stack, [ $head, $values, $set_our_pk_in_table, 
                           $db_table_name, $nodes, $sub_table_index, $db_st_name, $sub_table ]);
            $head = $sub_table;
            goto TOP_TBL;
            # We have the sub_table $PK, so finish this out.
            TOPLESS_TBL:  

            # Check our handy-dandy one_to_n data structure
            if ($self->{one_to_n}->{$db_table_name}{XML::RDB::normalize($sub_table->getNodeName)}) {
                # This is a 1:N reference!
                # So this table can have multiple references to $sub_table
                #   So we need to stick ourself into the $sub_table as a FK
		            #
                # Our ID in this table is called:
                #   ${db_table_name}_FK_NAME
		            #
                # So we just got the PK of the 'N' table ($sub_table_index)
                # Later when we get the PK for this table we gotta update
	            #	that row we just created with that value
	            # BUT we won't know our PK 
                #   until we've actually been totally created - see below
                #   So we'll just remember to do it 4 now...
		            #
	            # Store sub table name & it's index so later we can put
	            #	our PK in there as the FK
	            my $stn = $rdb->mtn($sub_table->getNodeName);
                $set_our_pk_in_table->{$stn}{$sub_table_index} = 1;
            }
            else {
                # Plain old 1:1 sub table - just get this table's PK
                #   & stick it in appropriate slot
                $values->{$rdb->mtn("${db_st_name}_". $rdb->{PK_NAME})} = $sub_table_index;
            }
        }
    }

    # We've completely filled out this table SO
    #   dump values into DB
    #   insert into $db_table_name %values...
    *insert = DBIx::Recordset->Setup({
                              '!DataSource' => $rdb->{DBH},
                              '!Table' => "$db_table_name",
                                    });
    # We can have a table that only has an PK_NAME value 
    #   we don't want that (it's like a <node/> entity)
    #   so fill in the 'value' column
    if (!keys %{$values}) {
        $values->{$db_table_name."_value"} = 'present';
    }

    # Add the row

    # First generate a unique ID for this table
#    my $PK = $self->generate_id($db_table_name);
    my $PK = ++${$self->{_TABLE_PKS}{$db_table_name}};

    $values->{$rdb->{PK_NAME}} = $PK;
    # And add record
    $insert->Insert($values);

	# Now we just want to add a value into an existing record
	#	namely our PK in any 1:N table relationships
    foreach my $sub_table_name (keys %{$set_our_pk_in_table}) {
        foreach my $FPK (keys %{$set_our_pk_in_table->{$sub_table_name}}) {

		    # Set up values
		    my (%insert);
		    $insert{$rdb->{PK_NAME}} = $FPK;
           	$insert{$db_table_name ."_". $rdb->{FK_NAME}} = $PK;

            # And update the table with our PK in its FK column
           	DBIx::Recordset->Update({%insert, (
                                    '!DataSource' => $rdb->{DBH}, 
                                    '!Table' => $sub_table_name,
	        	                    '!PrimKey' => $rdb->{PK_NAME}
                                              )});
		    }
	  }
            
    $insert->Flush(); 

    if (scalar(@stack) > 0) {
      ($head, $values, $set_our_pk_in_table, $db_table_name, $nodes, 
       $sub_table_index, $db_st_name, $sub_table) = @{pop(@stack)};
      $sub_table_index = $PK; 
      goto TOPLESS_TBL;
    }

    DBIx::Recordset::Undef ('*insert');
    # and return our PK - simple enough!
    return $PK
}

##
# Handy dandy sub to generate unique IDs using DBIx::Sequence
#   based on table name
##
#sub generate_id
#{
#	my($self, $table_name) = @_;
#	$self->{sequence}->Next($table_name);
#}

1;