The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

#############################################################################
## $Id: RepositoryObjectSet.pm 9934 2007-09-11 18:04:00Z spadkins $
#############################################################################

package App::SessionObject::RepositoryObjectSet;
$VERSION = (q$Revision: 9934 $ =~ /(\d[\d\.]*)/)[0];  # VERSION numbers generated by svn

use App;
use App::Repository;
use App::SessionObject;

@ISA = ( "App::SessionObject" );

use strict;

=head1 NAME

App::SessionObject::RepositoryObjectSet - A set of repository objects described by a set of query parameters

=head1 SYNOPSIS

    use App::SessionObject::RepositoryObjectSet;

    ...

=cut

=head1 DESCRIPTION

A RepositoryObjectSet is a set of repository objects (i.e. rows in 
a database).

By using a RepositoryObjectSet instead of simply doing a query, you get
a variety of benefits.

 * session-level caching
 * find domains of given columns (get_column_values())
 * create unique and non-unique indexes of the object set based on
   groups of columns (get_index(), get_unique_index())
 * efficiently fetch single objects within the set or subsets of objects
   which share common values in a set of attributes

=cut

###########################################################################
# Support Routines
###########################################################################

sub _init {
    &App::sub_entry if ($App::trace);
    my ($self, $args) = @_;
    $self->SUPER::_init();
    my $table   = $self->{table} || die "table not defined";
    $self->_clear_cache_if_auto_params_changed() if ($self->{auto_params});   # sets params from auto_params
    $self->_clear_cache_if_auto_columns_changed() if ($self->{auto_columns}); # sets columns from auto_columns
    if (!$self->{columns} && !$self->{temporary}) {
        my $context = $self->{context};
        my $repname = $self->{repository};
        my $rep     = $context->repository($repname);
        $self->{columns} = $rep->_get_default_columns($table);
    }
    &App::sub_exit() if ($App::trace);
}

# This should only be relevant for temporary
sub set_objects {
    &App::sub_entry if ($App::trace);
    my ($self, $objects, $columns) = @_;
    if ($self->{temporary}) {
        $self->{objects} = $objects;
        delete $self->{index};
        delete $self->{unique_index};
        delete $self->{column_values};
        delete $self->{max_age_time};
        delete $self->{ext_summary};
        delete $self->{summary};
        if ($columns) {
            $self->{columns} = $columns;
        }
        elsif (!$self->{columns} && $#$objects > -1) {
            $columns = [ sort keys %{$objects->[0]} ];
            $self->{columns} = $columns;
        }
    }
    else {
        die "set_objects() is not allowed on a non-temporary object set";
    }
    &App::sub_exit() if ($App::trace);
}

sub _clear_cache {
    &App::sub_entry if ($App::trace);
    my ($self) = @_;
    if (!$self->{temporary}) {
        delete $self->{objects};
        delete $self->{index};
        delete $self->{unique_index};
        delete $self->{column_values};
        delete $self->{max_age_time};
        delete $self->{ext_summary};
        delete $self->{summary};
    }
    &App::sub_exit() if ($App::trace);
}

sub _clear_cache_if_objects_expired {
    &App::sub_entry if ($App::trace);
    my ($self, $options) = @_;
    my $max_age = $options->{max_age};
    $max_age = $self->{max_age} if (!defined $max_age);
    if (defined $max_age && $self->{objects}) {
        my $max_age_time = $self->{max_age_time};
        my $time = time();
        if (defined $max_age_time && $max_age_time <= $time - $max_age) {
            $self->_clear_cache();
        }
    }
    &App::sub_exit() if ($App::trace);
}

sub _clear_cache_if_auto_params_changed {
    &App::sub_entry if ($App::trace);
    my ($self, $options) = @_;
    if (defined $self->{auto_params}) {
        my $newparams = $self->substitute($self->{auto_params});
        if (!$self->{params}) {
            $self->{params} = $newparams;
        }
        else {
            my $changed = 0;
            my $params = $self->{params};
            foreach my $var (keys %$newparams) {
                if ($params->{$var} ne $newparams->{$var}) {
                    $changed = 1;
                    last;
                }
            }
            if ($changed) {
                $self->{params} = $newparams;
                $self->_clear_cache();
            }
        }
    }
    &App::sub_exit() if ($App::trace);
}

sub _clear_cache_if_auto_columns_changed {
    &App::sub_entry if ($App::trace);
    my ($self, $options) = @_;
    if (defined $self->{auto_columns}) {
        my (@auto_columns, %column, $new_columns);
        my $context = $self->{context};
        foreach my $wname (split(/,/, $self->{auto_columns})) {
            $new_columns = $context->so_get($wname);
            if ($new_columns) {
                push(@auto_columns, split(/,/, $new_columns));
            }
        }
        if (!$self->{columns}) {
            $self->{columns} = \@auto_columns;
            $self->_clear_cache();
        }
    }
    &App::sub_exit() if ($App::trace);
}

sub set_params {
    &App::sub_entry if ($App::trace);
    my ($self, $params) = @_;
    $params ||= {};
    my $redefined = 0;
    my $self_params = $self->{params};
    if (!$self_params) {
        $redefined = 1;
    }
    else {
        my (%param_already_defined, $key);
        foreach $key (keys %$self_params) {
            $param_already_defined{$key} = 1;
            if ((! exists $params->{$key}) ||
                (! defined $params->{$key} && defined $self_params->{$key}) ||
                (defined $params->{$key} && ! defined $self_params->{$key}) ||
                (defined $params->{$key} && defined $self_params->{$key} && $params->{$key} ne $self_params->{$key})) {
                $redefined = 1;
            }
        }
        if (!$redefined) {
            foreach $key (keys %$params) {
                if (! exists $self_params->{$key}) {
                    $redefined = 1;
                }
            }
        }
    }
    if ($redefined) {
        $self->{params} = { %$params };
        $self->_clear_cache();
    }
    &App::sub_exit() if ($App::trace);
}

sub update_params {
    &App::sub_entry if ($App::trace);
    my ($self, $params) = @_;
    my $self_params = $self->{params};
    die "params must be set before update_params() is called" if (!$self_params);
    my $param_changed = 0;
    foreach my $key (keys %$self_params) {
        if (exists $params->{$key} &&
            $self_params->{$key} ne $params->{$key}) {
            $self_params->{$key} = $params->{$key};
            $param_changed = 1;
        }
    }
    if ($param_changed && $self->{objects}) {
        $self->_clear_cache();
    }
    &App::sub_exit() if ($App::trace);
}

sub _get_all_objects {
    &App::sub_entry if ($App::trace);
    my ($self) = @_;
    my $objects = $self->{objects};
    if (!$objects) {
        if ($self->{temporary}) {
            $objects = [];
        }
        else {
            my $context = $self->{context};
            my $repname = $self->{repository};
            my $rep     = $context->repository($repname);
            my $table   = $self->{table} || die "table not defined";
            my $params  = $self->{params} || {};
            my $columns = $self->{columns};
            # Make a copy of $params so that if $db->get_objects() changes them,
            # it does not affect the cacheing aspects of the object set.
            $params = {%$params};
            $objects = $rep->get_objects($table, $params, $columns, {extend_columns => 1});
            $self->{objects}      = $objects;
            $self->{max_age_time} = time();
        }
    }
    &App::sub_exit($objects) if ($App::trace);
    return($objects);
}

###########################################################################
# Column Control
###########################################################################

sub set_columns {
    &App::sub_entry if ($App::trace);
    my ($self, $new_columns) = @_;
    $self->{columns} = $new_columns;
    $self->_clear_cache();
    &App::sub_exit() if ($App::trace);
}

sub include_columns {
    &App::sub_entry if ($App::trace);
    my ($self, $new_columns) = @_;
    my $columns = $self->{columns};
    if (!$columns) {
        my $repname = $self->{repository};
        my $context = $self->{context};
        my $rep = $context->repository($repname);
        my $table = $self->{table} || die "table not defined on object_set [$self->{name}]";
        $columns = $rep->_get_default_columns($table);
        $self->{columns} = $columns;
    }
    my (%colidx, $column_added, $column);
    for (my $i = 0; $i <= $#$columns; $i++) {
        $colidx{$columns->[$i]} = $i;
    }
    for (my $i = 0; $i <= $#$new_columns; $i++) {
        $column = $new_columns->[$i];
        if (! defined $colidx{$column}) {
            push(@$columns, $column);
            $colidx{$column} = $#$columns;
            $column_added = 1;
        }
    }
    if ($column_added) {
        $self->_clear_cache();
    }
    &App::sub_exit() if ($App::trace);
}

sub get_repository {
    &App::sub_entry if ($App::trace);
    my ($self) = @_;
    my $context = $self->{context};
    my $repname = $self->{repository};
    my $rep     = $context->repository($repname);
    &App::sub_exit($rep) if ($App::trace);
    return($rep);
}

sub get_table {
    &App::sub_entry if ($App::trace);
    my ($self) = @_;
    my $table = $self->{table};
    &App::sub_exit($table) if ($App::trace);
    return($table);
}

sub get_columns {
    &App::sub_entry if ($App::trace);
    my ($self) = @_;
    my $columns = $self->{columns};
    &App::sub_exit($columns) if ($App::trace);
    return($columns);
}

sub get_key_columns {
    &App::sub_entry if ($App::trace);
    my ($self) = @_;
    my $repname     = $self->{repository};
    my $context     = $self->{context};
    my $rep         = $context->repository($repname);
    my $table       = $self->{table} || die "table not defined";
    my $table_def   = $rep->get_table_def($table);
    my $column_defs = $table_def->{column};
    my $columns     = $self->{columns};
    if (!$columns) {
        $columns = $rep->_get_default_columns($table);
        $self->{columns} = $columns;
    }
    my (@key_columns, $column);
    for (my $i = 0; $i <= $#$columns; $i++) {
        $column = $columns->[$i];
        if ($column_defs->{$column}{is_key}) {
            push(@key_columns, $column);
        }
    }
    &App::sub_exit(\@key_columns) if ($App::trace);
    return(\@key_columns);
}

sub get_column_defs {
    &App::sub_entry if ($App::trace);
    my ($self) = @_;
    my $context     = $self->{context};
    my $repname     = $self->{repository};
    my $rep         = $context->repository($repname);
    my $table       = $self->{table} || die "table not defined";
    my $table_def   = $rep->get_table_def($table);
    my $column_defs = $table_def->{column};
    &App::sub_exit($column_defs) if ($App::trace);
    return($column_defs);
}

###########################################################################
# Accessing individual objects
###########################################################################

sub get_index {
    &App::sub_entry if ($App::trace);
    my $self = shift;
    my $options = (ref($_[0]) eq "HASH") ? shift : {};
    my $key_name = ref($_[0]) ? "" : shift;
    my $key_columns = shift;
    $key_name = join(",", @$key_columns) if (!$key_name && ref($key_columns) eq "ARRAY");
    $key_name ||= "ie1";

    $self->_clear_cache_if_auto_params_changed($options) if (defined $self->{auto_params});
    $self->_clear_cache_if_objects_expired($options) if ((defined $options->{max_age} || defined $self->{max_age}) && $self->{objects});

    my $index = $self->{index}{$key_name};
    if (!$index) {
        if ($self->{key}{$key_name}) {
            $key_columns = $self->{key}{$key_name};
        }
        die "no list of columns given or known for key [$key_name]" if (!$key_columns);
        my ($key);
        $index = {};
        my $objects = $self->_get_all_objects();
        foreach my $object (@$objects) {
            $key = join(",", @{$object}{@$key_columns});
            if ($index->{$key}) {
                push(@{$index->{$key}}, $object);
            }
            else {
                $index->{$key} = [ $object ];
            }
        }
        $self->{index}{$key_name} = $index;
    }
    &App::sub_exit($index) if ($App::trace);
    return($index);
}

# $self->get_unique_index($key_columns);
# $self->get_unique_index($key_name, $key_columns);
# $self->get_unique_index($key_name);
sub get_unique_index {
    &App::sub_entry if ($App::trace);
    my $self = shift;
    my $options = (ref($_[0]) eq "HASH") ? shift : {};
    my $key_name = ref($_[0]) ? "" : shift;
    my $key_columns = shift;
    $key_name = join(",", @$key_columns) if (!$key_name && ref($key_columns) eq "ARRAY");
    $key_name ||= "ak1";

    $self->_clear_cache_if_auto_params_changed($options) if (defined $self->{auto_params});
    $self->_clear_cache_if_objects_expired($options) if ((defined $options->{max_age} || defined $self->{max_age}) && $self->{objects});

    my $unique_index = $self->{unique_index}{$key_name};
    if (!$unique_index) {
        if ($self->{key}{$key_name}) {
            $key_columns = $self->{key}{$key_name};
        }
        die "no list of columns given or known for key [$key_name]" if (!$key_columns);
        my ($key);
        $unique_index = {};
        my $objects = $self->_get_all_objects();
        foreach my $object (@$objects) {
            $key = join(",", @{$object}{@$key_columns});
            $unique_index->{$key} = $object;
        }
        $self->{unique_index}{$key_name} = $unique_index;
    }
    &App::sub_exit($unique_index) if ($App::trace);
    return($unique_index);
}

sub get_summary {
    &App::sub_entry if ($App::trace);
    my ($self, $key_columns, $options) = @_;
    $key_columns = [] if (!$key_columns);
    my $key_name = join(",", @$key_columns);

    $self->_clear_cache_if_auto_params_changed($options) if (defined $self->{auto_params});
    $self->_clear_cache_if_objects_expired($options) if ((defined $options->{max_age} || defined $self->{max_age}) && $self->{objects});

    my $summary = $self->{summary}{$key_name};
    if (!$summary) {
        $summary = {};
        my $rep          = $self->get_repository();
        my $table        = $self->get_table();
        my $index        = $self->get_index($key_columns);
        my $columns      = $self->get_columns();
        my ($objects, $ext_summaries, %options, $summarized_rows);
        foreach my $key (keys %$index) {
            $objects = $index->{$key};
            $ext_summaries    = {};
            %options          = ( ext_summaries => $ext_summaries );
            $summarized_rows  = $rep->summarize_rows($table, $objects, $columns, $key_columns, \%options);
            $self->{ext_summary}{$key_name}{$key} = $ext_summaries;
            $summary->{$key} = $summarized_rows->[0];
        }
        $self->{summary}{$key_name} = $summary;
    }
    &App::sub_exit($summary) if ($App::trace);
    return($summary);
}

sub get_ext_summary {
    &App::sub_entry if ($App::trace);
    my ($self, $key_columns, $options) = @_;
    $key_columns = [] if (!$key_columns);
    my $key_name = join(",", @$key_columns);

    $self->_clear_cache_if_auto_params_changed($options) if (defined $self->{auto_params});
    $self->_clear_cache_if_objects_expired($options) if ((defined $options->{max_age} || defined $self->{max_age}) && $self->{objects});

    my $ext_summary = $self->{ext_summary}{$key_name};
    if (!$ext_summary) {
        $self->get_summary($key_columns);
        $ext_summary = $self->{ext_summary}{$key_name};
    }
    &App::sub_exit($ext_summary) if ($App::trace);
    return($ext_summary);
}

sub get_column_values {
    &App::sub_entry if ($App::trace);
    my ($self, $column, $options) = @_;

    $self->_clear_cache_if_auto_params_changed($options) if (defined $self->{auto_params});
    $self->_clear_cache_if_objects_expired($options) if ((defined $options->{max_age} || defined $self->{max_age}) && $self->{objects});

    my $values = $self->{column_values}{$column};
    if (!$values) {
        $values = [];
        my $objects = $self->_get_all_objects();
        my (%count, $value);
        foreach my $object (@$objects) {
            $value = $object->{$column};
            if (!$count{$value}) {
                $count{$value} = 1;
                push(@$values, $value);
            }
            else {
                $count{$value} ++;
            }
        }
        $self->{column_values}{$column} = $values;
    }
    &App::sub_exit($values) if ($App::trace);
    return($values);
}

# $self->get_object($options, $key, $key_columns);
# $self->get_object($key, $key_columns);
# $self->get_object($key, $key_name, $key_columns);
# $self->get_object($key, $key_name);
sub get_object {
    &App::sub_entry if ($App::trace);
    my $self = shift;
    my $options = (ref($_[0]) eq "HASH") ? shift : {};
    my $key = shift;
    my $key_name = ref($_[0]) ? "" : shift;
    my $key_columns = shift;
    $key_name = join(",", @$key_columns) if (!$key_name && ref($key_columns) eq "ARRAY");
    $key_name ||= "ak1";

    $self->_clear_cache_if_auto_params_changed($options) if (defined $self->{auto_params});
    $self->_clear_cache_if_objects_expired($options) if ((defined $options->{max_age} || defined $self->{max_age}) && $self->{objects});

    my $unique_index = $self->get_unique_index($key_name, $key_columns);
    my $object = $unique_index->{$key};
    &App::sub_exit($object) if ($App::trace);
    return($object);
}

# $self->get_objects($key, $options, $key_name);
# $self->get_objects($key, $key_columns);
# $self->get_objects($key, $key_name, $key_columns);
# $self->get_objects($key, $key_name);
sub get_objects {
    &App::sub_entry if ($App::trace);
    my $self = shift;
    my $options = (ref($_[0]) eq "HASH") ? shift : {};
    my $key = shift;
    die "key not scalar" if (ref($key));
    my $key_name = ref($_[0]) ? "" : shift;
    my $key_columns = shift;
    $key_name = join(",", @$key_columns) if (!$key_name && ref($key_columns) eq "ARRAY");
    $key_name ||= "ie1";

    $self->_clear_cache_if_auto_params_changed($options) if (defined $self->{auto_params});
    $self->_clear_cache_if_objects_expired($options) if ((defined $options->{max_age} || defined $self->{max_age}) && $self->{objects});

    my ($objects);
    if ($key) {
        my $index = $self->get_index($key_name, $key_columns);
        $objects = $index->{$key} || [];
    }
    else {
        $objects = $self->_get_all_objects();
    }
    &App::sub_exit($objects) if ($App::trace);
    return($objects);
}

=head1 ACKNOWLEDGEMENTS

 * Author:  Stephen Adkins <spadkins@gmail.com>
 * License: This is free software. It is licensed under the same terms as Perl itself.

=head1 SEE ALSO

L<C<App::Context>|App::Context>,
L<C<App::Repository>|App::Repository>

=cut

1;