The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

package Xmldoom::Definition::SAXHandler;
use base qw(XML::SAX::Base Exporter);

use Xmldoom::Definition::Property::Simple;
use Xmldoom::Definition::Property::Object;
use Xmldoom::Definition::Property::PlaceHolder;
use Xmldoom::Criteria::XML;
use Xmldoom::Threads;
use DBIx::Romani::Query::XML::Util qw/parse_boolean/;
use XML::GDOME;
use Module::Runtime qw(use_module);
use strict;

use Data::Dumper;

our @EXPORT_OK = qw( $OBJECT_NS $OBJECT_PERL_NS );

our $OBJECT_NS      = "http://gna.org/projects/xmldoom/object";
our $OBJECT_PERL_NS = "http://gna.org/projects/xmldoom/object-perl";

# In the interest of speed and memory and Perl's poor garbage collection, we actually
# run this parser twice over the same file to get all its data.

sub new
{
	my $class = shift;
	my $args = shift;

	my $database;

	if ( ref($args) eq 'HASH' )
	{
		$database = $args->{database};
	}
	else
	{
		$database = $args;
	}

	my $self = {
		database => $database,
		phase    => 1,

		# for phase 2
		cur_obj     => undef,
		prop_name   => undef,
		prop_type   => undef,
		prop_args   => undef,
		dom_doc     => undef,
		dom_stack   => [],
		ignore_obj  => 0,
		in_criteria => 0,
	};

	bless  $self, $class;
	return $self;
}

sub start_document 
{
	my ($self, $doc) = @_;

	if ( $self->{phase} > 2 )
	{
		die "Cannot run more than twice on the same input";
	}
}

sub end_document 
{
	my ($self, $doc) = @_;

	# move to next phase
	$self->{phase}++;
}

sub start_element 
{
	my ($self, $el) = @_;

	# simple aliases
	my $name = $el->{'LocalName'};
	my $attrs = $el->{'Attributes'};

	# automatically return if we are ignoring the object
	if ( $self->{ignore_obj} )
	{
		return;
	}

	# if we are in criteria, we operate in super quaazy mode
	if ( $self->{in_criteria} )
	{
		if ( $self->{phase} == 2 )
		{
			# create a new element
			my $element = $self->{dom_doc}->createElement($name);
			while ( my ($key, $value) = each %$attrs )
			{
				# remove crazy NS symbols that Perl SAX uses.
				$key =~ s/^\{\}//;

				$element->setAttribute($key, $value->{Value});
			}
			
			# add to parent and push to stack
			$self->{dom_stack}->[-1]->appendChild($element);
			push @{$self->{dom_stack}}, $element;
		}

		# drop the monkey and run!
		return;
	}
	
	if ( $name eq 'objects' )
	{
		# just let pass through
	}
	elsif ( $name eq 'object' and not $self->{prop_name} )
	{
		if ( $self->{phase} == 1 )
		{
			# actually create the object in the definition, and attach
			# it to the appropriate table.
			my $object_name = $attrs->{'{}name'}->{Value};
			my $table_name  = $attrs->{'{}table'}->{Value};

			if ( $self->{database}->has_table( $table_name ) )
			{
				my $object = $self->{database}->create_object( $object_name, $table_name );

				# set the Perl class
				if ( $attrs->{"{$OBJECT_PERL_NS}class"} )
				{
					Xmldoom::Object::BindToObject( $attrs->{"{$OBJECT_PERL_NS}class"}->{Value}, $object );
				}
			}
			else
			{
				print STDERR "WARNING: Can't find table '$table_name' in the database schema, so we are ignoring the '$object_name' object definition\n";
			}
		}
		elsif ( $self->{phase} == 2 )
		{
			# retrieve and set as the current object
			my $object_name = $attrs->{'{}name'}->{Value};

			if ( $self->{database}->has_object( $object_name ) )
			{
				$self->{cur_obj} = $self->{database}->get_object( $object_name );
				$self->{ignore_obj} = 0;
			}
			else
			{
				$self->{ignore_obj} = 1;
			}
		}
	}
	elsif ( $name eq 'property' )
	{
		$self->{prop_name} = $attrs->{'{}name'}->{Value};
		if ( not $self->{prop_name} )
		{
			die "Invalid property name: '$self->{prop_name}'";
		}

		my $args = { 
			parent  => $self->{cur_obj},
			name    => $self->{prop_name},
			shared  => Xmldoom::Threads::is_shared($self->{database}),
		};

		if ( defined $attrs->{'{}description'} )
		{
			$args->{description} = $attrs->{'{}description'}->{Value};
		}
		if ( defined $attrs->{'{}searchable'} )
		{
			$args->{searchable} = parse_boolean( $attrs->{'{}searchable'}->{Value} );
		}
		if ( defined $attrs->{'{}reportable'} )
		{
			$args->{reportable} = parse_boolean( $attrs->{'{}reportable'}->{Value} );
		}
		if ( defined $attrs->{'{}get_name'} )
		{
			$args->{get_name} = $attrs->{'{}get_name'}->{Value};
		}
		if ( defined $attrs->{'{}set_name'} )
		{
			$args->{set_name} = $attrs->{'{}set_name'}->{Value};
		}

		$self->{prop_args} = $args;
	}
	elsif ( $name eq 'simple' or $name eq 'custom' or ( $name eq 'object' and $self->{prop_name} ) )
	{
		if ( $self->{phase} == 1 )
		{
			# we skip over these in phase 1 !!
			return;
		}

		# convenience
		my $args = $self->{prop_args};

		# do the specifics
		if ( $name eq 'simple' )
		{
			if ( defined $attrs->{'{}attribute'} )
			{
				$args->{attribute} = $attrs->{'{}attribute'}->{Value};
			}
		}
		elsif ( $name eq 'object' )
		{
			$args->{object_name} = $attrs->{'{}name'}->{Value};

			if ( defined $attrs->{'{}inter_table'} )
			{
				$args->{inter_table} = $attrs->{'{}inter_table'}->{Value};
			}
		
		}
		elsif ( $name eq 'custom' )
		{
			if ( defined $attrs->{"{$OBJECT_PERL_NS}class"} )
			{
				$args->{perl_class} = $attrs->{"{$OBJECT_PERL_NS}class"}->{Value};
			}
		}

		# set our property name
		$self->{prop_type} = $name;
	}
	elsif ( $name eq 'trans' )
	{
		if ( $self->{phase} == 1 )
		{
			# we skip over these in phase 1 !!
			return;
		}

		if ( $self->{prop_type} ne 'simple' )
		{
			die "<trans/> tag must be inside of <simple> property";
		}

		my $dir  = $attrs->{'{}dir'}->{Value} || 'both';
		my $from = $attrs->{'{}from'}->{Value};
		my $to   = $attrs->{'{}to'}->{Value};

		if ( $dir eq 'out' or $dir eq 'both' )
		{
			if ( not defined $self->{prop_args}->{trans_from} )
			{
				$self->{prop_args}->{trans_from} = { };
			}
			$self->{prop_args}->{trans_from}->{$from} = $to;
		}

		if ( $dir eq 'both' )
		{
			# reverse these to fill in the next section
			my $temp = $from;
			$from = $to;
			$to = $temp;
		}

		if ( $dir eq 'in' or $dir eq 'both' )
		{
			if ( not defined $self->{prop_args}->{trans_to} )
			{
				$self->{prop_args}->{trans_to} = { };
			}
			$self->{prop_args}->{trans_to}->{$from} = $to;
		}
	}
	elsif ( $name eq 'options' )
	{
		if ( $self->{phase} == 1 )
		{
			# we skip over these in phase 1 !!
			return;
		}

		if ( exists $self->{prop_args}->{options} )
		{
			die "<options> tag cannot be defined twice";
		}

		# set us up
		$self->{prop_args}->{options} = [ ];
		if ( defined $attrs->{'{}inclusive'} )
		{
			$self->{prop_args}->{inclusive} = parse_boolean( $attrs->{'{}inclusive'}->{Value} );
		}
		if ( defined $attrs->{'{}dependent'} )
		{
			$self->{prop_args}->{options_dependent} = parse_boolean( $attrs->{'{}dependent'}->{Value} );
		}

		# split for the two property types
		if ( $self->{prop_type} eq 'simple' )
		{
			if ( defined $attrs->{'{}table'} )
			{
				$self->{prop_args}->{options_table} = $attrs->{'{}table'}->{Value};
			}
			if ( defined $attrs->{'{}column'} )
			{
				$self->{prop_args}->{options_column} = $attrs->{'{}column'}->{Value};
			}
		}
		elsif ( $self->{prop_type} eq 'object' )
		{
			if ( defined $attrs->{'{}property'} )
			{
				$self->{prop_args}->{options_property} = $attrs->{'{}property'}->{Value};
			}
		}
	}
	elsif ( $name eq 'criteria' )
	{
		# mark our special mode
		$self->{in_criteria} = 1;
		
		if ( $self->{phase} == 1 )
		{
			# we skip over these in phase 1 !!
			return;
		}

		if ( not exists $self->{prop_args}->{options} )
		{
			die "<criteria/> tag must be inside of <options> section";
		}

		# here we create our initial DOM document
		$self->{dom_doc} = XML::GDOME->createDocument( undef, 'criteria', undef );
		push @{$self->{dom_stack}}, $self->{dom_doc}->documentElement;
	}
	elsif ( $name eq 'option' )
	{
		if ( $self->{phase} == 1 )
		{
			# we skip over these in phase 1 !!
			return;
		}

		if ( $self->{prop_type} ne 'simple' or not exists $self->{prop_args}->{options} )
		{
			die "<option/> tag must be inside of <options> section";
		}

		my $opt   = {
			value       => $attrs->{'{}value'}->{Value},
			description => $attrs->{'{}description'}->{Value}
		};

		push @{$self->{prop_args}->{options}}, $opt;
	}
	elsif ( $name eq 'hints' )
	{
		if ( $self->{phase} == 1 )
		{
			# we skip over these in phase 1 !!
			return;
		}

		if ( exists $self->{prop_args}->{hints} )
		{
			die "Cannot nest <hints> tags";
		}

		# set us up
		$self->{prop_args}->{hints} = { };
	}
	elsif ( $name eq 'hint' )
	{
		if ( $self->{phase} == 1 )
		{
			# we skip over these in phase 1 !!
			return;
		}

		if ( not exists $self->{prop_args}->{hints} )
		{
			die "<hint/> tag must be inside of <hints> section";
		}

		my $name  = $attrs->{'{}name'}->{Value};
		my $value = $attrs->{'{}value'}->{Value};

		if ( not defined $value )
		{
			$value = 1;
		}

		if ( defined $self->{prop_args}->{$name} )
		{
			die "Cannot defined multiple property hint's named '$name'";
		}

		$self->{prop_args}->{hints}->{$name} = $value;
	}
	elsif ( $name eq 'key' )
	{
		if ( $self->{phase} == 1 )
		{
			# we skip over these in phase 1 !!
			return;
		}

		if ( $self->{prop_type} eq 'object' )
		{
			$self->{prop_args}->{key_attributes} = [ ];
		}
		else
		{
			die "Can only put a <key/> section inside of an <object/> property.";
		}
	}
	elsif ( $name eq 'attribute' )
	{
		if ( $self->{phase} == 1 )
		{
			# we skip over these in phase 1 !!
			return;
		}

		if ( not defined $self->{prop_args}->{key_attributes} )
		{
			die "<attribute/> tag must be inside of <key> section";
		}

		if ( defined $attrs->{'{}name'} )
		{
			push @{$self->{prop_args}->{key_attributes}}, $attrs->{'{}name'}->{Value};
		}
	}
	else
	{
		die "Unknown tag: $name";
	}
}

sub characters 
{
	my ($self, $h) = @_;

	# simple alias
	my $text = $h->{'Data'};

	if ( $self->{in_criteria} and $self->{phase} == 2 )
	{
		# add text nodes to the object on the top of the stack
		my $node = $self->{dom_doc}->createTextNode($text);
		$self->{dom_stack}->[-1]->appendChild($node);
	}
}

sub end_element 
{
	my ($self, $el) = @_;

	# simple alias
	my $name = $el->{'LocalName'};

	if ( $self->{in_criteria} )
	{
		if ( $name eq 'criteria' )
		{
			# exist freaky-deeky mode
			$self->{in_criteria} = 0;
		}

		if ( $self->{phase} == 2 )
		{
			# pop tag off the stack.
			pop @{$self->{dom_stack}};

			# now, take care of business
			if ( $name eq 'criteria' )
			{
				# parse the criteria DOM and add it to the property options.
				my $criteria = Xmldoom::Criteria::XML::parse_dom( $self->{dom_doc} );
				$self->{prop_args}->{options_criteria} = $criteria;

				# release the dom from memory
				$self->{dom_doc} = undef;
			}
		}

		return;
	}

	# clear state vars...
	if ( $name eq 'object' and not $self->{prop_name} )
	{
		$self->{cur_obj} = undef;
		$self->{ignore_obj} = 0;
	}
	elsif ( $name eq 'property' )
	{
		$self->{prop_name} = undef;
	}
	elsif ( $name eq 'simple' or $name eq 'custom' or ( $name eq 'object' and $self->{prop_name} ) )
	{
		if ( $self->{phase} == 1 or $self->{ignore_obj} )
		{
			# we skip over these in phase 1 !!
			return;
		}

		my $prop;
		
		# do the specifics
		if ( $name eq 'simple' )
		{
			# create simple!
			$prop = Xmldoom::Definition::Property::Simple->new($self->{prop_args});
		}
		elsif ( $name eq 'object' )
		{
			# create object!
			$prop = Xmldoom::Definition::Property::Object->new($self->{prop_args});
		}
		elsif ( $name eq 'custom' )
		{
			my $perl_class;

			# choose either the given class or use the PlaceHolder
			if ( defined $self->{prop_args}->{perl_class} )
			{
				$perl_class = delete $self->{prop_args}->{perl_class};
				use_module($perl_class);
			}
			else
			{
				$perl_class = 'Xmldoom::Definition::Property::PlaceHolder';
			}

			# create a place holder property
			$prop = $perl_class->new($self->{prop_args});
		}

		# add 'er!
		$self->{cur_obj}->add_property( $prop );

		# clear state
		$self->{prop_type} = undef;
	}
}

1;