# $Id: Object.pm,v 1.34 2008-04-29 17:05:38 mike Exp $ package Keystone::Resolver::DB::Object; use strict; use warnings; use Carp; sub new { my $class = shift(); my($db) = shift(); my @fields = $class->physical_fields(); my %hash = (_db => $db); foreach my $i (1 .. @fields) { my $key = $fields[$i-1]; my $value = $_[$i-1]; $hash{$key} = $value; } return bless \%hash, $class; } sub class { my $this = shift(); my $class = ref $this; $class =~ s/^Keystone::Resolver::DB:://; return $class; } # Accessors and delegations sub db { shift()->{_db} } sub log { shift()->{_db}->log(@_) } sub quote { shift()->{_db}->quote(@_) } # Default implementations of subclass-specific virtual functions # fields() must be explicitly provided for searchable classes # virtual_fields() must be explicitly provided for searchable classes sub mandatory_fields { qw() } # search_fields() must be explicitly provided for searchable classes # display_fields() must be explicitly provided for searchable classes sub fulldisplay_fields { shift()->display_fields(@_) } sub field_map { {} } # Returns an empty array if it's OK to delete this object, or # otherwise an array of one or more strings, each specifying a reason # why not. Can be overridden by subclasses, but by default insists on # no non-dependent links. # sub undeletable { my $this = shift(); my @reasons; my %fields = $this->fields(); foreach my $key (sort keys %fields) { my $ref = $fields{$key}; if (ref $ref && defined $ref->[3]) { my($linkfield, $linkclass, $linkto) = @$ref; ### This is wasteful: it would be better to use a method # that only counts hits instead of fetching all the data # and constructing all the objects, but there is as yet # no such method, my @hits = $this->db()->find($linkclass, undef, $linkto, $this->field($linkfield)); my $n = @hits; if ($n == 1) { push @reasons, "a $linkclass depends on it"; } elsif ($n != 0) { push @reasons, "$n $linkclass objects depend on it"; } } } return @reasons; } # Returns a list of all the field specified by fields(), with types # drawn from fulldisplay_fields() where available and using "t" when # not. # # Fields which are used as the link-field in a virtual-field recipe # of the "dependent-link" type are omitted (e.g. service_type_id # from the Service class, because it is the link-field in the # service_type recipe). # # Virtual fields that are of not of the "dependent-link" type have a # exclude-at-creation-time attribute prepended to their type, if they # don't already have it. # sub editable_fields { my $class = shift(); my @allfields = $class->fields(); my %hash = @allfields; my(%omitFields, %virtualFields); foreach my $key (keys %hash) { my $value = $hash{$key}; if (defined $value && ref $value) { ### The correct test here might not be for @$value==3 but # something like defined $value[3]. See all the virtual # fields in Service.pm and think harder. if (@$value == 3) { $omitFields{$value->[0]} = 1; } else { $virtualFields{$key} = 1; } } } foreach my $skip ($class->uneditable_fields()) { $omitFields{$skip} = 1; } my %fdfields = $class->fulldisplay_fields(); my @res; while (@allfields) { my $name = shift @allfields; my $recipe = shift @allfields; if (defined $omitFields{$name}) { warn "omitting '$name' from editable_field($class)\n"; next; } my $display = $fdfields{$name} || "t"; if (defined $virtualFields{$name}) { $display = "X$display" if $display !~ /X/; warn "made '$name' readonly '$display' in editable_field($class)\n"; } push @res, ($name, $display); } return @res; } # List of fields to omit from the return of editable fields (unless # that method has been overridden, of course). This list is empty in # general, but can be used to knock out link-fields and suchlike as # required. # sub uneditable_fields { return (); } sub physical_fields { my $class = shift(); my @allfields = $class->fields(); my @pfields; while (@allfields) { my $name = shift @allfields; my $recipe = shift @allfields; push @pfields, $name if !defined $recipe; } return @pfields; } sub virtual_fields { my $class = shift(); my @allfields = $class->fields(); my @vfields; while (@allfields) { my $name = shift @allfields; my $recipe = shift @allfields; push @vfields, $name, $recipe if defined $recipe; } return @vfields; } # Parses full-type strings such as those used on the RHS of # display_fields() arrays, e.g. "c", "Lt", "Rn". Returns an array of # four elements: # 0: whether the field is a link # 1: whether the field is readonly # 2: the field's core type # 3: whether the field should be excluded at creation time. # (It would make more sense if 2 and 3 were reversed, but existing # code assumes the first three elements from before the fourth was # added.) # sub analyse_type { my $_unused_this = shift(); my($type, $field) = @_; return (undef, undef, $type) if ref $type; my $link = ($type =~ s/L//); my $readonly = ($type =~ s/R//); my $exclude = ($type =~ s/X//); # Special-case the fields that we know may never change $readonly = 1 if grep { $field eq $_ } qw(id tag); return ($link, $readonly, $type, $exclude); } # Returns name of CSS class to be used for displaying fields of the # specified type. ### Knows about what's in "style.css" # sub type2class { my $this = shift(); my($type) = @_; return "enum" if ref($type) eq "ARRAY"; return $type if grep { $type eq $_ } qw(t c n b); return "error"; } sub create { my $class = shift(); my($db, %maybe_data) = @_; my %data; foreach my $key (keys %maybe_data) { $data{$key} = $maybe_data{$key} if $maybe_data{$key} ne "" && grep { $_ eq $key } $class->physical_fields(); } my $table = $class->table(); my $sql = "INSERT INTO " . $db->quote($table) . " (" . join(", ", map { $db->quote($_) } sort keys %data) . ") VALUES" . " (" . join(", ", map { sql_quote($data{$_}) } sort keys %data) . ")"; $db->do($sql); my $id = $db->last_insert_id($table); die "can't get new record's ID" if !defined $id; return $db->find1($class, id => $id); } sub sql_quote { my($text) = @_; my $sq = "'"; $text =~ s/$sq/''/g; return "'$text'"; } # Returns a label to be used on-screen for the specified field sub label { my $this = shift(); my($field, $label) = @_; return $label if defined $label; my $map = $this->field_map(); $label = $map->{$field}; return $label if defined $label; # No explicit label passed, and none in config: use default rules $label = $field; $label =~ s/_/ /g; return ucfirst($label); } # Return the components needed to identify a linked-to object sub link { my $this = shift(); my($field) = @_; my %virtual = $this->virtual_fields(); my $ref = $virtual{$field}; return undef if !defined $ref; my($linkfield, $linkclass, $linkto) = @$ref; my $linkid = $this->field($linkfield); return ($linkclass, $linkto, $linkid, $linkfield); } # Returns the number of fields modified, dies on error sub update { my $this = shift(); my(%maybe_data) = @_; my %data; foreach my $key (keys %maybe_data) { $data{$key} = $maybe_data{$key} if (!defined $this->field($key) || $maybe_data{$key} ne $this->field($key)); } return 0 if !%data; # nothing to do my $sql = "UPDATE " . $this->quote($this->table()) . " SET " . join(", ", map { $this->quote($_) . " = " . sql_quote($data{$_}) } sort keys %data) . " WHERE " . $this->quote("id") . " = " . $this->id(); $this->db()->do($sql); foreach my $key (keys %data) { $this->field($key, $data{$key}); } return scalar keys %data; } sub delete { my $this = shift(); my $sql = "DELETE FROM " . $this->quote($this->table()) . " WHERE " . $this->quote("id") . " = " . $this->id(); $this->db()->do($sql); # Wow, that embarrasingly easy } sub field { my $this = shift(); my($fieldname, $value) = @_; die "$this: request for system-function field '$fieldname'" if grep { $_ eq $fieldname } qw(table fields mandatory_fields physical_fields virtual_fields search_fields sort_fields display_fields fulldisplay_fields field_map field); if (grep { $_ eq $fieldname } $this->physical_fields()) { $this->{$fieldname} = $value if defined $value; return $this->{$fieldname}; } my %virtual; eval { %virtual = $this->virtual_fields() }; if (!defined $virtual{$fieldname}) { confess "$this: field `$fieldname' not defined"; } elsif (defined $value) { die "can't set virtual field '$fieldname'='$value'"; } else { return $this->virtual_field($fieldname); } } sub virtual_field { my $this = shift(); my($fieldname) = @_; my %virtual = $this->virtual_fields(); my $ref = $virtual{$fieldname}; my($linkfield, $class, $linkto, $sortby, $valfield) = @$ref; my $value = $this->field($linkfield); return undef if !defined $value; # e.g. link-field in new record if (defined $sortby) { # Link is to multiple records my @obj = $this->db()->find($class, $sortby, $linkto, $value); #warn "$this->virtual_fields($fieldname) -> @obj"; return [ @obj ]; } # Link is to a single "parent" record my $obj = $this->db()->find1($class, $linkto, $value); if (!defined $obj) { # The link is broken! The Dark Lord's reign begins! return "[$class:$linkto:$value]"; } if (defined $valfield) { return $obj->field($valfield); } else { return $obj->render_name(); } } sub AUTOLOAD { my $this = shift(); my $class = ref $this || $this; use vars qw($AUTOLOAD); (my $fieldname = $AUTOLOAD) =~ s/.*:://; die "$class: request for field '$fieldname' on undefined object" if !defined $this; return $this->field($fieldname, @_); } sub DESTROY {} # Avoid warning from AUTOLOAD() sub render { my $this = shift(); my $class = ref($this); my $name; eval { $name = $this->tag(); }; if ($@ || !$name) { undef $@; ### should this really be necessary? eval { $name = $this->name(); }; if ($@ || !$name) { undef $@; ### should this really be necessary? $name = undef; } } my $text = "$class " . $this->id(); $text .= " ($name)" if defined $name; return $text; } sub render_name { my $this = shift(); my $res; eval { $res = $this->name() }; if (!$@ && defined $res) { #warn "returning name()='$res'"; return $res; } eval { $res = $this->tag() }; if (!$@ && defined $res) { #warn "returning tag()='$res'"; return $res; } my $id = $this->id(); if (defined $id) { #warn "returning id '$id'"; return ref($this) . " " . $id; } #warn "returning new"; return "[NEW]"; } 1;