# Copyright 2002-2008 Josh Clark and Global Moxie, LLC. This code cannot be
# redistributed without permission from globalmoxie.com.  For more
# information, consult your Big Medium license.
#
# $Id: Data.pm 3043 2008-03-31 14:00:38Z josh $

package BigMed::Data;
use strict;
use warnings;
use utf8;
use Carp;
use base qw(BigMed::Error);
use BigMed::Elements;
use BigMed::Driver;
use BigMed::Trigger;

use vars qw(%schema %source_map $AUTOLOAD);
%schema     = ();
%source_map = ();
my $DRIVER;

###########################################################
# SCHEMA SETTERS
###########################################################

sub get_base_schema {
    my $class = shift;
    my $columns;
    my $parent;
    {
        no strict 'refs';
        $parent = ${"${class}::ISA"}[0];
    }

    if ( $class eq 'BigMed::Data' || $parent eq 'BigMed::Data' ) {

        #no parent
        $columns = [
            {   name  => 'id',
                type  => 'id',
                index => 1,
            },
            {   name  => 'mod_time',
                type  => 'system_time',
                index => 1,
            },
            {   name => 'create_time',
                type => 'system_time',
            },
        ];
    }
    else {
        foreach my $column ( $parent->data_columns ) {
            push( @{$columns}, $schema{$parent}->{elements}->{$column} );
        }
    }
    return $columns;
}

sub set_schema {
    my $class = shift;
    @_ % 2
      && croak 'set_schema arguments must be hash form: odd number of '
      . 'parameters. Check key/value pairs';
    my %arg = @_;
    my $lc_class;
    if ( $class =~ /([^:]+)\z/ms ) {
        $lc_class = $1;
        $lc_class = lc($lc_class);    #have to do it in 2 steps for Perl 5.6.1
    }
    $arg{source} ||= $lc_class . 's' if $lc_class;
    $arg{source} or croak "No data source provided in schema for $class";
    $arg{label} ||= $lc_class || $arg{source};
    $arg{elements} = [] unless $arg{elements};
    ref( $arg{elements} ) eq 'ARRAY'
      or croak "Elements argument for $class must be array reference";

    my %desc = (
        source     => $arg{source},
        label      => $arg{label},
        systemwide => $arg{systemwide} ? 1 : 0,
    );
    my $base_elements = $class->get_base_schema();
    foreach my $ritem ( @{$base_elements}, @{ $arg{elements} } ) {
        if ( ref $ritem ne 'HASH' ) {
            croak "Element definition not a hash ref in '$class' schema";
        }
        my %item = %{$ritem};
        if ( !$item{name} || !$item{type} ) {
            croak 'Name and/or type missing in element definition '
              . "for '$class' schema.";
        }
        if ( !BigMed::Elements->element_exists( $item{type} ) ) {
            croak "Unknown element type '$item{type}' for '$item{name}' "
              . "column in '$class' schema";
        }
        if ( $desc{elements}->{ $item{name} } ) {
            croak "Duplicate element name '$item{name}' not allowed in "
              . "element definition for '$class' schema. (Maybe you used "
              . 'a name reserved for one of parent class elements?)';
        }
        push( @{ $desc{columns} }, $item{name} );
        $item{must_unpack} =
          $item{multiple} || BigMed::Elements->must_unpack( $item{type} );

        #indexed and unique items (unique must be indexed)
        $item{index} = 1 if $item{unique};
        push @{ $desc{index} }, \%item if $item{index};
        push @{ $desc{unique} }, ( scalar @{ $desc{index} } - 1 )
          if $item{unique};

        $desc{elements}->{ $item{name} } = \%item;
    }
    $desc{systemwide} = 1 if !$desc{elements}->{site};
    $schema{$class} = \%desc;
    $source_map{ $desc{source} } = $class;
    return $class;
}

###########################################################
# DATA OBJECT CONSTRUCTORS
###########################################################

sub new {
    my $class = shift;
    my $data_obj = bless {}, $class;
    $data_obj->initialize(@_);
    return $data_obj;
}

sub initialize {
    my $data_obj = shift;
    $data_obj->{_values} = { data_class => ref($data_obj) };
    $data_obj->{_stash} = {};
    return $data_obj;
}

sub clear {

    #removes all in-memory values from the object, returning
    #a clean data object. (Does not touch any data previously
    #saved to the data store for this object).
    $_[0]->initialize();
    return $_[0];
}

###########################################################
# BUILD ACCESSOR METHODS
###########################################################

#create the getters and setters based on column values
#for this class
sub AUTOLOAD {
    my $class = ref( $_[0] ) or return;
    no strict 'refs';
    my $method = $AUTOLOAD;
    $method =~ s/.*:://ms;
    my $relements = $schema{$class}->{elements};
    return unless $method =~ /[^A-Z]/ms;    #skip DESTROY et al
    if ( exists $relements->{$method} ) {   #getter
        if ($relements->{$method}->{multiple}
            || BigMed::Elements->property(
                $relements->{$method}->{type}, 'array'
            )
          )
        {
            *{$AUTOLOAD} = sub {            #deref array
                my $v = $_[0]->{_values}->{$method};

                #it's 2x faster to skip creating new array
                #unless necessary
                return ref $v eq 'ARRAY' ? @{$v}
                  : ref $v    eq 'HASH'  ? %{$v}
                  : ( my @array = defined $v ? ($v) : () );
            };
        }
        else {
            *{$AUTOLOAD} = sub { #scalar, return as-is
                return $_[0]->{_values}->{$method};
            };
        }

        goto &{$AUTOLOAD};
    }
    elsif ( $method =~ s/\Aset_//ms
        && exists $relements->{$method} )
    {    #setter
        *{$AUTOLOAD} = sub {
            my $obj = $_[0];

            #ideally we would de- and re-reference the stowed
            #object, but we need the performance savings here.
            #it's a modest savings, though, so if it becomes
            #important to add this back in, we can do it.
            # my $value = !ref $_[1] ? $_[1]
            #   : ref $_[1] eq 'ARRAY' ? [ @{ $_[1] } ]
            #   : ref $_[1] eq 'HASH' ? { %{ $_[1] } }
            #   : $_[1];

            my $old = $obj->{_values}->{$method};
            $obj->{_values}->{$method} = $_[1];

            #mod check very simple, doesn't deal with refs
            $obj->{_modified} ||= !defined $old
              || !defined $_[1]
              || $old ne $_[1];

            return $_[1];
        };

        goto &{$AUTOLOAD};
    }
    elsif ( $method =~ s/^_ref_//ms
        && exists $relements->{$method} )
    {    #_ref_ reference getter for complex structures
        *{$AUTOLOAD} = sub {
            my $v = $_[0]->{_values}->{$method};
            return !ref $v ? $v
              : ref $v eq 'ARRAY' ? [@{$v}]
              : ref $v eq 'HASH'  ? { %{$v} }
              : $v;
        };
        goto &{$AUTOLOAD};
    }
    return croak "Method '$method' does not exist in the $class data class.";
}

# can - Help UNIVERSAL::can() cope with our AUTOLOADed methods
sub can {
    my ( $self, $method ) = @_;
    my $subref = $self->SUPER::can($method);
    return $subref if $subref;    # can found it; it's a real method

    # Method doesn't currently exist; should it, though?
    my $class = ref $self ? ref($self) : $self;
    my $check_method = $method;
    $check_method =~ s/^set_//ms;
    return unless exists $schema{$class}->{elements}->{$check_method};

    # Return an anon sub that will work when it's eventually called
    return sub {
        my $self = $_[0];

        # The method is being called.  The real method may have been
        # created in the meantime; if so, don't call AUTOLOAD again
        my $subref = $self->SUPER::can($method);
        goto &{$subref} if $subref;

        $AUTOLOAD = $method;
        goto &AUTOLOAD;
    };
}

###########################################################
# PATCH THROUGH TO DRIVER METHODS
###########################################################

sub set_driver {
    $DRIVER = BigMed::Driver->new( $_[1], $_[2] );
    return $DRIVER;
}

sub driver {
    return $DRIVER;
}

sub save {
    defined $DRIVER
      or croak 'Big Medium driver must be initialized before saving data';
    defined( $_[0]->call_trigger('before_save') ) or return;
    defined( $DRIVER->save(@_) )                  or return;
    defined( $_[0]->call_trigger('after_save') )  or return;
    return 1;
}

sub select {
    defined $DRIVER
      or croak 'Big Medium driver must be initialized before calling select';
    my $class = ref $_[0] || $_[0];
    return $DRIVER->select( $_[1], $_[2], $class->data_source );
}

sub reindex {
    defined $DRIVER
      or croak 'Big Medium driver must be initialized before calling reindex';
    my $class = shift;
    $class = ref $class if ref $class;
    return $DRIVER->reindex($class->data_source, @_);
}

sub join_has {
    defined $DRIVER
      or croak 'Big Medium driver must be initialized before select_join';
    my $class = ref $_[0] || $_[0];
    return $DRIVER->join_has( $_[1], $_[2], $_[3], $class->data_source );
}

sub join_points_to {
    defined $DRIVER
      or croak 'Big Medium driver must be initialized before select_join';
    my $class = ref $_[0] || $_[0];
    return $DRIVER->join_points_to( $_[1], $_[2], $_[3],
        $class->data_source );
}

sub join_pointed_from {
    defined $DRIVER
      or croak 'Big Medium driver must be initialized before select_join';
    my $class = ref $_[0] || $_[0];
    return $DRIVER->join_pointed_from( $_[1], $_[2], $_[3],
        $class->data_source );
}

sub fetch {
    defined $DRIVER
      or croak 'Big Medium driver must be initialized before fetching data';
    my $class = ref $_[0] || $_[0];
    return $DRIVER->fetch( $_[1], $_[2], $class->data_source );
}

sub trash {
    defined $DRIVER
      or croak 'Big Medium driver must be initialized before trashing data';
    defined( $_[0]->call_trigger('before_trash') ) or return;
    defined( $DRIVER->trash(@_) )                  or return;
    defined( $_[0]->call_trigger('after_trash') )  or return;
    return 1;
}

sub update_id {
    defined $DRIVER
      or croak 'Driver must be initialized before calling update_id';
    return $DRIVER->update_id( $_[0] );
}

sub has_valid_id {
    defined $DRIVER
      or croak 'Driver must be initialized before calling has_valid_id';
    return $DRIVER->has_valid_id( $_[0] );
}

sub is_valid {
    my $class = ref( $_[0] ) ? ref( $_[0] ) : $_[0];
    return if !$class || !$_[1];
    croak "is_valid: No such element $_[1] in $class"
      if !exists $schema{$class}->{elements}->{ $_[1] }->{type};
    my $data = exists $_[2] ? $_[2] : $_[0]->{_values}->{ $_[1] };
    return BigMed::Elements::validate(
        $schema{$class}->{elements}->{ $_[1] }->{type}, $data );
}

sub is_unique {
    my $obj           = shift;
    my $column        = shift || croak 'is_unique: No data column specified';
    my $prefab_select = shift;

    my $class = ref $obj;
    ( $class && $obj->isa('BigMed::Data') )
      or croak 'is_unique is an object method; no object supplied';
    $schema{$class} || croak "is_unique: $class is an unknown data type";
    my %properties = $obj->properties;
    $properties{$column}->{index}
      || croak "is_unique: Data column '$column' is not an indexed column";

    my $value = $obj->$column;
    croak "is_unique: $column value is empty, no value to compare"
      if !defined $value || $value eq q{};

    my %search = ( $column => $obj->$column );
    $search{site} = $obj->site if !$class->systemwide;
    my $source = $prefab_select || $class;   #use provided selection if avail.
    my $matches = $source->select( \%search ) or return;

    my $unique = 1;
    while ( my $match = $matches->next ) {
        next if $obj->id && $match->id == $obj->id;
        $unique = 0;                         #found another object
        last;
    }
    return $unique;
}

sub copy {
    my $obj = shift;
    my $rparam = shift || {};

    my $class = ref $obj;
    my $clone = $class->new();
    $clone->update_id();
    foreach my $col ( $class->data_columns ) {
        next if $col eq 'id' || $col eq 'mod_time' || $col eq 'create_time';
        my $getter = "_ref_$col";
        my $v = $obj->$getter;
        if (ref $v eq 'ARRAY') {
            $v = [ @{$v} ];
        }
        elsif (ref $v eq 'HASH') {
            $v = { %{$v} };
        }
        my $setter = "set_$col";
        $clone->$setter($v);
    }
    if (!$obj->systemwide && $obj->can('set_site')) {
        my $site = $rparam->{target_site} || $obj->site;
        $clone->set_site(ref $site ? $site->id : $site);
    }
    defined( $obj->call_trigger('after_copy', $clone, $rparam) ) or return;
    return $clone;
}

###########################################################
# SCHEMA ACCESSORS
###########################################################

sub data_columns {
    my $class = ref $_[0] || $_[0];
    return if !$schema{$class};
    return @{ $schema{$class}->{columns} };
}

sub index_columns {
    my $class = ref $_[0] || $_[0];
    return if !$schema{$class};
    return @{ $schema{$class}->{index} };
}

sub unique_indices {

    #returns an array of the index values of the index_columns array
    #that correspond to columns marked as unique
    my $class = ref $_[0] || $_[0];
    return if !$schema{$class};
    return $schema{$class}->{unique} ? @{ $schema{$class}->{unique} } : ();
}

sub data_source {
    my $class = ref $_[0] || $_[0];
    return if !$schema{$class};
    return $schema{$class}->{source};
}

sub source_class {
    return $source_map{ $_[1] };
}

sub data_label {
    my $class = ref $_[0] || $_[0];
    return if !$schema{$class};
    return $schema{$class}->{label};
}

sub class_for_label {
    my $self  = shift;
    my $label = shift;
    foreach my $class ( keys %schema ) {
        return $class if $schema{$class}->{label} eq $label;
    }
    return;    #none found
}

sub properties {
    my $class = ref $_[0] || $_[0];
    return if !$schema{$class};
    return %{ $schema{$class}->{elements} };
}

sub default_value {
    my $class = ref $_[0] || $_[0];
    return
      if !$_[1]
      || !$schema{$class}
      || !$schema{$class}->{elements}->{ $_[1] };
    return $schema{$class}->{elements}->{ $_[1] }->{default};
}

sub supply_default_values {

    #set default values for any undefined values
    my $data_obj   = shift;
    my %properties = $data_obj->properties;
    foreach my $column ( $data_obj->data_columns ) {
        next
          if defined $data_obj->$column()
          || !defined $properties{$column}->{default};
        my $setter = "set_$column";
        $data_obj->$setter( $properties{$column}->{default} );
    }
    return $data_obj;
}

sub systemwide {
    my $class = ref $_[0] || $_[0];
    return if !$schema{$class};
    return $schema{$class}->{systemwide};
}

sub is_modified {
    return $_[0]->{_modified};
}

sub mark_unmodified {
    $_[0]->{_modified} = undef;
    return $_[0];
}

sub mark_modified {
    $_[0]->{_modified} = 1;
    return $_[0];
}


###########################################################
# STASH GETTERS/SETTERS
###########################################################

sub set_stash {
    my $data_obj = shift;
    @_ % 2
      && croak 'set_schema arguments must be hash form: odd number of '
      . 'parameters. Check key/value pairs';
    my %arg = @_;

    while ( my ( $k, $v ) = each(%arg) ) {
        $data_obj->{_stash}->{$k} = $v;
    }
    return $data_obj;
}

sub stash {
    my $data_obj = shift;
    if (wantarray) {
        my @value;
        foreach my $key (@_) {
            push @value,
              ( defined $key ? $data_obj->{_stash}->{$key} : undef );
        }
        return @value;
    }
    return $data_obj->{_stash}->{ $_[0] } if defined $_[0];
    return;
}

###########################################################
# HOOK ROUTINES
# wrappers to Class::Trigger's add_trigger and call_trigger
###########################################################

sub add_callback {
    my ( $self, $hook_name, $coderef ) = @_;
    if ( $hook_name =~ /trash_all$/ms ) {    #actually for driver objects
        my $hook_class = ref $self || $self;
        my $driver_sub = sub {
            my $sclass = $_[0]->selection_class;
            return 1 if !$sclass || !$sclass->isa($hook_class);
            $coderef->(@_);
        };
        BigMed::Driver->add_trigger( $hook_name, $driver_sub );
    }
    else {
        $self->add_trigger( $hook_name, $coderef );
    }
    return;
}

1;
__END__

=head1 Name

=head2 BigMed::Data

Big Medium base class for database objects

=head1 Synopsis

=head2 Creating a BigMed::Data subclass

All Big Medium content is managed through subclasses of BigMed::Data.
BigMed::Data is an abstract class and is used only via inheritance.
Create a BigMed::Data subclass by defining the data columns and their
properties, and register this definition via the set_schema method:

    package BigMed::MyContent;
    use BigMed::Data;
    use base qw(BigMed::Data);
    
    #define the data columns and properties
    my @data_schema = (
        {
            name     => 'name',
            type     => 'simple_text',
            index    => 1,
            required => 1,
            default  => '',
        },
        {
            name  => 'site',
            type  => 'system_id',
            index => 1,
        },
        {
            name    => 'content',
            type    => 'rich_text',
            default => '',
        },
    );
    
    #register the subclass and its properties
    __PACKAGE__->set_schema(  elements => \@data_schema );

=head2 Using a BigMed::Data subclass

Use a Big Medium subclass to search, load, save, update and delete
data from the Big Medium data store:

    use BigMed;
    use BigMed::MyContent;
    
    # Create a BigMed object to load the basic system config
    # and initialize the data driver
    my $BM = BigMed->new('Web');
    
    # Create a MyContent object
    my $data = BigMed::MyContent->new();
    
    # Get an id for the object
    $data->update_id() or $data->error_stop;
    my $id = $data->id;
    
    #Set the object's name, then print it.
    $data->set_name('My name');
    print $data->name;  # prints 'My name'
    
    #Save the object to disk
    $data->save or $data->error_stop;
    
    #Load the object from disk
    defined ( my $data2 = BigMed::MyContent->fetch($id) )
        or BigMed::MyContent->error_stop;
    print $data2->name; # prints 'My name'

=head1 Description

BigMed::Data is the base class for all Big Medium data/content
objects that are database-backed.

The BigMed::Data base class and its sub-classes define the
in-memory representation of Big Medium data and provide an easy,
consistent interface for accessing and manipulating Big Medium
data objects without needing to know any of the details of how
and where that data is ultimately stored.

The database can be any type of data store: a relational database,
a flatfile system, a remote storage system accessed via XML-RPC, etc.
BigMed::Data doesn't actually know anything about the database itself
or how the data will be stored. That information as well as the actual
data storage and retrieval is handled behind the scenes by the
BigMed::Driver class and its associated subclasses.

=head1 BigMed::Data Subclasses

The BigMed::Data base class does not itself have data objects. Instead,
Big Medium data is defined and contained in BigMed::Data subclasses.

To create a subclass, you just need to provide the data columns
to be used by the subclass, along with a bit of metadata about each
column, and then register this information with the C<set_schema>
method.

Start by declaring your subclass name (via C<package>) and using
BigMed::Data as the base class for the package:

    package BigMed::MyContent;
    use BigMed::Data;
    use base qw(BigMed::Data);

Then create your data definition. This should be an array of hash
references, where each hash reference represents a data column and
its associated metadata:

    my @data_schema = (
        {
            name     => 'name',
            type     => 'simple_text',
            index    => 1,
            required => 1,
            default  => '',
        },
        {
            name  => 'site',
            type  => 'system_id',
            index => 1,
        },
        {
            name  => 'section',
            type  => 'system_id',
            index => 1,
        },
        {
            name    => 'content',
            type    => 'rich_text',
            default => '',
        },
    );

Register the data definition via the set_schema method:

    BigMed::MyContent->set_schema( \@data_schema );

=head2 Column Metadata

The hash reference for each column consists of some or all of the
following key/value pairs (the keys are case-sensitive; they should
always be lowercase). Only name and type are required.

=over

=item * name => "column" (I<required>)

The name of the data column. This will be used as the method name to
retrieve the data for this column (and will also be used as part of
the method name for setting the data, too). The name could also
be used as a column name in a database behind the scenes. As a
result, it's recommended that you keep these column names simple:
lowercase, alphanumeric or underscore, 64 chars max, don't start
with a number.

=item * type => "element_type" (I<required>)

This is the BigMed::Element element type, which defines the data
format and editing interface for this data. The element type must
be a valid BigMed::Element type.

=item * index => 1

This optional item indicates whether this data column should be
indexed for fast retrieval and sorting. Indexed columns are available
to be searched and sorted via the C<select> method. Setting
to a true value means that it will be indexed. By default,
columns are not indexed.

Choose indexed columns carefully. More indexed columns might give you
more search/sort flexibility, but they also come with a performance
cost. In general, you should index only those key columns that you
will use to identify and sort these data elements.

=item * unique => 1

This optional item indicates whether this data column must have a unique
value.

If you attempt to C<save> an object with a column value that matches
a column value of another object already in the data store, and that column
has the unique attribute, the object will not be saved, and the C<save>
method will return undef and set an error message. Conflicts can be checked
before saving by using the C<is_unique> object method.

A few notes on how "uniqueness" is determined:

=over 4

=item * Objects are compared only to objects assigned to the same site,
unless the object class is "systemwide" (see the C<systemwide>
parameter of the C<set_schema> method below)

=item * Objects are compared only to objects from the same data source.

=item * "Uniqueness" is not case-sensitive.

=back

=item * default => "default value"

This optional item indicates the default value for this column.
This value is used to set values for undefined columns when the
C<supply_values> method is called.

=item * multiple => 1

This optional item indicates whether to allow multiple values for
this column. Setting to a true value means that it can
have multiple values. (When the item is retrieved via its getter
method, it will be returned as an array; in scalar context it will
return the number of items in the value.).

=item * options => [$value1, $value2 ... ]

For columns whose element type can offer a choice from a set of predefined
values (e.g. those that prompt with a C<< <select> >> tag), this optional
parameter provides a reference to the array of values. If left undefined,
the column will inherit the options, if any, defined for this element
type in BigMed::Element.

=item * labels => { $value1 => $label1, $value2 => $label2 ... }

For columns whose element type can offer a choice from a set of predefined
values (e.g. those that prompt with a C<< <select> >> tag), this optional
parameter provides the display labels for each value. The keys are the values
from the C<options> parameter above, and the values are the (unlocalized and
unescaped) labels to display. As with C<options> above, if the C<labels>
attribute is left undefined, the column will inherit the labels, if any,
defined for this element type in BigMed::Element.

=back

=head2 Inherited columns

All BigMed::Data subclasses inherit a basic set of data columns:

=over

=item * id

A unique numeric id for the data object. (You can obtain an id for
the object via the C<update_id> method). This column is indexed.

=item * mod_time

The last time the data object was saved. This column is indexed.

=item * create_time

The first time the data object was saved. This column is I<not>
indexed.

=back

=head2 Registering Your BigMed::Data Subclass

=over 0

=item C<set_schema>

=back

After defining your data columns in an array, you should register
the subclass by using the C<set_schema> method, a static
class method that must be called using the class name of your
subclass. Calls to the method accept a hash of parameters, and it typically
can be reduced to just a single parameter like so:

    BigMed::MyContent->set_schema( elements => \@data_schema );

    #for elements that are not site-specific but systemwide (like users)
    BigMed::User->set_schema(
        elements   => \@data_schema,
        systemwide => 1,
    );

To override default naming conditions and for data source and data label,
you can also set the source and/or label parameters as described below.

The key/value pairs in the argument may include:

=over

=item * elements => \@data_schema

A reference to your data schema array.

=item * systemwide => 1

If true, indicates that this data class is not site specific but system-wide.
The systemwide flag will also be set if the column has no "site" column.
For performance, some Big Medium data drivers partition site-specific data.
Setting a data subclass to be systemwide indicates that the data should be
available to all sites.

=item * source => "sourcename"

The name of the data source. This should be a name that is unique
to this subclass and will be used internally by the data driver,
which could use it as a SQL table name, a file directory, etc. It
is recommended to keep this name simple: a lowercase name that
consists only of the letters a-z.

If left undefined, the source name will correspond to the lower-case, plural
version of the last portion of the class name.

    BigMed::MyContent->set_schema();      #source is mycontents
    BigMed::Animal::Dog->set_schema();    #source is dogs

Note that this automatic pluralization is not particularly clever -- it just
adds a "s" to the end of class name, so some class names may result in
poor grammatical constructions:

    BigMed::Person->set_schema();           #source is persons
    BigMed::Person::Woman->set_schema();    #source is womans

    #in these cases, better to specify a source name explicitly
    BigMed::Person->set_schema( source => 'people' );
    BigMed::Person::Woman->set_schema( source => 'women' );

=item * label => "internal_identifier"

The name to use as an internal identifier (in localization text, for example)
to refer to this subclass within Big Medium.  If left undefined, the source
name will correspond to the lower-case version of the last portion
of the class name -- same treatment as source, above, but not pluralized.

    BigMed::Person->set_schema();          #label is person
    BigMed::Media::Image->set_schema();    #label is image

=back

=head1 Usage

=head2 System Initialization

Before using a BigMed::Data subclass and its objects, you
must first initialize the Big Medium system by creating a BigMed
or BigMed::Admin object. This automatically loads the configuration
settings for the system, including initializing the data driver
required for loading and saving data objects:

    use BigMed;
    my $BM = BigMed->new('Web');

or

    use BigMed::Admin;
    my $BM = BigMed::Admin->new('Web');

=head2 Creating a New Object

=over 0

=item * C<new>

To create a new object from scratch, use the C<new> constructor
method:

    my $data = BigMed::MyContent->new();

C<new> takes no arguments and returns an empty data object.

=item * C<<$data->copy( \%param )>>

    my $copy = $data->copy;
    my $copy_at_new_site = $data->copy( { target_site => $new_site } );

Returns a copy of the object on which the C<copy> method is called
(or undef on error). The copy is an identical unsaved clone of the
original object, except for three fields:

=over 4

1. The copy is assigned a new id.

2. The copy has no create_time value.

3. The copy has no mod_time value.

=back

The method accepts an optional argument, a hash reference of parameter
values. The only parameter used by the built-in method is the C<target_site>
parameter, which indicates the target site for the object copy. The
value may be either a BigMed::Site object or its id. This parameter has
no effect on systemwide data classes.

BigMed::Data subclasses commonly extend this method via the C<after_copy>
callback trigger (see the L</"Callbacks"> section below).

=back

=head2 Getting and Setting Data Values

BigMed::Data automagically creates getter/setter accessor methods
with names corresponding to the subclass's data methods.

=head3 Getters

To get the value of the object's id and foo columns:

    my $id = $data->id;
    my $foo = $data->foo;

Columns that are set to contain multiple values or key/value pairs
retrieve the full hash or array. For example, if our object belongs
to a class with columns named "sections" (a list of multiple values)
and "titles" (a list of key/value pairs), they would be retrieved like
so:

    my @sections = $data->sections;
    my %titles = $data->titles;

To retrieve these arrays or hashes as reference values, you can do that
by prepending _ref_ to the method name, like so:

    my $rsections = $data->_ref_sections; #reference to sections array
    my $rtitles = $data->_ref_titles; #reference to titles hash

(Note: These references do not refer directly to the stored values;
changing their values will not affect the values stored in the object.
Also, calling _ref_ for a scalar value returns the scalar, not a
reference).

=head3 Setters

To set the value, prepend "set_" to the column name like so:

    $data->set_id($id);
    $data->set_foo($foo);

Columns that contain multiple values or key/value pairs are set with
array and hash references, respectively:

    $data->set_sections(\@sections);
    $data->set_titles(\%titles);

(Note: The referenced data is copied, so any changes to the original
array or hash after setting the value will not affect the stored value).

=over 0

=item * C<update_id>

=back

The C<update_id> method is a special type of getter/setter method
that assigns and returns a unique id to the object if it
does not yet have an id. If the object already has an id, the object's
id remains unchanged. The return value is the id value.

Depending upon the Big Medium file driver being used, this method
typically involves database or disk access and could return an
error if there's a problem. If so, the return value will be undef,
and an error will be placed into the singleton error handler.
It's wise to include an error check when calling this routine:

    my $id = $data->update_id or $data->error_stop;

(You don't have to assign an id to an object yourself; if no id has
been assigned to the object when the C<save> method is called, an
id will be assigned automatically).

See also the related C<has_valid_id> method.

=head2 Default Values

When BigMed::Data objects are first created via the C<new> method,
their data values are all undefined.

If you want to get an object that's populated with default values,
two methods can help:

=over 0

=item * C<supply_default_values>

Replaces any undefined value in the object with the
default value for that column.

    $data->supply_default_values;

=item * C<default_value>

Looks up the default value of a column. The C<default_value> method
accepts the name of the column to look up and returns the default
value for that column. You can call this method as a static class
method:

    $default_value = BigMed::MyContent->default_value('column');

...or as an object method:

    $default_value = $data->default_value('column');

=back

=head2 Saving the Object

=over 0

=item * C<save>

=back

To save your data object, call the C<save> method:

    $data->save;

It's good practice to check for errors when calling this
method. If there's a problem, the return value will be undef, and an
error will be set to the singleton error array. Depending on
how and where you're using the C<save> method, you may want
to stop and display the error:

    $data->save or $data->error_stop;

...or return undef to the calling routine and let it decide
what to do with the error:

    $data->save or return undef;

=head2 Looking up Saved Objects

=over 0

=item * C<select>

=back

You can search and sort existing objects by using the C<select>
method:

    my $selection = BigMed::MyContent->select(
        \%search_terms,
        \%arguments
    );
    
    #search against a previous selection
    $winnow = $selection->select( \%search2, \%args2 );

C<select> returns a driver selection object that lets you manage the
results of the search. You can use this object to retrieve the
individual objects (via the C<fetch> method) and/or to do additional
searches within the found set (via the C<select> method).

C<select> takes two parameters, both of them hash references: the
first parameter contains the search terms 

=over

=item 1 Search terms

This is a reference to a hash where the keys are column names,
and the values are the column values to match. If the
search_terms parameter is undefined, or if the hash reference
is empty, the selection will match all objects in the class.

If you have multiple key/value pairs, the search operates as an
"and" match by default, selecting only the objects with matches for all
of the columns named in the keys. (You can change it to an "or" match
by setting the "any" argument to true in the search arguments, see below).

Only columns that are indexed can be searched.

The search-term values can come in one of three forms:

=over

=item * Scalar (exact match)

This selects only objects where the data column named in the key
matches the value (the match is case-insensitive). For example,
to find all BigMed::MyContent objects where the foo column is
equal to bar (or Bar, BAR, BaR, etc):

    my $selection = BigMed::MyContent->select(
        { 'foo' => 'bar' },
        \%arguments
    );

=item * Regular expression

This selects only objects where the data column named in the key
matches the regular expression (the match is case-sensitive or insensitive
according to whether you include the C<i> switch with the regexp).
For example, to find all BigMed::MyContent objects where the foo column
included bar anywhere in the text:

    my $selection = BigMed::MyContent->select(
        { 'foo' => qr/bar/ },
        \%arguments
    );

...and to make it case-insensitive, add the "i" switch to th regexp:

    my $selection = BigMed::MyContent->select(
        { 'foo' => qr/bar/i },
        \%arguments
    );

=item * Array reference ("or" match)

This is an "or" search that selects objects where the data column
named in the key matches any of the values named in the array
reference. For example, to find BigMed::MyContent objects where
the id is equal to $id1, $id2, or $id3:

    my $selection = BigMed::MyContent->select(
        { 'id' => [$id1, $id2, $id3] },
        \%arguments
    );

=item * Hash reference (range match)

This range match allows you to supply "from" and/or "to" values that
define the boundaries of a range to match. The search selects objects
where the data is is greater than or equal to the "from" value and
less than or equal to the "to" value.

If you omit the "to" value, then
the search will select all values greater than or equal to the
provided "from" value. Likewise, if you omit the "from" value,
the search will select all values less than or equal to the "to" value.
For example, to find all BigMed::MyContent objects where the name
column has a value between "a" and "czzz" inclusive:

    my $selection = BigMed::MyContent->select(
        { 'name' => { from=>'a', to=>'czzz' } },
        \%arguments
    );

=back

=item 2 Search arguments

This is a reference to a hash whose key/value pairs indicate various
ways to sort and massage the search results:

=over

=item * sort => "column"

The name of the column by which to sort the results:

    my $selection = BigMed::MyContent->select(
        \%search_terms,
        { 'sort' => 'name' }
    );

=item * sort => [ "column1", "column2", "column3" ... ]

If the value of the sort key is an array reference, the results
are sorted by the first column in the array, with matching values
sorted by the second column of the array. Objects with the same
values in the first and second column are then sorted by the third
column in the array, etc.

    my $selection = BigMed::MyContent->select(
        \%search_terms,
        { 'sort' => ['name', 'mod_time', 'create_time'] }
    );

=item * order => "ascend"

The direction in which to sort the results. The value should be either
"ascend" or "descend" -- the default order is "ascend."

    my $selection = BigMed::MyContent->select(
        \%search_terms,
        {
          'sort'  => 'name',
          'order' => 'ascend',
        }
    );

=item * order => [ "ascend", "descend", "descend" ... ]

Like sort, order  may also be put into an array
reference to indicate the sort order of multiple columns:

    my $selection = BigMed::MyContent->select(
        \%search_terms,
        {
          'sort'  => ['name', 'mod_time', 'create_time'],
          'order' => ['ascend', 'descend', 'descend'],
        }
    );

=item * any => 1, any => [ 'colname1', 'colname2', ... ]

Sets the search to be an OR match for one or more columns. By default,
searches are AND matches, returning only objects that match all search
terms. Setting the "any" argument turns the search into an OR search
or a mixed AND and OR search.

Setting "any" to a true scalar value will return results that match
any search term:

    my %search_terms = (
        'name' => 'foo',
        'year' => { from => 2001 },
        'city' => 'Akron',
    );
    
    # return items whose name is foo OR whose year is 2001 or higher OR
    # whose city is Akron.
    my $selection = BigMed::MyContent->select(
        \%search_terms,
        { any => 1 }
    );

Setting "any" to an array reference with the names of two or more columns
from the search terms returns results that match the terms for any
of those columns, but all of the remaining columns.

    my %search_terms = (
        'name' => 'foo',
        'year' => { from => 2001 },
        'city' => 'Akron',
    );
    
    # return only items whose city is Akron and, within that subset,
    # whose whose year is 2001 OR whose name is foo.
    my $selection = BigMed::MyContent->select(
        \%search_terms,
        { any => ['year', 'name'] }
    );

If set to a true value, the search will be an "or" match, returning
objects that match any search terms. 

=item * limit => N

Limit caps the number of results to N objects. The value of N
may be only a positive integer. 

    my $selection = BigMed::MyContent->select(
        \%search_terms,
        { 'limit' => 15 }
    );

In many cases, specifying a limit improves search performance, and
it's particularly good practice to limit searches where you know
there can be only one result (for example, when matching against a
single id):

    my $selection = BigMed::MyContent->select(
        { 'id'    => $id },
        { 'limit' => 1 }
    );

=item * offset => M

This shifts the results in the returned set by the number specified.
For example, an offset of 1 would drop the first result, and
the returned selection would start with the second item in the
matching set.

    my $selection = BigMed::MyContent->select(
        \%search_terms,
        { 'offset' => 1 }
    );

This is particularly useful when combined with limit to return
results M + 1 through M + N. For example, say that you wanted to do
something on a web page with groups of articles, with 15 objects per
page: This is useful, for example, when you want to display search
results in groups or pages. For example, say that you wanted to do
something on a web page with groups of articles, with 15 objects per
page:

    #this matches objects 1-15
    $selection = BigMed::MyContent->select(
        \%search_terms,
        { 'offset' => 0, 'limit' =>15 }
    );
    
    #this matches the next 16-30
    $selection = BigMed::MyContent->select(
        \%search_terms,
        { 'offset' => 15, 'limit' =>15 }
    );
    
    #this matches the next 31-45
    $selection = BigMed::MyContent->select(
        \%search_terms,
        { 'offset' => 30, 'limit' =>15 }
    );

=back

=back

=head2 Conducting Follow-Up Searches on a Found Set

After you have created a selection object with the C<select>
method, you can use that object to do additional searches within
the found set. These secondary searches are faster -- sometimes
I<much> faster -- than doing new searches. If you plan to
do a lot of work within a set of objects, doing secondary
searches against a selection object is the way to go.

To do this, you still use the C<select> method but as a method
of the selection object instead of a static class method.

    #create the selection object by loading
    #all objects for a specific site
    my $all_for_this_site = BigMed::MyContent->select(
        { 'site' => $site_id }
    );
    
    #get a secondary selection object for matches
    #against specific sections within the site
    my $my_sections = $all_for_this_site->select(
        { 'section' => [$section1, $section2] }
    )
    
    #and another object for matches against different
    #sections
    my $others = $all_for_this_site->select(
        { 'section' => [$section3, $section4] }
    )

If you plan to do many (thousands) of searches against a selection
object, you can optimize the search with the C<tune> method,
which indexes the selection against a single column for simple
scalar/exact matches. This is used, for example, by BigMed::Content
to look up pointed relationship objects quickly:

    my $pointers = BigMed::Pointer->select( { site=> $site_id } );
    $pointers->tune('source_id');

Selections can be tuned only for one column; tuning speeds any
search that includes that column for a scalar/exact match.

=head2 Fetching the Actual Objects

The C<select> method only collects matches into the driver selection
object. It does not actually return the matching objects themselves.
The selection object offers three methods to retrieve the objects.

The C<fetch> method allows you to retrieve either all of the objects
in the selection at once, or just the first object in the selection.
You can also use the C<fetch> method without a selection
object to collect and retrieve data objects directly in a single
statement.

The C<next> and C<previous> methods allow you to step through each
object in a selection set, loading each object one at a time. This
is particularly useful for dealing with large selection sets.

=head2 Loading All of the Objects in the Selection

=over 0

=item * C<fetch>

As an object method for the selection object, C<fetch> returns the
objects in the selection's found set:

    my @objects = $selection->fetch();

In a scalar context, only the first object in the set is returned:

    my $object = $selection->fetch();

C<fetch> can also be used without a selection object. See the
L</"Fetching Without a Selection Object"> section below.

=back

=head3 Empty Search Sets and Error Handling

If there are no matching results, C<fetch> will return an empty
array (in an array context) or an empty string (in a scalar
context). Be careful here, though; if C<fetch> encounters an
error, it will also return an empty array (in an array context) or
undef (in a scalar context) and stow the error into Big Medium's
singleton error object. Here's how to detect the difference
between an error and an empty set:

    #stop and display an error if there is one,
    #otherwise continue and handle the case of no matches
    @objects = $selection->fetch()
        or ($selection->error && $selection->error_stop);
    if (@objects == 0) {
        # ... no matching objects ...
    }
    
    #for scalar context
    defined ( my $object = $selection->fetch() )
        or $selection->error_stop;
    unless ($object) {
        # ... no matching object ...
    }

...or, depending on how/where you're using the C<fetch> method, you
may prefer to return an undef value from your current routine
and let the calling routine handle the error:

    #return undef if there's an error
    @objects = $selection->fetch()
        or ($selection->error && return undef);
    if (@objects == 0) {
        # ... no matching objects ...
    }
    
    #for scalar context
    defined ( my $object = $selection->fetch() )
        or return undef;
    unless ($object) {
        # ... no matching object ...
    }

=head3 Search Terms and Arguments with C<fetch>

The C<fetch> method optionally accepts the same arguments as the
C<select> method -- two hash references respectively containing
search terms and search arguments:

    my @objects = $selection->fetch(
        \%search_terms,
        \%arguments
    );

As with the C<select> method, these arguments tell the C<fetch> method
to find and sort matches within the selection object's existing set.
The matching objects are returned.

Unlike the C<select> method, however, the resulting selection is not
saved in a driver selection object and so the search  is not available
for later sub-searches. If you will need to search
within this found set later, it's faster to use select and then fetch
the results:

    my $sub_search = $selection->select(
        \%search_terms,
        \%arguments
    );
    my @objects = $sub_search->fetch()
        or ($selection->error && return undef);
    
    # ...do something with these objects...
    # ...and then do another search...
    
    $sub_search = $sub_search->select(
        \%more_search_terms,
        \%more_arguments
    );
    @objects = $sub_search->fetch()
        or ($selection->error && return undef);

=head3 Fetching Without a Selection Object

The C<fetch> method can also be used as a static class method
to retrieve objects without first creating a selection object.
Use the search_terms and arguments parameters to describe what
objects you would like to retrieve from the data store.

    defined (
        my $object = BigMed::MyContent->fetch(
            {id => $id}, {limit => 1}
        )
    ) or BigMed::MyContent->error_stop;

This is particularly handy for one-time searches where you know
you will not need any follow-up sub-searches on the found set.
(If you're dealing with large sets of objects, however, you may
want to use the C<select> method anyway, for more efficient use
of memory; see the C<next> and C<previous> methods below).

For the search described above (matching a single id), there's
a shortcut notation that accepts a single scalar parameter, the
numeric id of the object to load:

    defined ( my $object = BigMed::MyContent->fetch($id) )
        or BigMed::MyContent->error_stop;
    print 'no such record' if !$object;

=head2 C<next> and C<previous>: Step through the Set, One Object at a
Time

The C<fetch> routine loads all objects into memory at once, which can
be overly demanding on memory resources for very large sets of data.
To avoid that, the C<next> and C<previous> methods allow you to
follow an iterator pattern and step through a selection set and load
each object one at a time.

With each call of these iterator methods, the selection object keeps
track of your location in the set. So the next call to C<next> or
C<previous> will return the object relative to the current object.

When you have reached the end of the selection and no next/previous
object is available, an empty string will be returned. As with the
C<fetch> method, it's necessary to be careful with error checking
here, since the return value will be undef in the case of an error.

The examples for both methods show how to handle error checking
when using C<next> and C<previous> to step through an iterator loop.

=over

=item * C<next>

Returns the next object in the selection, relative to the last
object retrieved from C<next> or C<previous>. If C<next> or
C<previous> have not yet been called, the first object in the
selection is returned.

    #get the selection
    my $selection = BigMed::MyContent->select(
        \%search_terms,
        \%arguments
    );
    
    #step through each object in the selection
    my $object;
    while ($object = $selection->next) {
        # ...do something with the object...
        print $object->id, ": ", $object->name, "\n";
    }
    
    #handle error if applicable
    defined $object or $selection->error_stop;

=item * C<previous>

Returns the previous object in the selection, relative to the last
object retrieved from C<next> or C<previous>.

The internal index for the selection object always starts at the
beginning, so if C<previous> is called on a fresh selection object,
no object will be returned since the index is set to the beginning
of the list, and there is no previous object. (See the
L</"Setting the Next/Previous Index"> section below).

    #get the selection
    my $selection = BigMed::MyContent->select(
        \%search_terms,
        \%arguments
    );
    
    #set the internal index to the end of the selection
    $selection->set_index( $selection->count );

    #step backwards through each object in the selection
    my $object;
    while ($object = $selection->previous) {
        # ...do something with the object...
        print $object->id, ": ", $object->name, "\n";
    }
    
    #handle error if applicable
    defined $object or $selection->error_stop;

=back

=head3 Setting the Next/Previous Index

The selection object's internal next/previous index updates each
time that you call the C<next> or C<previous> method and keeps
track of your current location in the index. It's sometimes
useful to reset the index value to a new value (to set the index
at the end of the set, for example, in order to step backwards
through the list via the C<previous> method, or to reset the index
to the beginning of the selection in order to step through the list
all over again).

=over

=item * C<set_index>

Use the C<set_index> method on the selection object to set the index
to a specific numeric value:

    $selection->set_index(0); #set index to start of list

The next/previous index treats the selection set as a zero-based
list. Where the index is n, C<next> returns the nth object in this
zero-based list, and C<previous> returns the (n-1)th object.

For example, when the index is 0, C<next> returns
the first object in the zero-based list, and C<previous> returns
nothing because there is no previous object. When the index is 1,
C<next> returns the second object, and C<previous> returns the
first.

To set the index to the start of the list, set it to zero:

    $selection->set_index(0);

To set the index to the end of the list, set it to the size of the
list (use the C<count> method to get the size):

    $selection->set_index( $selection->count );

=back

=head2 Deleting an Object

=over

=item * C<trash>

The C<trash> method may be used on an individual data object to
remove the object from the data store. (The in-memory data values
of the data object remain unchanged; only the disk data is removed).

    $data->trash or $data->error_stop;

The C<trash> method returns a true value if it's successful,
or an undef value if there was an error (the error message
is added to the singleton error handler).

=item * C<trash_all>

The C<trash_all> method may be used on a selection object to
remove all of the objects in the selection from the data store.

    $selection->trash_all or $data->error_stop;

The C<trash_all> method returns a true value if it's successful,
or an undef value if there was an error (the error message
is added to the singleton error handler).

=item * C<clear>

The C<clear> method clears only the in-memory values of an
object without removing or otherwise altering the values in the
data store. It's essentially the same as replacing the existing
object with a new object for the approiate BigMed::Data subclass.

    $data->clear;

The return value of the C<clear> method is the object itself.

=back

=head2 "Join" Methods

Join is used in quotes here because they don't deliver a SQL-style join
but rather a way to select a set of objects in one data class
based on search/sort criteria for associated objects.
For example, you can use C<join_points_to> to retrieve a list
of pages sorted by author name (a many-to-many relationship), or
C<join_has> to retrieve a list of the 10 pages with the most recent
comments (a has/belongs-to relationship).

    #get pages for authors whose last name starts with A, sorted
    #alphabetically by last name
    my $author_pages = BigMed::Content::Page->join_points_to(
        { join => 'BigMed::Person', relation => 'author', site => $site_id },
        { last_name => {from => 'a', to => 'azz' } },
        { sort => 'last_name', order => 'ascend' }
    );
    
    #get the five pages that have been commented on most recently
    my $latest = BigMed::Content::Page->join_has(
        { join => 'BigMed::Comment', key => 'page', unique => 1, limit => 5 },
        { site => $site_id },
        { sort => 'create_time', order => 'descend' }
    );

Unlike a normal SQL join, however, you cannot mix and match object
types.  All of the returned objects will always be one object type
(the same data class you used to request the join method). So, in the
two examples above, the requests will return selections of
BigMed::Content::Page objects. Likewise, the search/sort for associated
objects can apply to just one data class at a time (BigMed::Person
in the first example and BigMed::Comment in the second).

=head3 The methods

The specific parameter syntax is a bit further below, but for now here's
a round-up of the three join methods:

=over 4

=item * join_has

Returns objects whose related objects are part of a has/belongs-to
relationship and have a foreign-key column specifying the ID of
the object to which they are related.

In the context of BigMed::Content objects, these are objects related
to primary objects in "has" relationships.

Returns undef (and adds an error message to the error queue) if there
is an i/o error along the way.

The previously-mentioned comments example is an example of a has relationship:

    #get the five pages that have been commented on most recently
    my $latest = BigMed::Content::Page->join_has(
        { join => 'BigMed::Comment', key => 'page', unique => 1, limit => 5 },
        { site => $site_id },
        { sort => 'post_time', order => 'descend' }
    ) or $app->error_stop;

=item * join_points_to

Returns objects with a "points to" relationship when dealing with has-many or
belongs-to-many relationships. Content pages, for example, can point to
several authors which can in turn be associated with other content pages.
To find all pages by a single author:

    my $pages_by_josh = BigMed::Content::Page->join_points_to(
        { join => 'BigMed::Person', relation => 'author', site => $site_id },
        { last_name => 'Clark', first_name => 'Josh' }
    ) or $app->error_stop;

Returns undef (and adds an error message to the error queue) if there
is an i/o error along the way.

While join_points_to is definitely faster than fetching all of the pointer
objects and stepping through them to assemble matches one by one, the
drawback is that it does not expose to you the pointer link object
itself. If you need the metadata stored in the individual pointers, you'll
have to go about it the slower way.

=item * join_pointed_from

Searches in the reverse direction from join_points_to. So you can find
all of the authors of pages published in the last 24 hours like so:

    my $time = BigMed->bigmed_time();
    $time->shift_time('-1d');
    my $bmtime    = $time->bigmed_time;
    my $authors = BigMed::Person->join_pointed_from(
        { join => 'BigMed::Content::Page', relation => 'author' },
        { site => $site_id, mod_time => { from => $bmtime } },
    ) or $app->error_stop;

Returns undef (and adds an error message to the error queue) if there
is an i/o error along the way.

The same caveat applies here as described above with join_points_to:
the pointer object links are not exposed to you with join_pointed_from,
so if you need that data, you'll have to go about it a slower way.

=back

=head3 Join syntax

The join methods may be called either as class methods or as object methods.
If called as an object method on a driver selection object, the returned
objects will be limited to those already part of that selection object.

The syntax for the three join methods is largely the same except for the
method name itself. The methods accept three arguments:

=over 4

=item * \%join_args

A hash reference of parameters describing how you would like the join
to be performed. The keys are:

=over 4

=item * join

    join => $selection
    join => 'BigMed::Content::Page'

The required join parameter specifies the class name or object selection
that you would like to use to search for related objects. (The join methods
will search within these related objects and then return the "primary"
objects related to the found objects).

If the join parameter is a class name, the related-object search will take
place within the entire set of objects for that class. If the join parameter
is a driver selection object, the related-object search will be limited to
objects within that selection (which typically offers a performance boost).

=item * key

This setting applies only to the C<join_has> method, which requires it.
Specifies the foreign-key column in the related-object class that indicates
the primary object with which it is associated.

=item * pselect

This optional parameter applies only to the C<join_points_to> and
C<join_points_from> and specifies a driver selection object already
loaded with the set of pointer objects to use. This can give significant
performance gains if you're going to do multiple joins across the same
pointer selection. For example, if you're going to do join_points_to searches
to find the articles associated with each BigMed::Tag object in the system,
it makes sense to collect all of the pointers first and then use that
selection object in this pselect parameter.

=item * unique

If set to a true value, the join methods will not return the same object
more than once. (In our comments example above, the same page could have
all of the most recent comments; setting the unique flag guarantees that
you will receive five different pages).

=item * site

The site in which you're searching. As an alternative, you can instead specify
this parameter in the search_terms argument (if you do both, the join
argument's site parameter is used).

=item * limit

This optional setting specifies the maximum number of objects the method
should return. If set to any false value, the methods will return the full
set of matching objects.

=item * relation

This optional setting applies only to the C<join_points_to> and
C<join_pointed_from> methods. If specified, it limits the search only
to objects of the specified relation type.

=back

=item * \%search_terms

The search terms to be used in finding the related objects. This uses
the same parameter format as the search terms argument in the C<fetch> and
C<select> methods.

=item * \%search_args

The search arguments to be used in sorting/retrieving the related objects.
This uses the same parameter format as the search argument hash in the
C<fetch> and C<select> methods.

=back

=head3 Join results : "Strong" and "weak" selection objects

All of the join methods return a driver selection object that allows
you to fetch objects. When the join methods are called as object
methods of a selection object from a previous search, this returned
object is just like the select object that you would get back from
a regular C<select> request.

But beware: When calling a join method as a <I>class method</I> instead of
an object method, the returned selection object is a little dumb. This "weak"
selection object lacks much of the self-knowledge that a normal "strong"
C<select> selection object has, and it's useless for doing additional searches
via C<select> (and, in some contexts, via C<join_has>). Calling C<select>
on a weak selection object will almost always return an empty selection
object.

    #create a regular selection object composed of all content pages
    my $selection = BigMed::Content::Page->select( { site => $site_id } );
    
    #get pages published by Jill Smith
    #because we're calling join_points_to on a selection object, we
    #get a "strong" selection object back
    my $smith_pages = $selection->join_points_to(
        { join => 'BigMed::Person', relation => 'author', site => $site_id },
        { last_name => 'Smith', first_name => 'Jill' }
    );
    
    #we can search and sort on this strong object:
    #gather all of John smith's pages published today, most recent first
    my $smith_today = $smith_pages->select(
        { site => $site_id, pub_time => {from => $today_at_midnight } },
        { sort => 'pub_time', order => 'descend' }
    );
    
    #but if we gather author pages via the class method, we get a weak
    #object back:
    my $jones_pages = BigMed::Content::Page->join_points_to(
        { join => 'BigMed::Person', relation => 'author', site => $site_id },
        { last_name => 'Jones', first_name => 'Jane' }
    );
    
    #we can use this selection object to fetch our page objects.
    #but it's useless for searching or sorting via the select method.
    #this will always return an empty selection even if matches exist
    #in the system:
    my $empty_object = $jones_pages->select(
        { site => $site, pub_time => {from => $today_at_midnight } },
    );

Apart from this C<select> limitation, though, even a "weak" selection object
is at full capacity for the rest of the usual selection methods: C<fetch>,
C<next>, C<previous>, C<index>, C<count>, C<join_points_to>,
C<join_pointed_from> and, in some contexts, C<join_has>.

Here's the scoop on C<join_has>: A weak selection object may be used
as part of a C<join_has> request but only when it's
the object on which you're calling the object method. However, using
a selection object as the join parameter in a C<join_has> request
will typically return an empty set. (No problem doing this with
C<join_points_to> and C<join_pointed_from> requests, however).

For example:
    
    #this returns a weak join selection object:
    $first_select = BigMed::Content::Page->join_points_to(
        { join => 'BigMed::Person', relation => 'author', site => $site_id },
        { last_name => { from => 'a', to => 'azz' } },
        { sort      => 'last_name', order   => 'ascend' }
    ) or $app->error_stop;

    #it works fine to use a join selection object to call the method
    $new_select = $first_select->join_has(
        {   join => 'BigMed::Comment',
            key  => 'page',
            site => $site_id,
        }
    ) or $app->error_stop;

    #this does not! returns an empty selection
    $new_select = BigMed::Content::Page->join_has(
        {   join => $first_select,    # BAD!
            key  => 'page',
            site => $site_id,
        }
    ) or $app->error_stop;

=head2 The Stash

Every data object has a "stash," a place to store values not
included in the data class' element set. The stash is basically
just a handy place to stow temporary values associated with an
individual object. Stash elements are not saved to disk by the data
object's C<save> method and should be used only for in-memory uses.

To store values in a data object's stash, use the C<set_stash>
method. To retrieve values, use the C<stash> method.

The stash methods may be used on C<select> objects as well as individual
BigMed::Data objects.

=over

=item * C<set_stash>

The C<set_stash> method accepts a hash of values to set, with
the key being the name of the value and the value being, well,
the value.

    $data->set_stash('value_name' => 'thevalue');

or...

    $data->set_stash(
        'value_name' => 'thevalue',
        'another_name' => 'second_value'
        'one_more_name' => 'third_value'
    );

=item * C<stash>

The C<stash> method accepts one or more names of values stored
in the stash and returns those values. In an array context,
C<stash> returns the array of values. In a scalar context,
C<stash> returns only the value for the name of the first
argument.

    @value = $data->stash('stash_1', 'stash_2', 'stash_3');
    $value = $data->stash('stash1');

=back

=head2 Checking Column Uniqueness

=over

=item * $data->is_unique('column_name', $selection_obj)

Returns some true value if no other data object shares the same value in the
column name specified in the argument. Returns 0 if another object
has the same value, or undef if an I/O error was encountered along the
way.

    defined ( my $unique = $data->is_unique('slug') )
      or $data->error_stop;    # i/o error
    print "Not a unique slug value.\n" if !$unique;

A selection object may be provided as the optional second argument. If so,
That selection object will be used to check uniqueness, instead of searching
from disk. This helps performance if you're using is_unique against many
objects and/or columns. For example:

    my $preload_selection = BigMed::Page({site=>$site_id});
    my $duplicate = '';
    foreach qw(slug title pub_time) {
        defined (
          my $unique = $data->is_unique($_, $preload_selection)
        ) or $data->error_stop;    #i/o error
        next if $unique;
        $duplicate = '';
        last;
    }
    print "Found duplicate in: ", $duplicate, "\n";

In site-specific objects (e.g., BigMed::Page, BigMed::Section, etc),
uniqueness is checked only within the specific site. Objects in other 
sites may have the same column value.

=back

=head1 Callbacks

There are several hooks available for inserting your own callback methods.
You may register these callbacks for BigMed::Data or any subclass. The
callbacks are inheritable by any subclasses of the class for which you
register the method. You may also add callbacks to individual BigMed::Data
objects. (See the BigMed::Trigger documentation for details on the order
and handling of inherited and multiple callbacks for the same hook).

=head2 Callback methods

=over 4

=item add_callback($hook_name, \&coderef)

    Foo->add_callback('before_save', \&foo_before_save);   #class method
    $foo->add_callback('before_save', \&foo_before_save);  #object-specific

This method may be called as either a class method or an object method.
Class methods will register the callback for all objects of that class
and all subclasses. Object methods will register ther callback for that
object only; when the object goes out of scope, the callback goes out
of scope, too.

The callback should return a defined value on success. An undefined return
value will be treated as an exception: no further callbacks will be
executed, and the "parent" method (e.g., C<save>, C<trash>, C<trash_all>)
will abort and return and undef value.

=back

=head2 The hooks

The available hooks for registering callbacks are:

=over 4

=item * after_copy

Callback routines receive three arguments:

=over 4

=item 1. The original data object.

=item 2. The copyied data object.

=item 3. The hash reference of parameter values passed to the C<copy>
method.

=back

=item * before_save

Callback routines receive the BigMed::Data object as the sole argument.

=item * after_save

Callback routines receive the BigMed::Data object as the sole argument.

=item * before_trash

Callback routines receive the BigMed::Data object as the sole argument.

=item * after_trash

Callback routines receive the BigMed::Data object as the sole argument.

=item * before_trash_all

Callback routines receive the select object as the sole argument.
Calls to trash_all do not invoke the before_trash and after_trash
callbacks. If you want something to happen with trash_all, you must
add a separate before_trash_all callback.

=item * after_trash_all

Ditto on the notes re before_trash_all.

=item * before_reindex

Called just before a data table is updated via the reindex method. For
site-specific data classes, the trigger is called before each site
is updated. For systemwide data classes, it's called just once before
the entire class is updated.

The calback receives three arguments: The data class, the site object
(if systemwide, this argument is undefined), and a reference to the hash
of columns and values to set during reindexing, if any.

=back

=head1 Utility Methods

These methods perform data maintenance tasks or retrieve information about
the composition and nitty-gritty details of the subclass objects and classes.

=over 4

=item * C<$class->reindex( { colname => $value } )>

Rebuilds the data index for the selected class; only necessary if you have
changed the order of indexed columns in the data class. The
optional argument is a hash reference that allows you to set a column value
for all objects along the way (if you're adding a new column, for example,
and want all objects to have a certain value in that column).

Returns true on success or undefined if there was an error (error message
is placed in the error queue).

=item * C<$obj->has_valid_id>

Returns true if the object has an id that has been assigned, either by
saving or requesting C<update_id> (even for unsaved objects). This
effectively asks whether the driver has seen this ID before for this
data source.

If not true, the object's ID is undefined and an untrue value is returned.

=item * C<$obj->is_modified>

Returns thrue if the object has been modified since it was created
(if loaded via C<new>) or since it was fetched (if loaded via C<fetch>,
C<next> or C<previous>). This determines, for example, if the modified
time should be updated when the object is saved.

Items are marked as modified when a set_
method is called and the value is different than the previous value.
For now at least, this comparison is super-simple and works only for
scalar values, not array/hash references. Setting an object attribute to
an array or hash reference will mark the object as modified even if
all of the contained values are the same as the original value.

You can also set and clear the modified flag manually via the C<mark_modified>
and C<mark_unmodified> methods.

=item * C<$obj->mark_unmodified>

Turns off the object's modified flag so that the C<is_modified> method
returns false when requested.

=item * C<$obj->mark_modified>

Turns on the object's modified flag so that the C<is_modified> method
returns true when requested.

=item * C<data_label>

Returns the subclass's label name (the name used in the Big Medium
interface for the subclass's data objects). This method
can be used either as an object method or a static class method:

    $label = BigMed::MyContent->data_label;

or...

    $label = $data->data_label;

=item * data_columns

Returns an array of the data column names for this subclass. This
method can be used either as an object method or a static class
method:

    @columns = BigMed::MyContent->data_columns;

or...

    @columns = $data->data_columns;

=item * C<data_source>

Returns the name of the data source for this subclass. This method
can be used either as an object method or a static class method:

    $data_source = BigMed::MyContent->data_source;

or...

    $data_source = $data->data_source;

=item * C<class_for_label($label)>

Returns the class name associated with the label in the argument.

=item * C<properties>

Returns a hash describing the properties of all of the data columns
for this subclass. The keys are the column names, and the values
are hash references to the metadata info for the column. The metadata
names and values are described above in the
L</"Column Metadata"> section, with the addition of the must_unpack
method element (see below).

This method can be used either as aon object method or a static
class method:

    %properties = BigMed::MyContent->properties;

or...

    %properties = $data->properties;

For example, the individual properties of a column would be accessed
like so:

    #look up the type and index properties of the 'name' column:
    $type = $properties{name}->{type};
    $index = $properties{name}->{index};

In addition to the column metadata described earlier, the column
hash reference also includes a must_unpack index which can be checked
to determine whether the value must be unpacked (marshaled) by
BigMed::Element when reading from the data store (primarily useful
to BigMed::Driver subclasses).

=item * C<index_columns>

Returns an array of hash references representing the metadata for
each of the indexed columns for this subclass. The hash references
consist of the same key/value pairs described above in the
L</"Column Metadata"> section.

This method can be used either as aon object method or a static
class method:

    @index_cols = BigMed::MyContent->index_columns;

or...

    @index_cols = $data->index_columns;

For example, the individual properties of the individual indexed
columns would be accessed like so:

    #get the name and type of the first two indexed columns:
    $name1 = $index_cols[0]->{name};
    $type1 = $index_cols[0]->{type};
    
    $name2 = $index_cols[1]->{name};
    $type2 = $index_cols[2]->{type};

=item * C<unique_indices>

Returns an array of index values corresponding to columns in the
C<index_columns> array that have the unique flag turned on. So, to
get the properties of all unique columns:

    my @unique_indices = BigMed::MyContent->unique_indices;
    my @index_columns = BigMed::MyContent->index_columns;
    my @unique_columns = @index_metadata[@unique_indices];

=item * C<source_class>

When given a data-source name, this method returns the name of the
subclass that uses that data source. This method can be called as
a static class method via BigMed::Data or any BigMed::Data subclass:

    $class = BigMed::Data->source_class('datasource');
    $class = BigMed::MyContent->source_class('datasource');

...or an object method on any BigMed::Data subclass object:

    $class = $data->('datasource');

=item * C<systemwide>

Returns true value (1) if the calling class or object is a
BigMed::Data subclass that is systemwide (like sites or users)
and not specific to an individual site (like articles and other
content).

This method can be called as a static class method:

    if (BigMed::MyContent->systemwide) {
        print "BigMed::MyContent is a systemwide class";
    }

...or as an object method on the subclass object:

    if ($data->systemwide) {
        print ref $data " is a systemwide class";
    }

=back

=head1 Author & Copyrights

This module and all Big Medium modules are copyright Josh Clark
and Global Moxie. All rights reserved.

Use of this module and the Big Medium content
management system are governed by Global Moxie's software licenses
and may not be used outside of the terms and conditions outlined
there.

For more information, visit the Global Moxie website at
L<http://globalmoxie.com/>.

Big Medium and Global Moxie are service marks of Global Moxie
and Josh Clark. All rights reserved.

=cut

