package UR::Object::Type; use warnings; use strict; require UR; our $VERSION = "0.37"; # UR $VERSION; use Sys::Hostname; use Cwd; use Scalar::Util qw(blessed); use Sub::Name; our %meta_classes; our $bootstrapping = 1; our @partially_defined_classes; our $pwd_at_compile_time = cwd(); # each method which caches data on the class for properties stores its hash key here # when properties mutate this is cleared our @cache_keys; sub property_metas { my $self = $_[0]; my @a = map { $self->property_meta_for_name($_) } $self->all_property_names(); return @a; } # Some accessor methods drawn from properties need to be overridden. # Some times because they need to operate during bootstrapping. Sometimes # because the method needs some special behavior like sorting or filtering. # Sometimes to optimize performance or cache data # This needs to remain overridden to enforce the restriction on callers sub data_source { my $self = shift; my $ds = $self->data_source_id(@_); return undef unless $ds; my $obj = UR::DataSource->get($ds) || $ds->get(); return $obj; } sub ancestry_class_metas { #my $rule_template = UR::BoolExpr::Template->resolve(__PACKAGE__,'id'); # Can't use the speed optimization of getting a template here. Using the Context to get # objects here causes endless recursion during bootstrapping map { __PACKAGE__->get($_) } shift->ancestry_class_names; #return map { $UR::Context::current->get_objects_for_class_and_rule(__PACKAGE__, $_) } # map { $rule_template->get_rule_for_values($_) } # shift->ancestry_class_names; } our $PROPERTY_META_FOR_NAME_TEMPLATE; push @cache_keys, '_property_meta_for_name'; sub property_meta_for_name { my ($self, $property_name) = @_; if (index($property_name,'.') != -1) { my @chain = split(/\./,$property_name); my $last_class_meta = $self; my $last_class_name = $self->id; my @pmeta; for my $full_link (@chain) { my ($link) = ($full_link =~ /^([^\-\?]+)/); my $property_meta = $last_class_meta->property_meta_for_name($link); push @pmeta, $property_meta; last if $link eq $chain[-1]; my @joins = UR::Object::Join->resolve_chain($last_class_name, $link); return unless @joins; $last_class_name = $joins[-1]{foreign_class}; $last_class_meta = $last_class_name->__meta__; } return unless (@pmeta and $pmeta[-1]); return @pmeta if wantarray; return $pmeta[-1]; } my $pos = index($property_name,'-'); if ($pos != -1) { $property_name = substr($property_name,0,$pos); } if (exists($self->{'_property_meta_for_name'}) and $self->{'_property_meta_for_name'}->{$property_name}) { return $self->{'_property_meta_for_name'}->{$property_name}; } $PROPERTY_META_FOR_NAME_TEMPLATE ||= UR::BoolExpr::Template->resolve('UR::Object::Property', 'class_name', 'property_name'); my $property; for my $class ($self->class_name, $self->ancestry_class_names) { my $rule = $PROPERTY_META_FOR_NAME_TEMPLATE->get_rule_for_values($class, $property_name); $property = $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Property', $rule); if ($property) { return $self->{'_property_meta_for_name'}->{$property_name} = $property; } } return; } # A front-end for property_meta_for_name, but # will translate the generic 'id' property into the class' real ID property, # if it's not called 'id' sub _concrete_property_meta_for_class_and_name { my($self,$property_name) = @_; my $property_meta = $self->property_meta_for_name($property_name); if ($property_meta and $property_meta->class_name eq 'UR::Object' and $property_meta->property_name eq 'id') { # This is the generic id property. Remap it to the class' real ID property name my @id_properties = $self->id_property_names; if (@id_properties == 1 and $id_properties[0] eq 'id') { return $property_meta; } return map { $self->_concrete_property_meta_for_class_and_name($_) } @id_properties; } return $property_meta; } sub _flatten_property_name { my ($self, $name) = @_; my $flattened_name = ''; my @add_keys; my @add_values; my @meta = $self->property_meta_for_name($name); for my $meta (@meta) { my @joins = $meta->_resolve_join_chain(); for my $join (@joins) { if ($flattened_name) { $flattened_name .= '.'; } $flattened_name .= $join->{source_name_for_foreign}; if (my $where = $join->{where}) { $flattened_name .= '-' . $join->sub_group_label; my $join_class = $join->{foreign_class}; my $bx2 = UR::BoolExpr->resolve($join_class,@$where); my $bx2_flat = $bx2->flatten(); # recurses through this my ($bx2_flat_template, @values) = $bx2_flat->template_and_values(); my @keys = @{ $bx2_flat_template->{_keys} }; for my $key (@keys) { next if substr($key,0,1) eq '-'; my $full_key = $flattened_name . '?.' . $key; push @add_keys, $full_key; push @add_values, shift @values; } if (@values) { Carp:confess("Unexpected mismatch in count of keys and values!"); } } } } return ($flattened_name, \@add_keys, \@add_values); }; our $DIRECT_ID_PROPERTY_METAS_TEMPLATE; sub direct_id_property_metas { my $self = _object(shift); $DIRECT_ID_PROPERTY_METAS_TEMPLATE ||= UR::BoolExpr::Template->resolve('UR::Object::Property', 'class_name', 'property_name', 'is_id >='); my $class_name = $self->class_name; my @id_property_objects = map { $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Property', $_) } map { $DIRECT_ID_PROPERTY_METAS_TEMPLATE->get_rule_for_values($class_name, $_, 0) } @{$self->{'id_by'}}; my $sort_sub = sub ($$) { return $_[0]->is_id cmp $_[1]->is_id }; @id_property_objects = sort $sort_sub @id_property_objects; if (@id_property_objects == 0) { @id_property_objects = $self->property_meta_for_name("id"); } return @id_property_objects; } sub parent_class_names { my $self = shift; return @{ $self->{is} }; } sub id_property_names { # FIXME Take a look at id_property_names and all_id_property_names. # They look extremely similar, but tests start dying if you replace one # with the other, or remove both and rely on the property's accessor method my $self = _object(shift); my @id_by; #my $template = UR::BoolExpr::Template->resolve('UR::Object::Type', 'class_name'); unless ($self->{id_by} and @id_by = @{ $self->{id_by} }) { foreach my $parent ( @{ $self->{'is'} } ) { my $parent_class = UR::Object::Type->get(class_name => $parent); #my $rule = $template->get_rule_for_values($parent); #my $parent_class = $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Type', $rule); next unless $parent_class; @id_by = $parent_class->id_property_names; last if @id_by; } } return @id_by; } sub all_id_property_names { # return shift->id_property_names(@_); This makes URT/t/99_transaction.t fail my $self = shift; unless ($self->{_all_id_property_names}) { my ($tmp,$last) = ('',''); $self->{_all_id_property_names} = [ grep { $tmp = $last; $last = $_; $tmp ne $_ } sort map { @{ $_->{id_by} } } map { __PACKAGE__->get($_) } ($self->class_name, $self->ancestry_class_names) ]; } return @{ $self->{_all_id_property_names} }; } sub direct_id_column_names { my $self = _object(shift); my @id_column_names = map { $_->column_name } $self->direct_id_property_metas; return @id_column_names; } sub ancestry_table_names { my $self = _object(shift); my @inherited_table_names = grep { defined($_) } map { $_->table_name } $self->ancestry_class_metas; return @inherited_table_names; } sub all_table_names { my $self = _object(shift); my @table_names = grep { defined($_) } ( $self->table_name, $self->ancestry_table_names ); return @table_names; } sub ancestry_class_names { my $self = shift; if ($self->{_ordered_inherited_class_names}) { return @{ $self->{_ordered_inherited_class_names} }; } my $ordered_inherited_class_names = $self->{_ordered_inherited_class_names} = [ @{ $self->{is} } ]; my @unchecked = @$ordered_inherited_class_names; my %seen = ( $self->{class_name} => 1 ); while (my $ancestor_class_name = shift @unchecked) { next if $seen{$ancestor_class_name}; $seen{$ancestor_class_name} = 1; my $class_meta = $ancestor_class_name->__meta__; Carp::confess("Can't find meta for $ancestor_class_name!") unless $class_meta; next unless $class_meta->{is}; push @$ordered_inherited_class_names, @{ $class_meta->{is} }; unshift @unchecked, $_ for reverse @{ $class_meta->{is} }; } return @$ordered_inherited_class_names; } push @cache_keys, '_all_property_names'; sub all_property_names { my $self = shift; if ($self->{_all_property_names}) { return @{ $self->{_all_property_names} }; } my %seen = (); my $all_property_names = $self->{_all_property_names} = []; for my $class_name ($self->class_name, $self->ancestry_class_names) { next if $class_name eq 'UR::Object'; my $class_meta = UR::Object::Type->get($class_name); if (my $has = $class_meta->{has}) { push @$all_property_names, grep { not exists $has->{$_}{id_by} } grep { !exists $seen{$_} } sort keys %$has; foreach (@$all_property_names) { $seen{$_} = 1; } } } return @$all_property_names; } ######################################################################## # End of overridden property methods ######################################################################## sub _resolve_meta_class_name_for_class_name { my $class = shift; my $class_name = shift; #if ($class_name->isa("UR::Object::Type") or $meta_classes{$class_name} or $class_name =~ '::Type') { if ($meta_classes{$class_name} or $class_name =~ '::Type') { return "UR::Object::Type" } else { return $class_name . "::Type"; } } sub _resolve_meta_class_name { my $class = shift; my ($rule,%extra) = UR::BoolExpr->resolve_normalized($class, @_); my %params = $rule->params_list; my $class_name = $params{class_name}; return unless $class_name; return $class->_resolve_meta_class_name_for_class_name($class_name); } # This method can go away when we have the is_cached meta-property sub first_sub_classification_method_name { my $self = shift; # This may be one of many things which class meta-data should "inherit" from classes which # its instances inherit from. This value is set to the value found on the most concrete class # in the inheritance tree. return $self->{___first_sub_classification_method_name} if exists $self->{___first_sub_classification_method_name}; $self->{___first_sub_classification_method_name} = $self->sub_classification_method_name; unless ($self->{___first_sub_classification_method_name}) { for my $parent_class ($self->ancestry_class_metas) { last if ($self->{___first_sub_classification_method_name} = $parent_class->sub_classification_method_name); } } return $self->{___first_sub_classification_method_name}; } # Another thing that is "inherited" from parent class metas sub subclassify_by { my $self = shift; return $self->{'__subclassify_by'} if exists $self->{'__subclassify_by'}; $self->{'__subclassify_by'} = $self->__subclassify_by; unless ($self->{'__subclassify_by'}) { for my $parent_class ($self->ancestry_class_metas) { last if ($self->{'__subclassify_by'} = $parent_class->__subclassify_by); } } return $self->{'__subclassify_by'}; } sub resolve_composite_id_from_ordered_values { my $self = shift; my $resolver = $self->get_composite_id_resolver; return $resolver->(@_); } sub resolve_ordered_values_from_composite_id { my $self = shift; my $decomposer = $self->get_composite_id_decomposer; return $decomposer->(@_); } sub get_composite_id_decomposer { my $self = shift; my $decomposer; unless ($decomposer = $self->{get_composite_id_decomposer}) { my @id_property_names = $self->id_property_names; if (@id_property_names == 1) { $decomposer = sub { $_[0] }; } else { my $separator = $self->_resolve_composite_id_separator; $decomposer = sub { if (ref($_[0])) { # ID is an arrayref, or we'll throw an exception. my $id = $_[0]; my $underlying_id_count = scalar(@$id); # Handle each underlying ID, turning each into an arrayref divided by property value. my @decomposed_ids; for my $underlying_id (@$id) { push @decomposed_ids, [map { $_ eq '' ? undef : $_ } split(/\t/,$underlying_id)]; } # Count the property values. my $underlying_property_count = scalar(@{$decomposed_ids[0]}) if @decomposed_ids; $underlying_property_count ||= 0; # Make a list of property values, but each value will be an # arrayref of a set of values instead of a single value. my @property_values; for (my $n = 0; $n < $underlying_property_count; $n++) { $property_values[$n] = [ map { $_->[$n] } @decomposed_ids ]; } return @property_values; } else { # Regular scalar ID. no warnings 'uninitialized'; # $_[0] can be undef in some cases... return split($separator,$_[0]) } }; } Sub::Name::subname('UR::Object::Type::InternalAPI::composite_id_decomposer(closure)',$decomposer); $self->{get_composite_id_decomposer} = $decomposer; } return $decomposer; } sub _resolve_composite_id_separator { # TODO: make the class pull this from its parent at creation time # and only have it dump it if it differs from its parent my $self = shift; my $separator = "\t"; for my $class_meta ($self, $self->ancestry_class_metas) { if ($class_meta->composite_id_separator) { $separator = $class_meta->composite_id_separator; last; } } return $separator; } sub get_composite_id_resolver { my $self = shift; my $resolver; unless($resolver = $self->{get_composite_id_resolver}) { my @id_property_names = $self->id_property_names; if (@id_property_names == 1) { $resolver = sub { $_[0] }; } else { my $separator = $self->_resolve_composite_id_separator; $resolver = sub { if (ref($_[0]) eq 'ARRAY') { # Determine how big the arrayrefs are. my $underlying_id_count = scalar(@{$_[0]}); # We presume that, if one value is an arrayref, the others are also, # and are of equal length. my @id; for (my $id_num = 0; $id_num < $underlying_id_count; $id_num++) { # One value per id_property on the class. # Each value is an arrayref in this case. for my $value (@_) { no warnings 'uninitialized'; # Some values in the list might be undef $id[$id_num] .= $separator if $id[$id_num]; $id[$id_num] .= $value->[$id_num]; } } return \@id; } else { no warnings 'uninitialized'; # Some values in the list might be undef return join($separator,@_) } }; } Sub::Name::subname('UR::Object::Type::InternalAPI::composite_id_resolver(closure)',$resolver); $self->{get_composite_id_resolver} = $resolver; } return $resolver; } # UNUSED, BUT BETTER FOR MULTI-COLUMN FK sub composite_id_list_scalar_mix { # This is like the above, but handles the case of arrayrefs # mixing with scalar values in a multi-property id. my ($self, @values) = @_; my @id_sets; for my $value (@values) { if (@id_sets == 0) { if (not ref $value) { @id_sets = ($value); } else { @id_sets = @$value; } } else { if (not ref $value) { for my $id_set (@id_sets) { $id_set .= "\t" . $value; } } else { for my $new_id (@$value) { for my $id_set (@id_sets) { $id_set .= "\t" . $value; } } } } } if (@id_sets == 1) { return $id_sets[0]; } else { return \@id_sets; } } sub id_property_sorter { # Return a closure that sort can use to sort objects by all their ID properties # This should be the same order that an SQL query with 'order by ...' would return them my $self = shift; return $self->{'_id_property_sorter'} ||= $self->sorter(); } sub sorter { #TODO: make this take +/- indications of ascending/descending #TODO: make it into a closure for speed #TODO: there are possibilities of it sorting different than a DB on mixed numbers and alpha data my ($self,@properties) = @_; push @properties, $self->id_property_names; my $key = join("__",@properties); my $sorter = $self->{_sorter}{$key}; unless ($sorter) { my @is_numeric; my @is_descending; for my $property (@properties) { if ($property =~ m/^(-|\+)(.*)$/) { push @is_descending, $1 eq '-'; $property = $2; # yes, we're manipulating the original list element } else { push @is_descending, 0; } my $pmeta; if ($self->isa("UR::Object::Set::Type")) { # If we're a set, we want to examine the property of our members. my $subject_class = $self->class_name; $subject_class =~ s/::Set$//g; $pmeta = $subject_class->__meta__->property($property); } else { $pmeta = $self->property($property); } if ($pmeta) { my $is_numeric = $pmeta->is_numeric; push @is_numeric, $is_numeric; } elsif ($UR::initialized) { Carp::cluck("Failed to find property meta for $property on $self? Cannot produce a sorter for @properties"); push @is_numeric, 0; } else { push @is_numeric, 0; } } no warnings; # don't print a warning about undef values ...alow them to be treated as 0 or '' $sorter = $self->{_sorter}{$key} ||= sub($$) { for (my $n = 0; $n < @properties; $n++) { my $property = $properties[$n]; my($first,$second) = $is_descending[$n] ? ($_[1]->$property,$_[0]->$property) : ($_[0]->$property,$_[1]->$property); if (!defined($second)) { return -1; } elsif (!defined($first)) { return 1; } my $cmp = $is_numeric[$n] ? $first <=> $second : $first cmp $second; return $cmp if $cmp; } return 0; }; } Sub::Name::subname("UR::Object::Type::sorter__" . $self->class_name . '__' . $key, $sorter); return $sorter; } sub is_meta { my $self = shift; my $class_name = $self->class_name; return grep { $_ ne 'UR::Object' and $class_name->isa($_) } keys %meta_classes; } sub is_meta_meta { my $self = shift; my $class_name = $self->class_name; return 1 if $meta_classes{$class_name}; return; } # Things that can't safely be removed from the object cache. our %uncachable_types = ( ( map { $_ => 0 } keys %UR::Object::Type::meta_classes), # meta-classes are locked in the cache... 'UR::Object' => 1, # .. except for UR::Object 'UR::Object::Ghost' => 0, 'UR::DataSource' => 0, 'UR::Context' => 0, 'UR::Object::Index' => 0, ); sub is_uncachable { my $self = shift; my $class_name = $self->class_name; unless (exists $uncachable_types{$class_name}) { foreach my $type ( keys %uncachable_types ) { if ($class_name->isa($type)) { $uncachable_types{$class_name} = $uncachable_types{$type}; last; } } unless (exists $uncachable_types{$class_name}) { die "Couldn't determine is_uncachable() for $class_name"; } } return $uncachable_types{$class_name}; } # Mechanisms for generating object IDs when none were specified at # creation time sub autogenerate_new_object_id_uuid { require Data::UUID; my $uuid = Data::UUID->new->create_hex(); $uuid =~ s/^0x//; return $uuid; } our $autogenerate_id_base_format = join(" ",Sys::Hostname::hostname(), "%s", time); # the %s gets $$ when needed our $autogenerate_id_iter = 10000; sub autogenerate_new_object_id_urinternal { my($self, $rule) = @_; my @id_property_names = $self->id_property_names; if (@id_property_names > 1) { # we really could, but it seems like if you # asked to do it, it _has_ to be a mistake. If there's a legitimate # reason, this check should be removed $self->error_message("Can't autogenerate ID property values for multiple ID property class " . $self->class_name); return; } return sprintf($autogenerate_id_base_format, $$) . " " . (++$autogenerate_id_iter); } sub autogenerate_new_object_id_datasource { my($self,$rule) = @_; my ($data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($self); if ($data_source) { return $data_source->autogenerate_new_object_id_for_class_name_and_rule( $self->class_name, $rule ); } else { Carp::croak("Class ".$self->class." has id_generator '-datasource', but the class has no data source to delegate to"); } } # Support the autogeneration of unique IDs for objects which require them. sub autogenerate_new_object_id { my $self = shift; my $rule = shift; my $id_generator = $self->id_generator; if (ref($id_generator) eq 'CODE') { return $id_generator->($self,$rule); } elsif ($id_generator and $id_generator =~ m/^\-(\S+)/) { my $id_method = 'autogenerate_new_object_id_' . $1; unless ($self->can($id_method)) { Carp::croak("'$id_generator' is an invalid id_generator for class " . $self->class_name . ": Can't locate object method '$id_method' via package ".ref($self)); } return $self->$id_method($rule); } else { # delegate to the data source my ($data_source) = $UR::Context::current->resolve_data_sources_for_class_meta_and_rule($self); if ($data_source) { return $data_source->autogenerate_new_object_id_for_class_name_and_rule( $self->class_name, $rule ) } } } # from ::Object->generate_support_class our %support_class_suffixes = map { $_ => 1 } qw/Set View Viewer Ghost Iterator Value/; sub generate_support_class_for_extension { my $self = shift; my $extension_for_support_class = shift; my $subject_class_name = $self->class_name; unless ($subject_class_name) { Carp::confess("No subject class name for $self?"); } return unless defined $extension_for_support_class; if ($subject_class_name eq "UR::Object") { # Carp::cluck("can't generate $extension_for_support_class for UR::Object!\n"); # NOTE: we hit this a bunch of times when "getting" meta-data objects during boostrap. return; } unless ($support_class_suffixes{$extension_for_support_class}) { #$self->debug_message("Cannot generate a class with extension $extension_for_support_class."); return; } my $subject_class_obj = UR::Object::Type->get(class_name => $subject_class_name); unless ($subject_class_obj) { $self->debug_message("Cannot autogenerate $extension_for_support_class because $subject_class_name does not exist."); return; } my $new_class_name = $subject_class_name . "::" . $extension_for_support_class; my $class_obj; if ($class_obj = UR::Object::Type->is_loaded($new_class_name)) { # getting the subject class autogenerated the support class automatically # shortcut out return $class_obj; } no strict 'refs'; my @subject_parent_class_names = @{ $subject_class_name . "::ISA" }; my @parent_class_names = grep { UR::Object::Type->get(class_name => $_) } map { $_ . "::" . $extension_for_support_class } grep { $_->isa("UR::Object") } grep { $_ !~ /^UR::/ or $extension_for_support_class eq "Ghost" } @subject_parent_class_names; use strict 'refs'; unless (@parent_class_names) { if (UR::Object::Type->get(class_name => ("UR::Object::" . $extension_for_support_class))) { @parent_class_names = "UR::Object::" . $extension_for_support_class; } } unless (@parent_class_names) { #print Carp::longmess(); #$self->error_message("Cannot autogenerate $extension_for_support_class for $subject_class_name because parent classes (@subject_parent_class_names) do not have classes with that extension."); return; } my @id_property_names = $subject_class_obj->id_property_names; my %id_property_names = map { $_ => 1 } @id_property_names; if ($extension_for_support_class eq 'Ghost') { my $subject_class_metaobj = UR::Object::Type->get($self->meta_class_name); # Class object for the subject_class my %class_params = map { $_ => $subject_class_obj->$_ } grep { my $p = $subject_class_metaobj->property_meta_for_name($_) || Carp::croak("Can't no metadata for property '$_' of class ".$self->meta_class_name); ! $p->is_delegated and ! $p->is_calculated } $subject_class_obj->__meta__->all_property_names; delete $class_params{generated}; delete $class_params{meta_class_name}; delete $class_params{subclassify_by}; delete $class_params{sub_classification_meta_class_name}; delete $class_params{id_generator}; delete $class_params{id}; delete $class_params{is}; my $attributes_have = UR::Util::deep_copy($subject_class_obj->{attributes_have}); my $class_props = UR::Util::deep_copy($subject_class_obj->{has}); for (values %$class_props) { delete $_->{class_name}; delete $_->{property_name}; } %class_params = ( %class_params, class_name => $new_class_name, is => \@parent_class_names, is_abstract => 0, has => [%$class_props], attributes_have => $attributes_have, id_properties => \@id_property_names, ); $class_obj = UR::Object::Type->define(%class_params); } else { $class_obj = UR::Object::Type->define( class_name => $subject_class_name . "::" . $extension_for_support_class, is => \@parent_class_names, ); } return $class_obj; } sub has_table { my $self = shift; if ($bootstrapping) { return 0; } return 1 if $self->table_name; # FIXME - shouldn't this call inheritance() instead of parent_classes()? my @parent_classes = $self->parent_classes; for my $class_name (@parent_classes) { next if $class_name eq "UR::Object"; my $class_obj = UR::Object::Type->get(class_name => $class_name); if ($class_obj->table_name) { return 1; } } return; } sub most_specific_subclass_with_table { my $self = shift; return $self->class_name if $self->table_name; foreach my $class_name ( $self->class_name->inheritance ) { my $class_obj = UR::Object::Type->get(class_name => $class_name); return $class_name if ($class_obj && $class_obj->table_name); } return; } sub most_general_subclass_with_table { my $self = shift; my @subclass_list = reverse ( $self->class_name, $self->class_name->inheritance ); foreach my $class_name ( $self->inheritance ) { my $class_obj = UR::Object::Type->get(class_name => $class_name); return $class_name if ($class_obj && $class_obj->table_name); } return; } sub _load { my $class = shift; my $rule = shift; $rule = $rule->normalize; my $params = $rule->legacy_params_hash; # While core entity classes are actually loaded, # support classes dynamically generate for them as needed. # Examples are Acme::Employee::View::emp_id, and Acme::Equipment::Ghost # Try to parse the class name. my $class_name = $params->{class_name}; # See if the class autogenerates from another class. # i.e.: Acme::Foo::Bar might be generated by Acme::Foo unless ($class_name) { my $namespace = $params->{namespace}; if (my $data_source = $params->{data_source_id}) { $namespace = $data_source->get_namespace; } if ($namespace) { # FIXME This chunk seems to be getting called each time there's a new table/class #Carp::cluck("Getting all classes for namespace $namespace from the filesystem..."); my @classes = $namespace->get_material_classes; return $class->is_loaded($params); } my @params = %$params; Carp::confess("Non-class_name used to find a class object: @params"); } # Besides the common case of asking for a class by its name, the next most # common thing is asking for multiple classes by their names. Rather than doing the # hard work of doing it "right" right here, just recursively call myself with each # item in that list if (ref $class_name eq 'ARRAY') { # FIXME is there a more efficient way to add/remove class_name from the rule? my $rule_without_class_name = $rule->remove_filter('class_name'); $rule_without_class_name = $rule_without_class_name->remove_filter('id'); # id is a synonym for class_name my @objs = map { $class->_load($rule_without_class_name->add_filter(class_name => $_)) } @$class_name; return $class->context_return(@objs); } # If the class is loaded, we're done. # This is an un-documented unique constraint right now. my $class_obj = $class->is_loaded(class_name => $class_name); return $class_obj if $class_obj; # Handle deleted classes. # This is written in non-oo notation for bootstrapping. no warnings; if ( $class_name ne "UR::Object::Type::Ghost" and UR::Object::Type::Ghost->can("class") and $UR::Context::current->get_objects_for_class_and_rule("UR::Object::Type::Ghost",$rule,0) ) { return; } # Check the filesystem. The file may create its metadata object. eval "use $class_name"; unless ($@) { # If the above module was loaded, and is an UR::Object, # this will find the object. If not, it will return nothing. $class_obj = $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); return $class_obj if $class_obj; } if ($@) { # We need to handle $@ here otherwise we'll see # "Can't locate UR/Object/Type/Ghost.pm in @INC" error. # We want to fall through "in the right circumstances". Carp::croak("Error while autoloading with 'use $class_name': $@") unless ($@ =~ /Can't locate \S+ in \@INC/); # FIXME: I think other conditions here will result in silent errors. } # Parse the specified class name to check for a suffix. my ($prefix, $base, $suffix) = ($class_name =~ /^([^\:]+)::(.*)::([^:]+)/); my @parts; ($prefix, @parts) = split(/::/,$class_name); for (my $suffix_pos = $#parts; $suffix_pos >= 0; $suffix_pos--) { $class_obj = $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0); if ($class_obj) { # the class was somehow generated while we were checking other classes for it and failing. # this can happen b/c some class with a name which is a subset of the one we're looking # for might "use" the one we want. return $class_obj if $class_obj; } my $base = join("::", @parts[0 .. $suffix_pos-1]); my $suffix = join("::", @parts[$suffix_pos..$#parts]); # See if a class exists for the same name w/o the suffix. # This may cause this function to be called recursively for # classes like Acme::Equipment::Set::View::upc_code, # which would fire recursively for three extensions of # Acme::Equipment. my $full_base_class_name = $prefix . ($base ? "::" . $base : ""); my $base_class_obj = UR::Object::Type->get(class_name => $full_base_class_name); if ($base_class_obj) { # If so, that class may be able to generate a support # class. $class_obj = $full_base_class_name->__extend_namespace__($suffix); if ($class_obj) { # Autogeneration worked. # We still defer to is_loaded, since other parameters # may prevent the newly "loaded" class from being # returned. return $UR::Context::current->get_objects_for_class_and_rule($class,$rule,0) } } } # If we fall-through to this point, no class was found and no module. return; } sub use_module_with_namespace_constraints { use strict; use warnings; my $self = shift; my $target_class = shift; # If you do "use Acme; $o = Acme::Rocket->new();", and Perl finds Acme.pm # at "/foo/bar/Acme.pm", Acme::Rocket must be under /foo/bar/Acme/ # in order to be dynamically loaded. my @words = split("::",$target_class); my $path; while (@words > 1) { my $namespace_name = join("::",@words[0..$#words-1]); my $namespace_expected_module = join("/",@words[0..$#words-1]) . ".pm"; if ($path = $INC{$namespace_expected_module}) { #print "got mod $namespace_expected_module at $path for $target_class\n"; $path =~ s/\/*$namespace_expected_module//g; } else { my $namespace_obj = UR::Object::Type->is_loaded(class_name => $namespace_name); if ($namespace_obj) { eval { $path = $namespace_obj->module_directory }; if ($@) { # non-module class # don't auto-use, but don't make a lot of noise about it either } } } last if $path; pop @words; } unless ($path) { #Carp::cluck("No module_directory found for namespace $namespace_name." # . " Cannot dynamically load $target_class."); return; } $self->_use_safe($target_class,$path); my $meta = UR::Object::Type->is_loaded(class_name => $target_class); if ($meta) { return $meta; } else { return; } } sub _use_safe { use strict; use warnings; my ($self, $target_class, $expected_directory) = @_; # TODO: use some smart module to determine whether the path is # relative on the current system. if (defined($expected_directory) and $expected_directory !~ /^[\/\\]/) { $expected_directory = $pwd_at_compile_time . "/" . $expected_directory; } my $class_path = $target_class . ".pm"; $class_path =~ s/\:\:/\//g; my @INC_COPY = @INC; if ($expected_directory) { unshift @INC, $expected_directory; } my $found = ""; for my $dir (@INC) { if ($dir and (-e $dir . "/" . $class_path)) { $found = $dir; last; } } if (!$found) { # not found @INC = @INC_COPY; return; } if ($expected_directory and $expected_directory ne $found) { # not found in the specified location @INC = @INC_COPY; return; } do { local $SIG{__DIE__}; local $SIG{__WARN__}; eval "use $target_class"; }; # FIXME - if the use above failed because of a compilation error in the module we're trying to # load, then the error message below just tells the user that "Compilation failed in require" # and isn't propogating the error message about what caused the compile to fail if ($@) { #local $SIG{__DIE__}; @INC = @INC_COPY; die ("ERROR DYNAMICALLY LOADING CLASS $target_class\n$@"); } for (0..$#INC) { if ($INC[$_] eq $expected_directory) { splice @INC, $_, 1; last; } } return 1; } # Create the table behind this class in the specified database. # Currently, it creates sql valid for SQLite for support of loading # up a testing DB. Maybe this should be moved somewhere under the # DataSource objects sub mk_table { my($self,$dbh) = @_; return 1 unless $self->has_table; $dbh ||= $self->dbh; my $table_name = $self->table_name(); # we only care about properties backed up by a real column my @props = grep { $_->column_name } $self->direct_property_metas(); my $sql = "create table $table_name ("; my @cols; foreach my $prop ( @props ) { my $col = $prop->column_name; my $type = $prop->data_type; my $len = $prop->data_length; my $nullable = $prop->nullable; my $string = "$col" . " " . $type; $string .= " NOT NULL" unless $nullable; push @cols, $string; } $sql .= join(',',@cols); my @id_cols = $self->direct_id_column_names(); $sql .= ", PRIMARY KEY (" . join(',',@id_cols) . ")" if (@id_cols); # Should we also check for the unique properties? $sql .= ")"; unless ($dbh->do($sql) ) { $self->error_message("Can't create table $table_name: ".$DBI::errstr."\nSQL: $sql"); return undef; } 1; } # sub _object # This is used to make sure that methods are called # as object methods and not class methods. # The typical case that's important is when something # like UR::Object::Type->method(...) is called. # If an object is expected in a method and it gets # a class instead, well, unpredictable things can # happen. # # For many methods on UR::Objects, the implementation # is in UR::Object. However, some of those methods # have the same name as methods in here (purposefully), # and those UR::Object methods often get the # UR::Object::Type object and call the same method, # which ends up in this file. The problem is when # those methods are called on UR::Object::Type # itself it come directly here, without getting # the UR::Object::Type object for UR::Object::Type # (confused yet?). So to fix this, we use _object to # make sure we have an object and not a class. # # Basically, we make sure we're working with a class # object and not a class name. # sub _object { return ref($_[0]) ? $_[0] : $_[0]->__meta__; } # new version gets everything, including "id" itself and object ref properties push @cache_keys, '_all_property_type_names'; sub all_property_type_names { my $self = shift; if ($self->{_all_property_type_names}) { return @{ $self->{_all_property_type_names} }; } #my $rule_template = UR::BoolExpr::Template->resolve('UR::Object::Type', 'id'); my $all_property_type_names = $self->{_all_property_type_names} = []; for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); #my $rule = $rule_template->get_rule_for_values($class_name); #my $class_meta = $UR::Context::current->get_objects_for_class_and_rule('UR::Object::Type',$rule); if (my $has = $class_meta->{has}) { push @$all_property_type_names, sort keys %$has; } } return @$all_property_type_names; } sub table_for_property { my $self = _object(shift); Carp::croak('must pass a property_name to table_for_property') unless @_; my $property_name = shift; for my $class_object ( $self, $self->ancestry_class_metas ) { my $property_object = UR::Object::Property->get( class_name => $class_object->class_name, property_name => $property_name ); if ( $property_object ) { return unless $property_object->column_name; return $class_object->table_name; } } return; } sub column_for_property { my $self = _object(shift); Carp::croak('must pass a property_name to column_for_property') unless @_; my $property_name = shift; my($properties,$columns) = @{$self->{'_all_properties_columns'}}; for (my $i = 0; $i < @$properties; $i++) { if ($properties->[$i] eq $property_name) { return $columns->[$i]; } } for my $class_object ( $self->ancestry_class_metas ) { my $column_name = $class_object->column_for_property($property_name); return $column_name if $column_name; } return; } sub property_for_column { my $self = _object(shift); Carp::croak('must pass a column_name to property_for_column') unless @_; my $column_name = shift; my($properties,$columns) = @{$self->{'_all_properties_columns'}}; for (my $i = 0; $i < @$columns; $i++) { if ($columns->[$i] eq $column_name) { return $properties->[$i]; } } for my $class_object ( $self->ancestry_class_metas ) { my $property_name = $class_object->property_for_column($column_name); return $property_name if $property_name; } return; } # Methods for maintaining unique constraints # This is primarily used by the class re-writer (ur update classes-from-db), but # BoolExprs use them,too # Adds a constraint by name and property list to the class metadata. The class initializer # fills this data in via the 'constraints' key, so it shouldn't call add_unique_constraint() # directly sub add_unique_constraint { my $self = shift; unless (@_) { Carp::croak('method add_unique_constraint requires a constraint name as a parameter'); } my $constraint_name = shift; my $constraints = $self->unique_property_set_hashref(); if (exists $constraints->{$constraint_name}) { Carp::croak("A constraint named '$constraint_name' already exists for class ".$self->class_name); } unless (@_) { Carp::croak('method add_unique_constraint requires one or more property names as parameters'); } my @property_names = @_; # Add a new constraint record push @{ $self->{'constraints'} } , { sql => $constraint_name, properties => \@property_names }; # invalidate the other cached data $self->_invalidate_cached_data_for_subclasses('_unique_property_sets', '_unique_property_set_hashref'); } sub remove_unique_constraint { my $self = shift; unless (@_) { Carp::croak("method remove_unique_constraint requires a constraint name as a parameter"); } my $constraint_name = shift; my $constraints = $self->unique_property_set_hashref(); unless (exists $constraints->{$constraint_name}) { Carp::croak("There is no constraint named '$constraint_name' for class ".$self->class_name); } # Remove the constraint record for (my $i = 0; $i < @{$self->{'constraints'}}; $i++) { if ($self->{'constraints'}->[$i]->{'sql'} = $constraint_name) { splice(@{$self->{'constraints'}}, $i, 1); } } $self->_invalidate_cached_data_for_subclasses('_unique_property_sets', '_unique_property_set_hashref'); } # This returns a list of lists. Each inner list is the properties/columns # involved in the constraint sub unique_property_sets { my $self = shift; if ($self->{_unique_property_sets}) { return @{ $self->{_unique_property_sets} }; } my $unique_property_sets = $self->{_unique_property_sets} = []; for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); if ($class_meta->{constraints}) { for my $spec (@{ $class_meta->{constraints} }) { push @$unique_property_sets, [ @{ $spec->{properties} } ] } } } return @$unique_property_sets; } # Return the constraint information as a hashref # keys are the SQL constraint name, values are a listref of property/column names involved sub unique_property_set_hashref { my $self = shift; if ($self->{_unique_property_set_hashref}) { return $self->{_unique_property_set_hashref}; } my $unique_property_set_hashref = $self->{_unique_property_set_hashref} = {}; for my $class_name ($self->class_name, $self->ancestry_class_names) { my $class_meta = UR::Object::Type->get($class_name); if ($class_meta->{'constraints'}) { for my $spec (@{ $class_meta->{'constraints'} }) { my $unique_group = $spec->{'sql'}; next if ($unique_property_set_hashref->{$unique_group}); # child classes override parents $unique_property_set_hashref->{$unique_group} = [ @{$spec->{properties}} ]; } } } return $unique_property_set_hashref; } # Used by the class meta meta data constructors to make changes in the # raw data stored in the class object's hash. These should really # only matter while running ur update # Args are: # 1) An UR::Object::Property object with attribute_name, class_name, id, property_name, type_name # 2) The method called: _construct_object, load, # 3) An id? sub _property_change_callback { my($property_obj,$method, $old_val, $new_val) = @_; return if ($method eq 'load' || $method eq 'unload'); my $class_obj = UR::Object::Type->get(class_name => $property_obj->class_name); my $property_name = $property_obj->property_name; $old_val = '' unless(defined $old_val); $new_val = '' unless(defined $new_val); if ($method eq 'create') { unless ($class_obj->{'has'}->{$property_name}) { my @attr = qw( class_name data_length data_type is_delegated is_optional property_name ); my %new_property; foreach my $attr_name (@attr ) { $new_property{$attr_name} = $property_obj->$attr_name(); } $class_obj->{'has'}->{$property_name} = \%new_property; } if (defined $property_obj->is_id) { &_id_property_change_callback($property_obj, 'create'); } } elsif ($method eq 'delete') { if (defined $property_obj->is_id) { &_id_property_change_callback($property_obj, 'delete'); } delete $class_obj->{'has'}->{$property_name}; } elsif ($method eq 'is_id' and $new_val ne $old_val) { my $change = $new_val ? 'create' : 'delete'; &_id_property_change_callback($property_obj, $change); } if (exists $class_obj->{'has'}->{$property_name}->{$method}) { $class_obj->{'has'}->{$property_name}->{$method} = $new_val; } # Invalidate the cache used by all_property_names() for my $key (@cache_keys) { $class_obj->_invalidate_cached_data_for_subclasses($key); } } # Some expensive-to-calculate data gets stored in the class meta hashref # and needs to be removed for all the existing subclasses sub _invalidate_cached_data_for_subclasses { my($class_meta, @cache_keys) = @_; delete @$class_meta{@cache_keys}; #my @subclasses = $class_meta->subclasses_loaded($class_obj->class_name); my @subclasses = @{$UR::Object::_init_subclasses_loaded{$class_meta->class_name}}; my %seen; while (my $subclass = shift @subclasses) { my $sub_meta = UR::Object::Type->get(class_name => $subclass); delete @$sub_meta{@cache_keys}; #push @subclasses, $sub_meta->subclasses_loaded($subclass); push @subclasses, @{$UR::Object::_init_subclasses_loaded{$sub_meta->class_name}}; } } # A streamlined version of the method just below that dosen't check that the # data in both places is the same before a delete operation. What was happening # was that an ID property got deleted and the position checks out ok, but then # a second ID property gets deleted and now the position dosen't match because we # aren't able to update the object's position property 'cause it's an ID property # and can't be changed. # # The short story is that we've lowered the bar for making sure it's safe to delete info sub _id_property_change_callback { my $property_obj = shift; my $method = shift; return if ($method eq 'load' || $method eq 'unload'); my $class = UR::Object::Type->get(class_name => $property_obj->class_name); if ($method eq 'create') { my $pos = $property_obj->id_by; $pos += 0; # make sure it's a number if ($pos <= @{$class->{'id_by'}}) { splice(@{$class->{'id_by'}}, $pos, 0, $property_obj->property_name); } else { # $pos is past the end... probably an id property was deleted and another added push @{$class->{'id_by'}}, $property_obj->property_name; } } elsif ($method eq 'delete') { my $property_name = $property_obj->property_name; for (my $i = 0; $i < @{$class->{'id_by'}}; $i++) { if ($class->{'id_by'}->[$i] eq $property_name) { splice(@{$class->{'id_by'}}, $i, 1); return; } } #$DB::single = 1; Carp::confess("Internal data consistancy problem: could not find property named $property_name in id_by list for class meta " . $class->class_name); } else { # Shouldn't get here since ID properties can't be changed, right? #$DB::single = 1; Carp::confess("Shouldn't be here as ID properties can't change"); 1; } $class->{'_all_id_property_names'} = undef; # Invalidate the cache used by all_id_property_names } # # BOOTSTRAP CODE # sub get_with_special_parameters { my $class = shift; my $rule = shift; my %extra = @_; if (my $namespace = delete $extra{'namespace'}) { unless (keys %extra) { my @c = $namespace->get_material_classes(); @c = grep { $_->namespace eq $namespace } $class->is_loaded($rule->params_list); return $class->context_return(@c); } } return $class->SUPER::get_with_special_parameters($rule,@_); } sub __signal_change__ { my $self = shift; my @rv = $self->SUPER::__signal_change__(@_); if ($_[0] eq "delete") { my $class_name = $self->{class_name}; $self->ungenerate(); } return @rv; } our %STANDARD_VALID_SIGNALS = ( create => 1, 'delete' => 1, commit => 1, rollback => 1, load => 1, unload => 1, load_external => 1 ); sub _is_valid_signal { my $self = shift; my $aspect = shift; # Undefined attributes indicate that the subscriber wants any changes at all to generate a callback. return 1 if (! defined $aspect); # All standard creation and destruction methods emit a signal. return 1 if ($STANDARD_VALID_SIGNALS{$aspect}); for my $property ($self->all_property_names) { return 1 if $property eq $aspect; } if (!exists $self->{'_is_valid_signal'}) { $self->{'_is_valid_signal'} = { map { $_ => 1 } @{$self->{'valid_signals'}} }; } return 1 if ($self->{'_is_valid_signal'}->{$aspect}); foreach my $parent_meta ( $self->parent_class_metas ) { if ($parent_meta->_is_valid_signal($aspect)) { $self->{'_is_valid_signal'}->{$aspect} = 1; return 1; } } return 0; } sub generated { my $self = shift; if (@_) { $self->{'generated'} = shift; } return $self->{'generated'}; } sub ungenerate { my $self = shift; my $class_name = $self->class_name; delete $UR::Object::_init_subclass->{$class_name}; delete $UR::Object::Type::_inform_all_parent_classes_of_newly_loaded_subclass{$class_name}; do { no strict; no warnings; my @symbols_which_are_not_subordinate_namespaces = grep { substr($_,-2) ne '::' } keys %{ $class_name . "::" }; my $hr = \%{ $class_name . "::" }; delete @$hr{@symbols_which_are_not_subordinate_namespaces}; }; my $module_name = $class_name; $module_name =~ s/::/\//g; $module_name .= ".pm"; delete $INC{$module_name}; $self->{'generated'} = 0; } 1;