# Copyright (c) 1999-2010 bivio Software, Inc. All rights reserved. # # Visit http://www.bivio.biz for more info. # # This library is free software; you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License as # published by the Free Software Foundation; either version 2.1 of the # License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library; If not, you may get a copy from: # http://www.opensource.org/licenses/lgpl-license.html # # $Id: Model.pm,v 2.62 2011/10/28 00:31:54 nagler Exp $ package Bivio::Biz::Model; use strict; use Bivio::Base 'Collection.Attributes'; use Bivio::IO::Trace; # C is more interface than implementation, it provides # a common set of methods for L, # L, L. our($VERSION) = sprintf('%d.%02d', q$Revision: 2.62 $ =~ /\d+/g); our($_TRACE); my($_IDI) = __PACKAGE__->instance_data_index; my($_LOADED_ALL_PROPERTY_MODELS); my($_S) = b_use('SQL.Support'); my($_SS) = b_use('SQL.Statement'); my($_CL) = b_use('IO.ClassLoader'); sub as_string { my($self) = @_; my($ci) = $self->[$_IDI]->{class_info}; return ref($self) . '(' . join(',', map($self->get_field_type($_)->to_string($self->unsafe_get($_)), @{$ci->{as_string_fields}}), ) . ')'; } sub assert_is_instance { my($self) = @_; b_die('operation not supported on classes, use get_instance') unless ref($self); return $self; } sub assert_not_singleton { my($self) = shift->assert_is_instance; b_die("can't create, update, read, or delete singleton instance") if $self->[$_IDI]->{is_singleton}; return $self; } sub clone_return_is_self { return shift->is_instance ? 0 : 1; } sub delete { # Not supported. die('not supported'); } sub delete_from_request { my($self) = @_; return $self->delete_from_req($self->unsafe_get_request || return); } sub die { my($self, @args) = @_; $self->throw_die( 'DIE', { message => Bivio::IO::Alert->format_args(@args), program_error => 1, }, caller, ); # DOES NOT RETURN } sub do_iterate { #(self, code_ref, string, hash_ref) : self #(self, code_ref, string, string, hash_ref) : self my($self, $do_iterate_handler) = (shift, shift); # Like L but does not return anything. For each row, # calls L followed by # L. Terminates the iteration with # L when there are no more rows or if # I returns false. my($iterate_start) = $_[0] && !ref($_[0]) && $_[0] =~ /iterate_start/ && $self->can($_[0]) ? shift : 'iterate_start'; $self->$iterate_start(@_); while ($self->iterate_next_and_load) { next if $do_iterate_handler->($self); $self->put_on_request($self) unless $self->is_ephemeral; last; } $self->iterate_end; return $self; } sub field_decl { my($proto) = shift; if (ref($_[0]) eq 'ARRAY') { my($decls, $defaults) = (shift, shift); $defaults = { type => $defaults, constraint => shift, } unless ref($defaults) eq 'HASH'; $defaults->{constraint} ||= 'NONE'; return map({ my($decl) = ref($_) ? $_ : [$_]; my($i) = 0; ref($_) eq 'HASH' ? {%$defaults, %$decl} : +{ %$defaults, map({ my($d) = $decl->[$i++]; ref($d) eq 'HASH' ? %$d : defined($d) ? ($_ => $d) : (); } qw(name type constraint)), }; } @$decls); } my($defaults) = []; unshift(@$defaults, pop(@_)) while ref($_[$#_]) ne 'ARRAY'; Bivio::Die->die('expecting class and declarations') unless @_ > 1; Bivio::Die->die('uneven (class, declarations) tuples') if @_ % 2; return map( (shift(@_) => [$proto->field_decl(shift(@_), @$defaults)]), 1 .. @_ / 2, ); } sub field_decl_exclude { my($self, $field, $info) = @_; $info = b_use('IO.Ref')->nested_copy($info); my($ne) = sub { my($x) = @_; return (ref($x) eq 'HASH' ? $x->{name} : $x) ne $field; }; while (my($k, $v) = each(%$info)) { if (ref($v) eq 'ARRAY') { @$v = map( ref($_) eq 'ARRAY' ? [grep($ne->($_), @$_)] : grep($ne->($_), $_), @$v, ); } elsif (!ref($v)) { delete($info->{$k}) if ($v || '') eq $field; } else { b_die($k, ': unexpected value type: must be array_ref or scalar'); } } return $info; } sub field_decl_from_property_model { my($self, $class) = @_; my($m) = $self->get_instance($class); return map( $m->simple_package_name . ".$_", @{$m->get_info('column_names')}, ); } sub field_equals { my($self, $field, $value) = @_; return $self->get_field_type($field)->is_equal($value, $self->get($field)); } sub format_uri_for_this_property_model { my($self, $task, $name) = @_; # Formats a uri for I and model I of I. Blows up if not all # the primary keys are available for I. Doesn't load the I. # I can be a name or L. $task = Bivio::Agent::TaskId->from_name($task) unless ref($task); my($query, $mi) = _get_model_query($self, $name); $self->throw_die('MODEL_NOT_FOUND', { message => 'missing primary keys in self for model', entity => $name}) unless $query; return $self->get_request->format_uri( $task, $mi->format_query_for_this($query), undef, undef); } sub from_req { my($proto, $req, $class) = @_; return $req->get(_class($proto, $class)); } sub get_as { my($self, $field, $format) = @_; return $self->get_field_info($field, 'type')->$format($self->get($field)); } sub get_auth_id { return _well_known_value(@_); } sub get_auth_id_name { my($self) = @_; return _well_known_name( $self, [qw(auth_id realm_id)], $self->get_info('auth_id'), ); } sub get_auth_user_id { return _well_known_value(@_); } sub get_auth_user_id_name { my($self) = @_; return _well_known_name( $self, [qw(auth_user_id user_id)], [grep(/\buser_id$/, @{$self->get_info('column_names')})], ); } sub get_field_alias_value { my($self, $alias) = @_; return $self->get( ($self->get_info('column_aliases')->{$alias} || $self->die($alias, ': not a field alias') )->{name}); } sub get_field_constraint { # Returns the constraint for this field. # # Calls L, so subclasses only need # to override C. return shift->get_field_info(shift, 'constraint'); } sub get_field_info { return shift->internal_get_sql_support_no_assert->get_column_info(@_); } sub get_field_type { # Returns the type of this field. # # Calls L, so subclasses only need # to override C. return shift->get_field_info(shift, 'type'); } sub get_info { return shift->internal_get_sql_support_no_assert->get(shift); } sub get_instance { my($proto, $class) = @_; # Returns the singleton for I. If I is supplied, it may be just # the simple name or a fully qualified class name. It will be loaded with # L using the I map. # I may also be an instance of a model. # # May not be called on anonymous Models without I argument. return _get_class_info(_class($proto, $class))->{singleton}; } sub get_model { my($self) = @_; # Same as L, but dies if # the model could not be loaded. my($model) = shift->unsafe_get_model(@_); $self->throw_die('MODEL_NOT_FOUND', { message => 'unable to load model', entity => $model}) unless $model->is_loaded; return $model; } sub get_model_info { my($self, $model) = @_; return $self->unsafe_get_model_info($model) || b_die($model, ': no such model'); } sub get_primary_id { return _well_known_value(@_); } sub get_primary_id_name { my($self) = @_; return _well_known_name( $self, ['primary_id'], $self->get_info('primary_key_names'), ); } sub get_qualified { my($self, $field) = @_; # Returns the qualified field value if it exists or strips the model from # I and tries to get unqualified. return $self->has_keys($field) ? $self->get($field) : $self->get(($field =~ /(?<=\.)(\w+)$/)[0] || $self->die($field, ': not a qualified name')); } sub get_request { my($self) = @_; return $self->unsafe_get_request || b_die($self, ': request not set'); } sub handle_call_autoload { return _new_with_query(@_); } sub has_fields { return shift->internal_get_sql_support_no_assert->has_columns(@_); } sub has_iterator { my($self) = @_; # Returns true if there is an iterator started on this model. my($fields) = $self->[$_IDI]; return $fields->{iterator} ? 1 : 0; } sub internal_clear_model_cache { my($self) = @_; # Called to clear the cache of models. Necessary # when a reload occurs. my($fields) = $self->[$_IDI]; delete($fields->{models}); return; } sub internal_get_iterator { my($self) = @_; return $self->[$_IDI]->{iterator} || $self->die('iteration not started'); } sub internal_get_sql_support { return shift->assert_not_singleton->internal_get_sql_support_no_assert; } sub internal_get_sql_support_no_assert { my($self) = @_; return $self->assert_is_instance->[$_IDI]->{class_info}->{sql_support}; } sub internal_get_statement { return shift->assert_not_singleton->[$_IDI]->{class_info}->{statement}; } sub internal_initialize { # B # # Returns an hash_ref describing the model suitable for passing # to L # or L. return (caller(1))[3] =~ /::internal_initialize$/ ? {} : Bivio::Die->die( shift, ': abstract method; internal_initialize must be defined'); } sub internal_initialize_local_fields { Bivio::IO::Alert->warn_deprecated('use field_decl'); return [shift->field_decl(@_)]; } sub internal_initialize_sql_support { # B. # # Returns the L object # for this model. Bivio::Die->die(shift, ': abstract method'); } sub internal_iterate_next { my($self, $it, $row, $converter) = @_; # Returns (I, I) on success or () if no more. if (ref($it) eq 'HASH') { $converter = $row; $row = $it; $it = $self->internal_get_iterator; } else { # deprecated form } return $self->internal_get_sql_support->iterate_next( $self, $it, $row, $converter) ? ($self, $row) : (); } sub internal_put_iterator { my($self, $it) = @_; # Sets the iterator and returns its argument. return $self->[$_IDI]->{iterator} = $it; } sub is_ephemeral { my($self) = @_; return $self->[$_IDI]->{ephemeral} ? 1 : 0; } sub is_instance { my($self) = @_; # Returns true if is a normal instance and not singleton or class. return !ref($self) || $self->[$_IDI]->{is_singleton} ? 0 : 1; } sub iterate_end { my($self, $it) = @_; # Terminates the iterator. See L. # Does not modify model state, i.e. if loaded, stays loaded. # # B my($fields) = $self->[$_IDI]; $self->internal_get_sql_support->iterate_end( $it || $self->internal_get_iterator); # Deprecated form passes in an iterator, which can only clear # if the caller hasn't "changed" iterators. $fields->{iterator} = undef if !$it || $fields->{iterator} && $it == $fields->{iterator}; return; } sub iterate_next { # I is the resultant values by field name. # I is optional and is the name of a # L method, e.g. C. # # Returns false if there is no next. # # B return shift->internal_iterate_next(@_) ? 1 : 0; } sub local_field { Bivio::IO::Alert->warn_deprecated('use field_decl'); return shift->field_decl(@_); } sub map_iterate { my($self, $map_iterate_handler) = (shift, shift); # Calls L or I (if supplied) # to start the iteration with I. For each row, calls # L followed by # L. Terminates the iteration with # L. # # Returns the aggregated result of L # as an array_ref, calling L to get each # row's values. # # If I is C, the default handler simply returns all # the rows. my($iterate_start) = $_[0] && !ref($_[0]) && $_[0] =~ /iterate_start/ && $self->can($_[0]) ? shift : 'iterate_start'; my($res) = []; $self->$iterate_start(@_); my($op) = ref($map_iterate_handler) ? $map_iterate_handler : defined($map_iterate_handler) ? sub {shift->get($map_iterate_handler)} : sub {shift->get_shallow_copy}; while ($self->iterate_next_and_load) { push(@$res, $op->($self)); } $self->iterate_end; return $res; } sub merge_initialize_info { my($proto, $parent, $child) = @_; # Merges two model field definitions (I into I) into a new # hash_ref. my($res) = {%$child}; foreach my $k (keys(%$parent)) { if ( ref($parent->{$k}) ne 'ARRAY' || $k =~ /^(auth_id|date|primary_id|primary_key)$/, ) { $res->{$k} = $parent->{$k} unless exists($res->{$k}); } else { # Parent takes precedence on arrays unshift(@{$res->{$k} ||= []}, @{$parent->{$k}}); } } return $res; } sub new { my($proto, $req, $class) = _new_args(@_); # Creates a Model with I, if supplied. The class of the model is defined by # C<$proto>. If I is supplied, L is called # with I as its argument and the resultant class is instantiated. return $proto->get_instance($class)->new($req) if defined($class); my($ci) = _get_class_info(ref($proto) || $proto); my($self) = $proto->SUPER::new({@{$ci->{properties}}}); $self->[$_IDI] = { class_info => $ci, request => $req || (ref($proto) ? $proto->unsafe_get_request : undef), }; return $self; } sub new_anonymous { my($proto, $config, $req) = @_; # Creates an "anonymous" Model. There are two modes: initialization # and creation from existing. To initialize, you must supply # I. This will create the first anonymous instance. # I must be a class name, not a reference. # # To create an instance from an existing instance, I must # be an instance, not a class name. I is ignored. my($ci) = ref($proto) ? $proto->[$_IDI]->{class_info} : _initialize_class_info($proto, $config); # Make a copy of the properties for this instance. properties # is an array_ref for efficiency. my($self) = $proto->SUPER::new({@{$ci->{properties}}}); $self->[$_IDI] = { class_info => $ci, # Never save the request for first time anonymous classes request => ref($proto) ? $req : undef, anonymous => 1, }; return $self; } sub new_other { my($self, $model_name) = (shift, shift); return ($_S->is_qualified_model_name($model_name) ? $_S->parse_model_name($model_name)->{model} : $self->get_instance($model_name) )->new($self->get_request, @_); } sub new_other_with_query { my($proto, $name, $query) = @_; return _new_with_query($proto->get_instance($name), $query); } sub put { # Not supported. CORE::die('put: not supported'); } sub put_on_request { my($self, $durable) = @_; $self->set_ephemeral(0); return $self->unsafe_get_request ? $self->put_on_req($self->req, $durable) : $self; } sub set_ephemeral { my($self, $value) = @_; $self->[$_IDI]->{ephemeral} = @_ < 2 || $value ? 1 : 0; return $self; } sub throw_die { my($self, $code, $attrs, $package, $file, $line) = @_; # Terminate the I as entity and request in I with a specific code. # # I, I, and I need not be defined $package ||= (caller)[0]; $file ||= (caller)[1]; $line ||= (caller)[2]; $attrs ||= {}; ref($attrs) eq 'HASH' || ($attrs = {message => $attrs}); $attrs->{model} = $self; Bivio::Die->throw($code, $attrs, $package, $file, $line); # DOES NOT RETURN } sub unsafe_get_model { # Returns the named PropertyModel associated with this instance. # If it can be loaded, it will be. See # L. # my($self, $class, $query) = @_; # $query ||= {}; # return $self->new_other($class) # ->unsafe_load({map({($_ => $query->{$_} || $self)} # @{$self->get_instance($class)->get_model_keys()})}); my($self, $name) = @_; my($fields) = $self->[$_IDI]; return ($fields->{models} ||= {})->{$name} ||= _load_other_model($self, $name); } sub unsafe_get_model_info { my($self, $model) = @_; return $self->get_info('models') ->{ref($model) ? $model->simple_package_name : $model}; } sub unsafe_get_request { my($self) = @_; # Returns the request associated with this model (if defined). # Otherwise, returns the current request, if any. my($req); $req = $self->[$_IDI]->{request} if ref($self); # DON'T SET the request for future calls, because this may # be an anonymous model or a singleton. return $req ? $req : b_use('Agent.Request')->get_current; } sub _as_string_fields { my($sql_support) = @_; # Returns as_string_fields. return $sql_support->get('as_string_fields') if $sql_support->has_keys('as_string_fields'); my($res) = [@{$sql_support->get('primary_key_names')}]; unshift(@$res, 'name') if $sql_support->has_columns('name') && !grep($_ eq 'name', @$res); return $res; } sub _assert_class_name { my($class) = @_; # Ensures that the class conforms to the naming conventions. Bivio::Die->die($class, ': is a base class; it cannot be initialized' .' as a model') if $class =~ /Base$/; my($super) = 'Bivio::Biz::' .($class =~ /(ListForm|Form|List)$/ ? $1 : 'Property') .'Model'; Bivio::Die->die($class, ': must be a ', $super) unless UNIVERSAL::isa($class, $super); return; } sub _class { my($proto, $class) = @_; return ref($proto) || $proto unless defined($class); return b_use('Model', $class) unless ref($class); return ref($class) || $class; } sub _get_class_info { my($class) = @_; no strict qw(refs); #TODO: Name should be something like Bivio::Biz::Model::_class_info, not _CLASS_INFO _initialize_class_info($class) unless defined(*{$class . '::'}{HASH}->{_CLASS_INFO}); return *{$class . '::'}{HASH}->{_CLASS_INFO}; } sub _get_model_query { my($self, $name) = @_; # Returns the model (query, instance) by looking for the model. # Asserts operation is valid my($sql_support) = $self->internal_get_sql_support; my($models) = $sql_support->get('models'); $self->die("$name: no such model") unless defined($models->{$name}); my($m) = $models->{$name}; my($properties) = $self->internal_get; my($req) = $self->unsafe_get_request; # Always store the model. my($mi) = $m->{instance}->new($req); my($query) = {}; my($map) = $m->{primary_key_map}; foreach my $pk (keys(%$map)) { my($v); unless (defined($v = $properties->{$map->{$pk}->{name}})) { # If there is an auth_id, use it if this is the missing # primary key. my($auth_id) = $mi->get_info('auth_id'); unless ($auth_id && $pk eq $auth_id->{name}) { Bivio::IO::Alert->warn( $self, ': loading ', $m->{instance}, ' missing key ', $map->{$pk}->{name}); return (undef, $mi); } $v = $req->get('auth_id'); } $query->{$pk} = $v; } return ($query, $mi); } sub _initialize_class_info { my($class, $config) = @_; # Initializes from class or from config. config is supplied for # anonymous models (currently, only ListModels). # This may load the models and we'll try to get the class_info # again after the models are loaded. _load_all_property_models(); # Have here for safety to avoid infinite recursion if called badly. { no strict qw(refs); return if !$config && defined *{$class . '::'}{HASH}->{_CLASS_INFO}; } _assert_class_name($class) unless $config; my($stmt) = $_SS->new(); my($sql_support) = $class->internal_initialize_sql_support($stmt, $config); my($ci) = { sql_support => $sql_support, statement => $stmt, as_string_fields => _as_string_fields($sql_support), # Is an array, because faster than a hash_ref for our purposes properties => [map { ($_, undef); } @{$sql_support->get('column_names')}, ], }; return $ci if $config; { no strict qw(refs); *{$class . '::'}{HASH}->{_CLASS_INFO} = $ci; } $ci->{singleton} = $class->new; delete($ci->{singleton}->[$_IDI]->{request}); $ci->{singleton}->[$_IDI]->{is_singleton} = 1; return; } sub _load_all_property_models { return if $_LOADED_ALL_PROPERTY_MODELS; $_LOADED_ALL_PROPERTY_MODELS = 1; my($models) = $_CL->map_require_all( 'Model', sub { my($class, $file) = @_; # We don't load classes which end in List, Form, or Base. return $class =~ /(Form|List|Base)$/ ? 0 : 1; }); foreach my $class (@$models) { $class->get_instance; } return; } sub _load_other_model { my($self, $name) = @_; # Does a bunch of asssertion checking my($query, $mi) = _get_model_query($self, $name); return $mi unless $query; my($aliases) = $self->get_info('column_aliases'); my($values) = $self->internal_get; return $mi->internal_load_properties({ map({ my($k) = $aliases->{"$name.$_"}; unless ($k && exists($values->{$k})) { $mi->unauth_load($query); return $mi; } ($k => $values->{$k}); } @{$mi->get_info('column_names')}), }); } sub _new_args { my($proto, $req, $class) = @_; # Returns (proto, req, class). Figures out calling form and returns # the correct parameter values. if (defined($req) && !ref($req)) { Bivio::Die->die($req, ': bad parameter, expecting a Bivio::Agent::Request', ) if defined($class); $class = $req; $req = undef; } return ($proto, $req || $proto->unsafe_get_request, $class); } sub _new_with_query { my($proto, $query) = @_; # Instantiates I and loads/processes I if supplied. my($self) = $proto->new; return $self unless $query; my($is_unauth) = $proto->my_caller(1) =~ /unauth/; if ($self->isa('Bivio::Biz::FormModel')) { $self->process($query); } elsif ($self->isa('Bivio::Biz::ListModel')) { my($method) = $is_unauth ? 'unauth_load_all' : 'load_all'; $self->$method($query); $self->set_cursor(0); } elsif ($self->isa('Bivio::Biz::PropertyModel')) { my($method) = $is_unauth ? 'unauth_load_or_die' : 'load'; $self->$method($query); } else { b_die($self, ': does not support query argument: ', $query); } return $self; } sub _well_known_name { my($self, $names, $choices) = @_; foreach my $n (@$names) { my($constant) = uc($n) . '_FIELD'; return $self->$constant() if $self->can($constant); } $self->die($names, ': no choices') unless defined($choices); return $choices->{name} if ref($choices) eq 'HASH'; $self->die($choices, ": too many $names->[0] values") if @$choices > 1; $self->die($choices, ": too few $names->[0] values") if @$choices < 1; return $choices->[0]; } sub _well_known_value { my($self) = @_; my($name) = $self->my_caller . '_name'; return $self->get($self->$name()); } 1;