package UR::Object; # deprecated parts of the UR::Object API use warnings; use strict; require UR; our $VERSION = "0.39"; # UR $VERSION; use Data::Dumper; use Scalar::Util qw(blessed); sub get_with_special_parameters { # When overridden, this allows a class to take non-properties as parameters # to get(), and handle loading in a special way. Ideally this is handled by # a custom data source, or properties with smart definitions. my $class = shift; my $rule = shift; Carp::confess( "Unknown parameters to $class get(). " . "Implement get_with_special_parameters() to handle non-standard" . " (non-property) query options.\n" . "The special params were " . Dumper(\@_) . "Rule ID: " . $rule->id . "\n" ); } sub get_or_create { my $self = shift; return $self->get( @_ ) || $self->create( @_ ); } sub set { my $self = shift; my @rvals; while (@_) { my $property_name = shift; my $value = shift; push(@rvals, $self->$property_name($value)); } if(wantarray) { return @rvals; } else { return \@rvals; } } sub property_diff { # Ret hashref of the differences between the object and some other object. # The "other object" may be a hashref or hash, in which case it will # treat each key as a property. my ($self, $other) = @_; my $diff = {}; # If we got a hash instead of a hashref... if (@_ > 2) { shift; $other = { @_ } } no warnings; my $self_value; my $other_value; my $class_object = $self->__meta__; for my $property ($class_object->all_property_names) { if (ref($other) eq 'HASH') { next unless exists $other->{$property}; $other_value = $other->{$property}; } else { $other_value = $other->$property; } $self_value = $self->$property; $diff->{$property} = $self_value if ($other_value ne $self_value); } return $diff; } # TODO: make this a context operation sub unload { my $proto = shift; my ($self, $class); ref $proto ? $self = $proto : $class = $proto; my $cx = $UR::Context::current; if ( $self ) { # object method # The only things which can be unloaded are things committed to # their database in the exact same state. Everything else must # be reverted or deleted. return unless $self->{db_committed}; if ($self->__changes__) { #warn "NOT UNLOADING CHANGED OBJECT! $self $self->{id}\n"; return; } $self->__signal_change__('unload'); if ($ENV{'UR_DEBUG_OBJECT_RELEASE'}) { print STDERR "MEM UNLOAD object $self class ",$self->class," id ",$self->id,"\n"; } $cx->_abandon_object($self); return $self; } else { # class method # unload the objects in the class # where there are subclasses of the class # delegate to them my @unloaded; # unload all objects of this class my @involved_classes = ( $class ); for my $obj ($cx->all_objects_loaded_unsubclassed($class)) { push @unloaded, $obj->unload; } # unload any objects that belong to any subclasses for my $subclass ($cx->__meta__->subclasses_loaded($class)) { push @involved_classes, $subclass; push @unloaded, $subclass->unload; } # get rid of the loading info matching this class foreach my $template_id ( keys %$UR::Context::all_params_loaded ) { if (UR::BoolExpr::Template->get($template_id)->subject_class_name->isa($class)) { delete $UR::Context::all_params_loaded->{$template_id}; } } # Turn off the all_objects_are_loaded flags delete @$UR::Context::all_objects_are_loaded{@involved_classes}; return @unloaded; } } # TODO: replace internal calls to go right to the context method sub is_loaded { # this is just here for backward compatability for external calls # get() now goes to the context for data # This shortcut handles the most common case rapidly. # A single ID is passed-in, and the class name used is # not a super class of the specified object. # This logic is in both get() and is_loaded(). my $quit_early = 0; if ( @_ == 2 && !ref($_[1]) ) { unless (defined($_[1])) { Carp::confess(); } my $obj = $UR::Context::all_objects_loaded->{$_[0]}->{$_[1]}; return $obj if $obj; # we could safely return nothing right now, except # that a subclass of this type may have the object return unless $_[0]->__meta__->subclasses_loaded; # nope, there were no subclasses } my $class = shift; my $rule = UR::BoolExpr->resolve_normalized($class,@_); return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); } sub subclasses_loaded { return shift->__meta__->subclasses_loaded(); } # THESE SHOULD PROBABLY GO ON THE CLASS META sub all_objects_are_loaded { # Keep track of which classes claim that they are completely loaded, and that no more loading should be done. # Classes which have the above function return true should set this after actually loading everything. # This class will do just that if it has to load everything itself. my $class = shift; #$meta = $class->__meta__; if (@_) { # Setting the attribute $UR::Context::all_objects_are_loaded->{$class} = shift; } elsif (! exists $UR::Context::all_objects_are_loaded->{$class}) { # unknown... ask the parent classes and remember the answer foreach my $parent_class ( $class->inheritance ) { if (exists $UR::Context::all_objects_are_loaded->{$parent_class}) { $UR::Context::all_objects_are_loaded->{$class} = $UR::Context::all_objects_are_loaded->{$parent_class}; last; } } } return $UR::Context::all_objects_are_loaded->{$class}; } # Observer pattern (old) sub create_subscription { my $self = shift; my %params = @_; # parse parameters my ($class,$id,$method,$callback,$note,$priority); my %observer_params; @observer_params{'aspect','callback','note','priority','subject_id'} = delete @params{'method','callback','note','priority','id'}; $observer_params{'subject_class_name'} = $self->class; $observer_params{'priority'} = 1 unless defined $observer_params{'priority'}; if (!defined $observer_params{'subject_id'} and ref($self)) { $observer_params{'subject_id'} = $self->id; } if (my @unknown = keys %params) { Carp::croak "Unknown options @unknown passed to create_subscription!"; } # validate if (my @bad_params = %params) { Carp::croak "Bad params passed to add_listener: @bad_params"; } my $observer = UR::Observer->create(%observer_params); return unless $observer; return [@observer_params{'subject_class_name','subject_id','aspect','callback','note'}]; } sub validate_subscription { return 1; my ($self,$subscription_property) = @_; Carp::confess("The _create_object and _delete_object signals are no longer emitted!") if defined($subscription_property) and ($subscription_property eq '_create_object' or $subscription_property eq '_delete_object'); # Undefined attributes indicate that the subscriber wants any changes at all to generate a callback. return 1 if (!defined($subscription_property)); # All standard creation and destruction methods emit a signal. return 1 if ($subscription_property =~ /^(create|delete|commit|rollback|load|unload|load_external)$/); # A defined attribute in our property list indicates the caller wants callbacks from our properties. my $class_object = $self->__meta__; for my $property ($class_object->all_property_names) { return 1 if $property eq $subscription_property; } return 1 if ($class_object->_is_valid_signal($subscription_property)); # Bad subscription request. return; } sub inform_subscription_cancellation { # This can be overridden in derived classes if the class wants to know # when subscriptions are cancelled. return 1; } sub cancel_change_subscription ($@) { my ($class,$id,$property,$callback,$note); if (@_ >= 4) { ($class,$id,$property,$callback,$note) = @_; die "Bad parameters." if ref($class); } elsif ( (@_==3) or (@_==2) ) { ($class, $property, $callback) = @_; if (ref($_[0])) { $class = ref($_[0]); $id = $_[0]->id; } } else { die "Bad parameters."; } my %params; if (defined $class) { $params{'subject_class_name'} = $class; } if (defined $id) { $params{'subject_id'} = $id; } if (defined $property) { $params{'aspect'} = $property; } if (defined $callback) { $params{'callback'} = $callback; } if (defined $note) { $params{'note'} = $note; } my @observers = UR::Observer->get(%params); return unless @observers; if (@observers > 1) { Carp::croak('Matched more than one observer within cancel_change_subscription(). Params were: ' . join(', ', map { "$_ => " . $params{$_} } keys %params)); } $observers[0]->delete(); } # This should go away when we shift to fully to a transaction log for deletions. sub ghost_class { my $class = $_[0]->class; $class = $class . '::Ghost'; return $class; } package UR::ModuleBase; # Method for setting a callback using the old, non-command messaging API =pod =item message_callback $sub_ref = UR::ModuleBase->message_callback($type); UR::ModuleBase->message_callback($type, $sub_ref); This method returns and optionally sets the subroutine that handles messages of a specific type. =cut ## set or return a callback that has been created for a message type sub message_callback { my $self = shift; my ($type, $callback) = @_; my $methodname = $type . '_messages_callback'; if (!$callback) { # to clear the old, deprecated non-command messaging API callback return UR::Object->$methodname($callback); } my $wrapper_callback = sub { my($obj,$msg) = @_; my $obj_class = $obj->class; my $obj_id = (ref($obj) ? ($obj->can("id") ? $obj->id : $obj) : $obj); my $message_package = $type . '_package'; my $message_object = UR::ModuleBase::Message->create ( text => $msg, level => 1, package_name => $obj->$message_package(), call_stack => ($type eq "error" ? _current_call_stack() : []), time_stamp => time, type => $type, owner_class => $obj_class, owner_id => $obj_id, ); $callback->($message_object, $obj, $type); $_[1] = $message_object->text; }; # To support the old, deprecated, non-command messaging API UR::Object->$methodname($wrapper_callback); } sub message_object { my $self = shift; # see how we were called if (@_ < 2) { no strict 'refs'; # return the message object my ($type) = @_; my $method = $type . '_message'; my $msg_text = $self->method(); my $obj_class = $self->class; my $obj_id = (ref($self) ? ($self->can("id") ? $self->id : $self) : $self); my $msgdata = $self->_get_msgdata(); return UR::ModuleBase::Message->create ( text => $msg_text, level => 1, package_name => $msgdata->{$type . '_package'}, call_stack => ($type eq "error" ? _current_call_stack() : []), time_stamp => time, type => $type, owner_class => $obj_class, owner_id => $obj_id, ); } } foreach my $type ( UR::ModuleBase->message_types ) { my $retriever_name = $type . '_text'; my $compat_name = $type . '_message'; my $sub = sub { my $self = shift; return $self->$compat_name(); }; no strict 'refs'; *$retriever_name = $sub; } # class that stores and manages messages for the deprecated API package UR::ModuleBase::Message; use Scalar::Util qw(weaken); ##- use UR::Util; UR::Util->generate_readonly_methods ( text => undef, level => undef, package_name => undef, call_stack => [], time_stamp => undef, owner_class => undef, owner_id => undef, type => undef, ); sub create { my $class = shift; my $obj = {@_}; bless ($obj,$class); weaken $obj->{'owner_id'} if (ref($obj->{'owner_id'})); return $obj; } sub owner { my $self = shift; my ($owner_class,$owner_id) = ($self->owner_class, $self->owner_id); if (not defined($owner_id)) { return $owner_class; } elsif (ref($owner_id)) { return $owner_id; } else { return $owner_class->get($owner_id); } } sub string { my $self = shift; "$self->{time_stamp} $self->{type}: $self->{text}\n"; } sub _stack_item_params { my ($self, $stack_item) = @_; my ($function, $parameters, @parameters); return unless ($stack_item =~ s/\) called at [^\)]+ line [^\)]+\s*$/\)/); if ($stack_item =~ /^\s*([^\(]*)(.*)$/) { $function = $1; $parameters = $2; @parameters = eval $parameters; return ($function, @parameters); } else { return; } } package UR::Object; 1;