/usr/local/CPAN/Games-Object/Games/Object/Manager.pm
package Games::Object::Manager;
use strict;
use Exporter;
use Carp qw(carp croak confess);
use IO::File;
use Games::Object::Common qw(FetchParams LoadData SaveData ANAME_MANAGER);
use vars qw($VERSION @EXPORT_OK %EXPORT_TAGS @ISA);
$VERSION = "0.11";
@ISA = qw(Exporter);
@EXPORT_OK = qw($CompareFunction REL_NO_CIRCLE);
%EXPORT_TAGS = (
flags => [ qw(REL_NO_CIRCLE) ],
variables => [ qw($CompareFunction) ],
);
use vars qw($CompareFunction);
# Define flags.
use constant REL_NO_CIRCLE => 0x00000001; # Don't allow cir. relates
# Define the comparison function to use for processing order.
$CompareFunction = '_CompareDefault';
# Define the default process info.
my @ProcessList = (
'process_queue',
'process_pmod',
'process_tend_to',
);
my $ProcessLimit = 100;
####
## INTERNAL FUNCTIONS
# Default comparison function when determining the order of processing of
# two objects.
sub _CompareDefault { $b->priority() <=> $a->priority() }
# Comparison function when using the creation order option
sub _CompareAddOrder {
my $cmp = $b->priority() <=> $a->priority();
$cmp == 0 ? $a->order() <=> $b->order() : $cmp;
}
# Create a relation methods
sub _CreateRelators
{
my %args = @_;
my $realname = $args{name};
my $name = $args{relate_method};
my $uname = $args{unrelate_method};
my $rname = $args{related_method};
my $iname = $args{is_related_method};
my $lname = $args{related_list_method};
no strict 'refs';
*$name = sub {
my $man = shift;
my $args = ( ref($_[$#_]) eq 'HASH' ? pop @_ : {} );
$man->relate(how => $realname,
self => $_[0],
object => $_[1],
other => $_[2],
args => $args);
} if (!defined(&$name));
*$uname = sub {
my $man = shift;
my $args = ( ref($_[$#_]) eq 'HASH' ? pop @_ : {} );
$man->unrelate(how => $realname,
object => $_[0],
other => $_[1],
args => $args);
} if (!defined(&$uname));
*$rname = sub {
my $man = shift;
$man->related(how => $realname, object => $_[0]);
} if (!defined(&$rname));
*$iname = sub {
my $man = shift;
$man->is_related(how => $realname, self => $_[0], object => $_[1]);
} if (!defined(&$iname));
*$lname = sub {
my $man = shift;
$man->related_list(how => $realname, self => $_[0]);
} if (!defined(&$lname));
}
####
## CONSTRUCTOR
# Basic constructor
sub new
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $man = {};
my %args = ();
# Fetch parameters.
FetchParams(\@_, \%args, [
[ 'opt', 'base_id', 0, 'int' ],
[ 'opt', 'process_list', \@ProcessList, 'arrayref' ],
[ 'opt', 'process_limit', $ProcessLimit, 'int' ],
] );
bless $man, $class;
# Define storage for created objects. Note that this means that objects
# will be persistent. They can go out of scope and still exist, since
# each is identified by a unique ID.
$man->{index} = {};
# Define tables that handle object relationships
$man->{relation_def} = {};
$man->{relate_to} = {};
$man->{relate_from} = {};
# Define a counter for creating objects when the user wants us to
# assume that every new object is unique. The starting number can be
# changed with base_id() but only if no objects have been created yet.
$man->{next} = $args{base_id};
# Define a counter that will be used to track the order in which objects
# are created. This is to support a new feature in v0.05
$man->{order} = 0;
# And if we are doing this, we want to try and use space efficiently by
# reclaiming unused IDs. Thus we track the lowest available opening.
# [ NOT YET IMPLEMENTED ]
$man->{reclaim} = 1;
$man->{avail} = 0;
# Track the highest priority object.
$man->{highest_pri} = 0;
# Define a table that shows what order process() is supposed to do
# things.
$man->{process_list} = $args{process_list};
# Define a limit to how many times the same item can be processed in a
# queue (see process_queue() for details)
$man->{process_limit} = $args{process_limit};
# Set the default inherit_from relationship.
$man->define_relation(
name => 'inherit',
relate_method => 'inherit',
unrelate_method => 'disinherit',
related_method => 'inheriting_from',
related_list_method => 'has_inherting',
is_related_method => 'is_inheriting_from',
flags => REL_NO_CIRCLE,
);
# Done.
$man;
}
# Constructor for loading entire container from a file.
sub load
{
my $proto = shift;
my $class = ref($proto) || $proto;
my $file = shift;
my $filename;
# If we got a filename instead of a file object, open the file.
if (!ref($file)) {
$filename = $file;
$file = IO::File->new();
$file->open("<$filename") or
croak "Unable to open manager file '$filename'";
}
# Initialize the object.
my $man;
if (ref($proto)) {
# This is a "load in place", meaning we're reloading to an
# existing object, so clear out the old stuff.
$man = $proto;
foreach my $key (keys %$man) { delete $man->{$key}; }
} else {
# Totally new object originating from the file.
$man = {};
bless $man, $class;
}
# Check the header to make sure this is manager data.
my $line = <$file>; chomp $line;
croak "Did not find manager header data in file"
if ($line ne 'OBJ:__MANAGER__');
$line = <$file>; chomp $line;
croak "Second line of manager data header bad"
if ($line !~ /^CL:(.+)$/);
my $mclass = $1;
# Load.
LoadData($file, $man);
$file->close() if defined($filename);
# Restore manager attributes to all objects.
foreach my $obj (values %{$man->{index}}) { $obj->manager($man); }
# Restore relators.
foreach my $rel (values %{$man->{relation_def}}) {
_CreateRelators(%$rel);
}
# Done.
bless $man, $mclass;
$man;
}
####
## MANAGER DATA METHODS
# Save the manager and its contents to a file.
sub save
{
my $man = shift;
my $file = shift;
my $filename;
# If we got a filename instead of a file object, open the file.
if (!ref($file)) {
$filename = $file;
$file = IO::File->new();
$file->open(">$filename") or
croak "Unable to open manager file '$filename'";
}
# Save header. This indicates that this is indeed manager object
# data and preserves the class.
print $file "OBJ:__MANAGER__\n" .
"CL:" . ref($man) . "\n";
# Save data. See the comments on the save() routine in Games::Object
# for why we copy the ref to an ordinary hash first.
my %hash = %$man;
SaveData($file, \%hash);
$file->close() if (defined($filename));
1;
}
# "Find" an object (i.e. look up its ID). If given something that is
# already an object, validates that the object is still valid. If the
# assertion flag is passed, an invalid object will result in a fatal error.
sub find
{
my ($man, $id, $assert) = @_;
if (!defined($id)) {
if ($assert) {
confess "Assertion failed: ID is undefined";
} else {
return undef;
}
}
$id = $id->id() if (ref($id) && UNIVERSAL::isa($id, 'Games::Object'));
if (defined($man->{index}{$id})) {
$man->{index}{$id};
} elsif ($assert) {
# Report with confess() so user can see where the assertion was made
confess "Assertion failed: '$id' is not a valid/managed object ID";
} else {
undef;
}
}
# Return the number of objects in the manager.
sub total_objects {
my $man = shift;
scalar keys %{$man->{index}};
}
# Returns the ID of an object, with the side effect that it validates that
# this object is really a Games::Object derivative and is being managed by
# this manager. The user specifies either the ID or the object ref. If valid,
# the ID is always returned (thus it can be used to guarantee the return of
# an ID when you're not sure if you were passed an object or the ID).
sub id
{
my ($man, $obj, $assert) = @_;
if (ref($obj) && UNIVERSAL::isa($obj, 'Games::Object')) {
my $id = $obj->id();
defined($man->{index}{$id}) ? $id : undef;
} elsif (defined($man->{index}{$obj})) {
$obj;
} elsif ($assert) {
# Report with confess() so user can see where the assertion was made
confess "Assertion failed: '$obj' is not a valid/managed object";
} else {
undef;
}
}
####
## OBJECT MANAGEMENT METHODS
# Add a new object to the manager. The user may either specify an ID (which
# must not already exist), or allow it to take a predefined ID from the object
# (if defined), or pick one on its own (if previous two undefined)
sub add
{
my ($man, $obj, $id) = @_;
# Pick new ID if needed.
$id = $obj->id() if (!defined($id));
$id = $man->{next}++ if (!defined($id));
# Make sure it does not exist.
croak "Attempt to add duplicate object ID '$id'"
if (defined($man->{index}{$id}));
# Add it. Do this before adding the manager link so we don't get
# a call back to us.
$man->{index}{$id} = $obj;
$obj->id($id);
# Add the manager attribute
$obj->manager($man);
# Done.
$id;
}
# Similar to add, but allows an object to already exist under this ID, in
# which case the old on is removed. Returns the same values as add(). The
# ID to replace is always taken from the existing object. The ID parameter
# is applied to the new object (thus it must not already exist).
sub replace
{
my ($man, $obj, $id) = @_;
# Get rid of the old object. Don't worry if the object does not
# already exist.
$man->remove($id);
# Add new one.
$man->add($obj, $id);
}
# Remove an object. Returns the object if the object was found and removed,
# undef if not. The on_removed action is invoked on the object (but before
# the object is actually removed so it can still access the manager linkage).
# User may specify additional args to be passed to the action() call.
sub remove
{
my $man = shift;
my $self = shift;
# If the last arg is a hash, this is additional args to any callback
# that might get invoked.
my $aargs = ( @_ && ref($_[$#_]) eq 'HASH' ? pop @_ : {} );
# Any remaining arg is other.
my $other = ( @_ ? shift : $self );
# If object does not exist, no need to go any further.
my $id = $man->id($self);
return undef if (!defined($man->{index}{$id}));
# Fetch the object and invoke action.
$self = $man->find($id);
$self->action(other => $other,
action => "object:remove",
args => $aargs);
# Break relationships TO this object. These are all done with the
# force option. This means that no tests will be done for each
# unrelate(), but post-unrelate() actions WILL occur.
my @hows = ();
@hows = keys %{$man->{relate_from}{$id}}
if (defined($man->{relate_from}{$id}));
foreach my $how (@hows) {
my @fobjs = @{$man->{relate_from}{$id}{$how}};
foreach my $fobj (@fobjs) {
$man->unrelate(
how => $how,
object => $fobj,
other => $other,
force => 1,
args => { source => 'remove:to', %$aargs },
);
}
}
# Break all relationships FROM this object to others.
@hows = ();
@hows = keys %{$man->{relate_to}{$id}}
if (defined($man->{relate_to}{$id}));
foreach my $how (@hows) {
my @objs = map { $man->find($_) } @{$man->{relate_from}{$id}{$how}};
foreach my $obj (@objs) {
$man->unrelate(
how => $how,
object => $obj,
other => $other,
force => 1,
args => { source => 'remove:from', %$aargs }
);
}
}
# Delete from internal tables, which should remove all references to
# it save the one we have.
delete $man->{index}{$id};
delete $man->{relate_to}{$id};
delete $man->{relate_from}{$id};
# Remove the manager attribute.
$self->manager(undef);
# Done.
$self;
}
# Go down the complete list of objects and perform a method call on each. If
# no args are given, 'process' is assumed. This will call them in order of
# priority.
#
# The caller may choose to filter the list by providing a CODE ref as the
# first argument. Only the objects for which the CODE ref returns true are
# considered (new in v0.10).
sub process
{
my $man = shift;
# Note that we grab the actual objects and not the ids in the sort.
# This is more efficient, as each object is simply a reference (a
# scalar with a fixed size) as opposed to a string (a scalar with
# a variable size).
my $method = shift;
my $code = ( ref($method) eq 'CODE' ? $method : undef );
$method = shift if ($code);
my @args = @_;
$method = 'process' if (!defined($method));
# Derive the object list.
my @objs = (
$code ?
grep { &$code($_, @args) }
grep { UNIVERSAL::can($_, $method) }
sort $CompareFunction values %{$man->{index}}
:
grep { UNIVERSAL::can($_, $method) }
sort $CompareFunction values %{$man->{index}}
);
# Process.
unshift @args, $man->{process_list} if ($method eq 'process');
foreach my $obj (@objs) {
$obj->$method(@args) if (UNIVERSAL::can($obj, $method));
}
# Return the number of objects processed.
scalar(@objs);
}
# Set/fetch the process list for the process() function. Note that the user is
# not limited to the methods found here. The methods can be in the subclass
# if desired. Note that we have no way to validate the method names here,
# so we take it on good faith that they exist.
sub process_list {
my $man = shift;
if (@_) { @{$man->{process_list}} = @_ } else { @{$man->{process_list}} }
}
####
## OBJECT RELATIONSHIP METHODS
# Check to see if a relationship is valid. If assertion flag present, this
# will bomb the program if the relationship is not present.
sub has_relation
{
my ($man, $how, $assert) = @_;
defined($man->{relation_def}{$how}) ? 1 :
$assert ? croak "'$how' is an invalid relationship type"
: 0;
}
# Define a new relationship. This allows objects to be related with the
# relate() method, or via a relator method created.
sub define_relation
{
my $man = shift;
my %args = ();
# Fetch parameters.
FetchParams(\@_, \%args, [
[ 'req', 'name', undef, 'string' ],
[ 'opt', 'relate_method', undef, 'string' ],
[ 'opt', 'unrelate_method', undef, 'string' ],
[ 'opt', 'related_method', undef, 'string' ],
[ 'opt', 'related_list_method', undef, 'string' ],
[ 'opt', 'is_related_method', undef, 'string' ],
[ 'opt', 'on_remove', undef, 'callback' ],
[ 'opt', 'flags', 0, 'int' ],
], 1 );
# Add it. Note that we allow redefinition at will.
my $rname = $args{name};
$args{relate_method} = $rname
if (!$args{relate_method});
$args{unrelate_method} = "un${rname}"
if (!$args{unrelate_method});
$args{related_method} = "${rname}_to"
if (!$args{related_method});
$args{related_list_method} = "${rname}_list"
if (!$args{related_list_method});
$args{is_related_method} = "is_${rname}"
if (!$args{is_related_method});
$man->{relation_def}{$rname} = \%args;
# Create relator.
_CreateRelators(%args);
# Done.
1;
}
# Relate two objects.
sub relate
{
my $man = shift;
my %args = ();
# Fetch parameters. Self is the thing being related to, object is
# the thing being related to it.
FetchParams(\@_, \%args, [
[ 'req', 'how', undef, sub { $man->has_relation(shift); } ],
[ 'req', 'self', undef, 'any' ],
[ 'req', 'object', undef, 'any' ],
[ 'opt', 'other', undef, 'any' ],
[ 'opt', 'force', 0, 'boolean' ],
[ 'opt', 'args', {}, 'hashref' ],
] );
my $how = $args{how};
my $self = $args{self};
my $object = $args{object};
my $other = $args{other};
my $force = $args{force};
my $aargs = $args{args};
# If other is undefined, then we set it equal to self, meaning we assume
# that the receipient of the object itself instigated the action.
$other = $self if (!defined($other));
# Do it. First fetch necesary parameters.
my $rel = $man->{relation_def}{$how};
my $doaction = "object:on_" . $rel->{relate_method};
my $tryaction = "object:try_" . $rel->{relate_method};
my $idself = $man->id($self); $self = $man->find($idself);
my $idobject = $man->id($object); $object = $man->find($idobject);
# Perform check to see if relationship is allowed. We do this
# before anything else, including attempting to unrelate it from
# whatever it may be currently related to. This way the relate
# check code can see how it is related now in case that means
# anything, plus it prevents orphaned objects (which would happen
# if we first unrelate()d it and then failed the relate() check).
my $check =
$force
||
$self->action(
action => $tryaction,
object => $object,
other => $other,
args => $aargs);
return 0 if (!$check);
# Relation is allowed, so check to see if already related.
if (defined($man->{relate_to}{$idobject}{$how})) {
# Already related in this fashion.
if ($man->{relate_to}{$idobject}{$how} eq $idself) {
# And to the same object, so do nothing (successfully).
return 1;
} elsif ($man->unrelate(
how => $how,
object => $object,
force => $force,
args => { source => 'relate', %$aargs } )) {
# The unrelate from the previous object succeeded, so
# invoke myself to try again.
return $man->relate(@_);
} else {
# The unrelate failed, so no-go.
return 0;
}
}
# Not currently related to anything in this way. The first
# thing we do is check the REL_NO_CIRCLE flag. If set,
# then we make a check to see if a circular reference would
# result from this. If so, then bomb, as this is assumed to
# be a logic error in the main program.
if ($rel->{flags} & REL_NO_CIRCLE) {
# Check to make sure no circular relationship would result from
# this (i.e. self is already related to object in this manner).
croak "Relating $idobject to $idself in manner $how would " .
"create a circular relationship"
if ($man->is_related(
object => $self,
self => $object,
how => $how,
distant => 1));
}
# Do it.
$man->{relate_to}{$idobject}{$how} = $idself;
$man->{relate_from}{$idself}{$how} = []
if (!defined($man->{relate_from}{$idself}{$how}));
push @{$man->{relate_from}{$idself}{$how}}, $idobject;
# Invoke post-relate actions.
$self->action(
object => $object,
other => $other,
action => $doaction,
args => $aargs,
);
# Done.
1;
}
# Return the object to which this one is related (if any)
sub related
{
my $man = shift;
my %args = ();
# Fetch parameters.
FetchParams(\@_, \%args, [
[ 'req', 'how', undef, sub { $man->has_relation(shift) } ],
[ 'req', 'object', undef, 'any' ],
] );
my $how = $args{how};
my $object = $args{object};
my $id = $man->id($object); $object = $man->find($id);
defined($man->{relate_to}{$id}) && # @*!&$ autovivication
defined($man->{relate_to}{$id}{$how}) ?
$man->find($man->{relate_to}{$id}{$how}) : undef;
}
# Return a list of items that are related to a paricular object in a certain
# way.
sub related_list
{
my $man = shift;
my %args = ();
# Fetch parameters.
FetchParams(\@_, \%args, [
[ 'req', 'how', undef, sub { $man->has_relation(shift) } ],
[ 'req', 'self', undef, 'any' ],
] );
my $how = $args{how};
my $self = $args{self};
# Return list of objects.
my $id = $man->id($self);
my @list = ();
@list = map { $man->find($_) } @{$man->{relate_from}{$id}{$how}}
if (defined($man->{relate_from}{$id})
&& defined($man->{relate_from}{$id}{$how}));
@list;
}
# Check to see if two objects are related. By default, this checks only if
# two objects are DIRECTLY related. However, specifying the "distant" flag
# will perform a recursive check to see if the relationship exists indirectly.
sub is_related
{
my $man = shift;
my %args = ();
# Fetch parameters.
FetchParams(\@_, \%args, [
[ 'req', 'how', undef, sub { $man->has_relation(shift); } ],
[ 'req', 'object', undef, 'any' ],
[ 'opt', 'self', undef, 'any' ],
[ 'opt', 'distant', 0, 'boolean' ],
] );
my $how = $args{how};
my $idobject = $man->id($args{object});
my $idself = $man->id($args{self});
my $distant = $args{distant};
return 0 if (!defined($idobject) || !defined($idself));
# If idobject is related to nothing then no relation.
return 0 if (!defined($man->{relate_to}{$idobject})
|| !defined($man->{relate_to}{$idobject}{$how}));
# If there is a direct relationships, success.
return 1 if ($man->{relate_to}{$idobject}{$how} eq $idself);
# If user did not want a distant relationship, then fail.
return 0 if (!$distant);
# Otherwise, check what idobject is related to and see if that is
# related to idself.
$man->is_related(
object => $man->{relate_to}{$idobject}{$how},
self => $idself,
how => $how,
distant => 1);
}
# Unrelate an object.
sub unrelate
{
my $man = shift;
my %args = ();
# Fetch parameters.
FetchParams(\@_, \%args, [
[ 'req', 'how', undef, sub { $man->has_relation(shift) } ],
[ 'req', 'object', undef, 'any' ],
[ 'opt', 'other', undef, 'any' ],
[ 'opt', 'args', {}, 'hashref' ],
] );
my $how = $args{how};
my $object = $args{object};
my $other = $args{other};
my $aargs = $args{args};
my $rel = $man->{relation_def}{$how};
my $doaction = "object:on_" . $rel->{unrelate_method};
my $tryaction = "object:try_" . $rel->{unrelate_method};
# Set the source if not already defined.
$aargs->{source} = 'direct' if (!defined($aargs->{source}));
# Get ID and check if related.
my $idobject = $man->id($object); $object = $man->find($idobject);
if (defined($man->{relate_to}{$idobject})
&& defined($man->{relate_to}{$idobject}{$how})) {
# Yes it is, so check that object to see if we can unrelate.
my $idself = $man->{relate_to}{$idobject}{$how};
my $self = $man->find($idself);
$other = $self if (!defined($other));
my $check =
$self->action(
object => $object,
other => $other,
action => $tryaction,
args => { %$aargs },
);
if ($check) {
# Check succeeded, so unrelate them.
delete $man->{relate_to}{$idobject}{$how};
my @nlist = ();
foreach my $item (@{$man->{relate_from}{$idself}{$how}}) {
push @nlist, $item if ($item ne $idobject);
}
@{$man->{relate_from}{$idself}{$how}} = @nlist;
# Invoke post-unrelate actions.
$self->action(
object => $object,
other => $other,
action => $doaction,
args => $aargs,
);
1;
} else {
0;
}
} else {
# Not related to anything in this manner. Since the end result
# is the same as the original condition, we consider this to
# be success.
1;
}
}
1;