package Xmldoom::Object; use Xmldoom::Definition; use Xmldoom::Object::Property; use Xmldoom::Object::Attribute; use Xmldoom::Object::LinkAttribute; use Xmldoom::ResultSet; use DBIx::Romani::Query::Function::Now; use DBIx::Romani::Query::Function::Count; use DBIx::Romani::Query::SQL::Literal; use DBIx::Romani::Query::SQL::Null; use Exception::Class::DBI; use Exception::Class::TryCatch; use Scalar::Util qw(weaken isweak); use Module::Runtime qw/ use_module /; use strict; # define our exceptions: use Exception::Class qw( Xmldoom::Object::RollbackException ); use Data::Dumper; # Connects registered class names to object definitions. We can do this # because in Perl the class namespace is global. our %OBJECTS; # this will bind this class to this table sub BindToObject { my $class = shift; my $object = shift; # assign this class name to this object $object->set_class( $class ); # store the definition to classname connection for future reference $OBJECTS{$class} = $object; } sub load { my $class = shift; # The object definition does all of the actual work with regard to # querying the database and getting the data. We just pass it along # to the correct Perl class. if ( not defined $OBJECTS{$class} ) { die "Cannot load() $class: No definition attached to Perl class"; } my $definition = $OBJECTS{$class}; my $data = $definition->load( @_ ); my $result = $class->new(undef, { data => $data }); # call user hook $result->_on_load(); return $result; } sub load_or_new { my ($class, $args) = @_; my $obj; try eval { $obj = $class->load( $args ); }; if ( my $err = catch ) { $obj = $class->new(); foreach my $key_name ( @{$obj->_get_key_names()} ) { if ( defined $args->{$key_name} ) { $obj->_set_attr( $key_name, $args->{$key_name} ); } } } return $obj; } sub SearchRS { my $class = shift; my $criteria = shift; # if no criteria, then we want to get all items if ( not defined $criteria ) { $criteria = Xmldoom::Criteria->new(); } # The object definition is responsible for performing the actual query # and getting a Roma result-set back for us. my $definition = $OBJECTS{$class}; my $rs = $definition->search_rs( $criteria ); # return our fully prepared result set return Xmldoom::ResultSet->new({ class => $class, result => $rs, conn => $rs->get_conn(), parent => $criteria->get_parent() }); } sub Search { my $class = shift; my $rs = $class->SearchRS( @_ ); my @ret; # unravel our result set while ( $rs->next() ) { push @ret, $rs->get_object(); } return wantarray ? @ret : \@ret; } sub SearchAttrsRS { my $class = shift; my $criteria = shift; # if no criteria, then we want to get all items if ( not defined $criteria ) { $criteria = Xmldoom::Criteria->new(); } return $OBJECTS{$class}->search_attrs_rs( $criteria, @_ ); } sub SearchAttrs { my $class = shift; my $rs = $class->SearchAttrsRS( @_ ); my @ret; # unravel our result set while ( $rs->next() ) { push @ret, $rs->get_row(); } # TODO: Some reference is being held somewhere! I can't # seem to figure this one out. $rs->{conn}->disconnect(); #$rs = undef; return wantarray ? @ret : \@ret; } sub SearchDistinctAttrsRS { my $class = shift; my $criteria = shift; # if no criteria, then we want to get all items if ( not defined $criteria ) { $criteria = Xmldoom::Criteria->new(); } return $OBJECTS{$class}->search_distinct_attrs_rs( $criteria, @_ ); } sub SearchDistinctAttrs { my $class = shift; my $rs = $class->SearchDistinctAttrsRS( @_ ); my @ret; # unravel our result set while ( $rs->next() ) { push @ret, $rs->get_row(); } return wantarray ? @ret : \@ret; } sub Count { my $class = shift; my $criteria = shift; # if no criteria, then we want to get all items if ( not defined $criteria ) { $criteria = Xmldoom::Criteria->new(); } return $OBJECTS{$class}->count( $criteria ); } sub new { my $class = shift; my $public_args = shift; my $private_args = shift; my $parent; my $parent_link; my $data; my $sets; if ( ref($private_args) eq "HASH" ) { $parent = $private_args->{parent}; $parent_link = $private_args->{parent_link}; $data = $private_args->{data}; } if ( ref($public_args) eq "HASH" ) { $sets = $public_args; } my $self = { parent => $parent, definition => $OBJECTS{$class}, dependents => [ ], original => { }, info => { }, key => { }, props => [ ], callbacks => { }, new => 1, }; # weaken reference to parent if ( defined $self->{parent} ) { weaken( $self->{parent} ); } # we are now an object bless $self, $class; # if we have data, then copy it into the info and key hashes. Otherwise # we should set all the default values. if ( defined $data ) { foreach my $column ( @{$self->{definition}->get_table()->get_columns()} ) { my $col_name = $column->get_name(); # put in their places $self->{info}->{$col_name} = Xmldoom::Object::Attribute->new( $data->{$col_name} ); if ( $column->is_primary_key() ) { # we need to store the keys twice so that we can pivot # on the key, if we need to change it. $self->{key}->{$col_name} = $data->{$col_name}; } } # copy info into original $self->{original} = { %$data }; # this is not a new object $self->{new} = 0; } else { # set our defaults foreach my $column ( @{$self->{definition}->get_table()->get_columns()} ) { $self->{info}->{$column->{name}} = Xmldoom::Object::Attribute->new( $column->{default} ); } } # link our attributes to the appropriate connections in the parent if ( $self->{parent} ) { if ( not defined $parent_link ) { # if they aren't specified then we guess... $parent_link = $self->{definition}->find_links( $self->{parent}->_get_object_name() )->[0]; } # TODO: a hack for inter_table where we won't have a parent link for now! if ( defined $parent_link and $parent_link->get_count() == 1 ) { foreach my $pconn ( @{$parent_link->get_column_names()} ) { $self->_link_attr( $pconn->{local_column}, $self->{parent}, $pconn->{foreign_column} ); } } } # setup the properties foreach my $prop ( @{$self->{definition}->get_properties()} ) { push @{$self->{props}}, Xmldoom::Object::Property->new( $prop, $self ); } # set the initial values if ( defined $sets ) { $self->set($sets); } return $self; } sub copy { my $self = shift; my $class = ref($self); my $copy = $class->new(); foreach my $name ( @{$self->{definition}->get_table()->get_column_names({ data_only => 1 })} ) { $copy->_set_attr( $name, $self->_get_attr($name) ); } return $copy; } sub _get_definition { return shift->{definition}; } sub _get_database { return shift->{definition}->get_database(); } sub _get_object_name { return shift->{definition}->get_name(); } sub _get_table { return shift->{definition}->get_table(); }; sub _get_key_names { return shift->_get_table()->get_column_names({ primary_key => 1 }); } sub _get_data_names { return shift->_get_table()->get_column_names({ data_only => 1 }); } sub _get_properties { return shift->{props}; } sub _get_original { return shift->{original}; } sub _get_key { return shift->{key}; } sub _get_attributes { my $self = shift; my $data = { }; while ( my ($name, $attr) = each %{$self->{info}} ) { $data->{$name} = $attr->get(); } return $data; } sub _get_property { my ($self, $name) = @_; foreach my $prop ( @{$self->_get_properties()} ) { if ( $prop->get_name() eq $name ) { return $prop; } } die "There is no property named '$name' on this object"; } sub _get_property_recursive { my ($self, $name) = @_; my $object = $self; my @stack = split /\//, $name; # go through all the sub-properties while ( @stack > 1 ) { # get the next name $name = shift @stack; foreach my $prop ( @{$object->_get_properties()} ) { if ( $prop->get_name() eq $name ) { if ( not $prop->get_definition()->isa('Xmldoom::Definition::Property::Object') or $prop->get_type() ne 'inherent' ) { die "Cannot _get_property() recursively through '$name' because it is not an inherent object property."; } # recurse, yo! $object = $prop->get(); } } } # get the final property! my $name = shift @stack; my $prop = $object->_get_property($name); # NOTE: we must return both the property and the object, because the property # will cease to be valid as soon as the object goes out of scope. return ( $object, $prop ); } sub _get_property_value { my $self = shift; my $args = shift; my $name; my $pretty = 0; if ( ref($args) eq 'HASH' ) { $name = $args->{name}; $pretty = $args->{pretty}; } else { $name = $args; $pretty = shift; } my ($object, $prop) = $self->_get_property_recursive($name); if ( $pretty ) { return $prop->get_pretty(); } else { return $prop->get(); } } sub _get_attr { my ($self, $name) = @_; my $col = $self->{definition}->get_table()->get_column( $name ); if ( not defined $col ) { die "Cannot get non-existant attribute \"$name\"."; } return $self->{info}->{$name}->get(); } sub _set_attr { my ($self, $name, $value) = @_; my $col = $self->{definition}->get_table()->get_column( $name ); if ( not defined $col ) { die "Cannot set non-existant attribute \"$name\"."; } # TODO: validate the attribute. if ( $self->{info}->{$name}->is_local() ) { # we can only set attributes that are local to us. $self->{info}->{$name}->set( $value ); } else { # if we are manually setting a link attribute, then this # overrides it setting a local attribute. $self->{info}->{$name} = Xmldoom::Object::Attribute->new( $value ); } # we are changed! $self->_changed(); } sub _link_attr { my ($self, $local_name, $object, $foreign_name) = @_; $self->{info}->{$local_name} = Xmldoom::Object::LinkAttribute->new( $object->{info}->{$foreign_name} ); } sub _register_callback { my ($self, $name, $cb) = @_; if ( not defined $self->{callbacks}->{$name} ) { $self->{callbacks}->{$name} = [ $cb ]; } else { push @{$self->{callbacks}->{$name}}, $cb; } } sub _unregister_callback { my ($self, $name, $cb) = @_; if ( defined $self->{callbacks}->{$name} ) { for( my $i = 0; $i < scalar @{$self->{callbacks}->{$name}}; $i++ ) { if ( $self->{callbacks}->{$name}->[$i] == $cb ) { splice @{$self->{callbacks}->{$name}}, $i, 1; last; } } } } sub _execute_callback { my $self = shift; my $name = shift; if ( defined $self->{callbacks}->{$name} ) { foreach my $cb ( @{$self->{callbacks}->{$name}} ) { $cb->call( $cb, @_ ); } } } sub save { my $self = shift; my $args = shift; my $commit = 1; my $conn; if ( ref($args) eq 'HASH' ) { $conn = $args->{conn}; $commit = $args->{commit} if defined $args->{commit}; } else { $conn = $args; # DRS: dumb dumb kludge -- I hate you, Perl ... my $tmp = shift; $commit = $tmp if defined $tmp; } my $status = $self->{new} ? 'insert' : 'update'; my $conn_owner = 0; if ( not defined $conn ) { $conn = $self->{definition}->create_db_connection(); $conn->begin(); # we are the connection owner (or, ALL YOUR CONNECTION ARE BELONG TO US) $conn_owner = 1; $commit = 1; } try eval { # call the user handler $self->_before_save( $status ); # save yourself! $self->do_save( $conn ); # loop through child references and call save() if ( defined $self->{dependents} ) { while ( scalar @{$self->{dependents}} ) { #my $child = shift @{$self->{dependents}}; my $child = $self->{dependents}->[0]; $child->save({ conn => $conn, commit => 0 }); shift @{$self->{dependents}}; } } # if an exception isn't thrown, we assume that all is well and commit $conn->commit() if $commit; }; my $error = catch; if ( $error ) { # make sure we are not attempting to rollback multiple times from the # same transaction. if ( not $error->isa( 'Xmldoom::Object::RollbackException' ) ) { # on the condition of error, we rollback() !! $conn->rollback() if $conn; # change the error to RollbackException so that the calling code knows # that we have already rollback()'d. $error = Xmldoom::Object::RollbackException->new( error => $error ); } } $conn->disconnect() if $conn and $conn_owner; $error->rethrow() if $error; # call the user handler $self->_on_save( $status ); # copy current values into the orginals stuff $self->{original} = $self->_get_attributes(); # call the user callbacks $self->_execute_callback("onsave", $self, $status); } sub do_save { my ($self, $conn) = @_; my $definition = $self->{definition}; my $table = $definition->get_table(); my $table_name = $definition->get_table_name(); my $query; my $values = { }; my $id_gen = { }; if ( $self->{new} ) { $query = $definition->get_insert_query(); foreach my $column ( @{$table->get_columns()} ) { my $col_name = $column->get_name(); if ( $self->{info}->{$col_name}->is_local() and not defined $self->{info}->{$col_name}->get() ) { # if the value is not defined, special behavior is required for # some special types. if ( $column->is_primary_key() and ($column->is_auto_increment or $column->get_id_generator()) ) { if ( $column->is_auto_increment() ) { # use the default connection id generator $id_gen->{$col_name} = $conn->create_id_generator(); } else { # use the module, yo! use_module($column->get_id_generator()); # use the custom id generator $id_gen->{$col_name} = $column->get_id_generator()->new({ conn => $conn, object => $self, table_name => $table_name, column_name => $col_name }); } if ( $id_gen->{$col_name}->is_before_insert() ) { my $id = $id_gen->{$col_name}->get_id(); # stash the contents of the id in the info hash $self->{info}->{$col_name}->set( $id ); # put our newly found value into the query $values->{$col_name} = DBIx::Romani::Query::SQL::Literal->new( $id ); # discard the id generator because this is already # taken care of. $id_gen->{$col_name} = undef; } else { # insert null, and grab the id from the id generator # after the insert. $values->{$col_name} = DBIx::Romani::Query::SQL::Null->new(); } } elsif ( $column->get_timestamp() ) { $values->{$col_name} = DBIx::Romani::Query::Function::Now->new(); } else { # else, insert a NULL! $values->{$col_name} = DBIx::Romani::Query::SQL::Null->new(); } } else { # straigt simple value... $values->{$col_name} = DBIx::Romani::Query::SQL::Literal->new( $self->_get_attr($col_name) ); } } } else { $query = $definition->get_update_query(); foreach my $column ( @{$table->get_columns()} ) { my $col_name = $column->get_name(); # add the primary key if ( $column->is_primary_key() ) { $values->{"key.$col_name"} = DBIx::Romani::Query::SQL::Literal->new( $self->{key}->{$col_name} ); } if ( $column->get_timestamp() eq 'current' ) { $values->{$col_name} = DBIx::Romani::Query::Function::Now->new(); } else { # ... and the normal values $values->{$col_name} = DBIx::Romani::Query::SQL::Literal->new( $self->_get_attr($col_name) ); } } } # execute, yo! #printf "save(): %s\n", $conn->generate_sql( $query, $values ); $conn->prepare( $query )->execute( $values ); # copy from the info, into the key, either for a newly db'd object or # for the primary key pivot. foreach my $col_name ( @{$table->get_column_names({ primary_key => 1 })} ) { if ( defined $id_gen->{$col_name} ) { # we saved the id generator because its a get # after insert. So, get, now... my $id = $id_gen->{$col_name}->get_id(); $self->{key}->{$col_name} = $id; $self->{info}->{$col_name}->set( $id ); } else { $self->{key}->{$col_name} = $self->{info}->{$col_name}->get(); } } if ( $self->{new} ) { $self->{new} = 0; } } sub _before_save { my ($self, $type) = @_; # Virtual. } sub _on_save { my ($self, $type) = @_; # Virtual. } sub _on_load { my ($self, $type) = @_; # Virtual. } sub delete { my $self = shift; # TODO: cascading deletes are cool too... my $definition = $self->{definition}; my $table = $definition->get_table(); my $query = $definition->get_delete_query(); my %values; foreach my $column ( @{$table->get_columns({ primary_key => 1 })} ) { $values{$column->{name}} = DBIx::Romani::Query::SQL::Literal->new( $self->{key}->{$column->{name}} ); } my $conn = $definition->create_db_connection(); # TODO: add error checking if ever we implement cascading deletes $conn->prepare( $query )->execute( \%values ); $conn->disconnect(); } # a private function that adds a child to list of dependent objects. This should only # be called by the child itself when it has changed. sub _add_dependent { my ($self, $child) = @_; # don't double add children foreach my $dep ( @{$self->{dependents}} ) { if ( $child == $dep ) { return; } } push @{$self->{dependents}}, $child; } # manually marks this object as changed sub _changed { my $self = shift; # we tell our parent that we are modified if ( defined $self->{parent} ) { $self->{parent}->_add_dependent($self); } } sub set { my $self = shift; my $args = shift; foreach my $prop ( @{$self->{props}} ) { my $prop_name = $prop->get_name(); if ( exists $args->{$prop_name} ) { $prop->set( $args->{$prop_name} ); delete $args->{$prop_name}; } } if ( scalar keys %$args ) { my $unknown = join ", ", keys %$args; die "Unknown properties: $unknown"; } } sub get { my $self = shift; my $values = { }; foreach my $prop ( @{$self->{props}} ) { $values->{$prop->get_name()} = $prop->get(); } return $values; } sub AUTOLOAD { my $self = shift; my $function = our $AUTOLOAD; if ( defined $self and $self->isa('Xmldoom::Object') ) { # remove the package name $function =~ s/.*:://; foreach my $prop ( @{$self->{props}} ) { foreach my $autoload_name ( @{$prop->get_autoload_list()} ) { if ( $function eq $autoload_name ) { return $prop->autoload( $function, @_ ); } } } } die sprintf "%s not a valid property function of %s.", $function, ref($self); } sub DESTROY { # TODO: some kind of clean-up? } 1; __END__ =pod =head1 NAME Xmldoom::Object =head1 SYNOPSIS # Assuming that 'MyObject' is a child of (->isa) Xmldoom::Object use MyObject; =head1 DESCRIPTION This is the base class for all Xmldoom managed classes. It defines their interfaces and the how they may be extended.