# PogoLink.pm - bidirectional relationship class for Pogo # 2000 Sey Nakajima use Pogo; # Abstract base class package PogoLink; use Carp; use strict; use vars qw(@Fields %Fields); BEGIN { @Fields = qw(OBJECT LINK LINKCLASS INVFIELD KEYFIELD SIZE LINKCLASSISARRAY); %Fields = map { $Fields[$_], $_+1 } (0 .. $#Fields); sub FIELDHASH { \%Fields } } sub new { my($class, $object, $linkclass, $invfield, $keyfield, $size) = @_; my $type = (Pogo::type_of($object))[0]; croak "Hash or array object required" unless $type eq 'HASH' || $type eq 'ARRAY'; my $self = new_tie Pogo::Harray 8, $object, $class; $self->{OBJECT} = $object; $self->{LINK} = undef; $self->{LINKCLASS} = $linkclass; $self->{INVFIELD} = $invfield; $self->{KEYFIELD} = $keyfield; $self->{SIZE} = $size; $self->{LINKCLASSISARRAY} = $invfield =~ /^\d+$/; $self; } sub clear { my $self = shift; my @objects = $self->getlist; return unless @objects; my $invfield = $self->{INVFIELD}; Pogo::tied_object($self)->begin_transaction; for my $object(@objects) { $self->_del($object); if( $self->{LINKCLASSISARRAY} ) { $object->[$invfield]->_del($self->{OBJECT}); } else { $object->{$invfield}->_del($self->{OBJECT}); } } Pogo::tied_object($self)->end_transaction; } sub del { my($self, $object) = @_; return unless $object && ref($object); return unless $self->find($object); my $invfield = $self->{INVFIELD}; Pogo::tied_object($self)->begin_transaction; $self->_del($object); if( $self->{LINKCLASSISARRAY} ) { $object->[$invfield]->_del($self->{OBJECT}); } else { $object->{$invfield}->_del($self->{OBJECT}); } Pogo::tied_object($self)->end_transaction; } sub add { my($self, $object) = @_; return unless $object && ref($object); my $linkclass = $self->{LINKCLASS}; croak "Class mismatch" if $linkclass && !$object->isa($linkclass); return if $self->find($object); my $invfield = $self->{INVFIELD}; my $type = (Pogo::type_of($object))[0]; croak "Hash object required" unless $type eq 'HASH' || ($type eq 'ARRAY' && (Pogo::type_of($object->[0]))[0] eq 'HASH'); my $invfieldvalue = $self->{LINKCLASSISARRAY} ? $object->[$invfield] : $object->{$invfield}; if( !$invfieldvalue && $object->can("INIT_$invfield") ) { my $initmethod = "INIT_$invfield"; no strict 'refs'; $object->$initmethod(); } $invfieldvalue = $self->{LINKCLASSISARRAY} ? $object->[$invfield] : $object->{$invfield}; croak "Inverse attribute must be a PogoLink::* object" unless (Pogo::type_of($invfieldvalue))[1] =~ /^PogoLink::/; Pogo::tied_object($self)->begin_transaction; $self->_add($object); $invfieldvalue->_add($self->{OBJECT}); Pogo::tied_object($self)->end_transaction; } package PogoLink::Scalar; use Carp; use strict; use vars qw(@ISA); @ISA = qw(PogoLink); sub get { my $self = shift; $self->{LINK}; } sub getlist { my $self = shift; return () unless $self->{LINK}; ($self->{LINK}); } sub find { my($self, $object) = @_; Pogo::equal($self->{LINK}, $object); } sub _del { my($self, $object) = @_; $self->{LINK} = undef if Pogo::equal($self->{LINK}, $object); } sub _add { my($self, $object) = @_; my $invfield = $self->{INVFIELD}; if( $self->{LINK} ) { if( $self->{LINKCLASSISARRAY} ) { $self->{LINK}->[$invfield]->_del($self->{OBJECT}); } else { $self->{LINK}->{$invfield}->_del($self->{OBJECT}); } } $self->{LINK} = $object; } package PogoLink::Array; use Carp; use strict; use vars qw(@ISA); @ISA = qw(PogoLink); sub get { my($self, $idx) = @_; return undef unless $self->{LINK}; defined $idx ? $self->{LINK}->[$idx] : @{$self->{LINK}}; } sub getlist { my $self = shift; return () unless $self->{LINK}; @{$self->{LINK}}; } sub find { my($self, $object) = @_; return 0 unless $self->{LINK}; grep Pogo::equal($_, $object), @{$self->{LINK}}; } sub _del { my($self, $object) = @_; return unless $self->{LINK}; @{$self->{LINK}} = grep !Pogo::equal($_, $object), @{$self->{LINK}}; } sub _add { my($self, $object) = @_; unless( $self->find($object) ) { $self->{LINK} = new Pogo::Array($self->{SIZE}) unless $self->{LINK}; push @{$self->{LINK}}, $object; } } package PogoLink::Hash; use Carp; use strict; use vars qw(@ISA); @ISA = qw(PogoLink); sub get { my($self, $key) = @_; return undef unless $self->{LINK}; defined $key ? $self->{LINK}->{$key} : values %{$self->{LINK}}; } sub getlist { my $self = shift; return () unless $self->{LINK}; values %{$self->{LINK}}; } sub getkeylist { my $self = shift; return () unless $self->{LINK}; keys %{$self->{LINK}}; } sub find { my($self, $object) = @_; return 0 unless $self->{LINK}; my $key = $self->{LINKCLASSISARRAY} ? $object->[$self->{KEYFIELD}] : $object->{$self->{KEYFIELD}}; exists $self->{LINK}->{$key}; } sub _del { my($self, $object) = @_; return unless $self->{LINK}; my $key = $self->{LINKCLASSISARRAY} ? $object->[$self->{KEYFIELD}] : $object->{$self->{KEYFIELD}}; delete $self->{LINK}->{$key}; } sub _add { my($self, $object) = @_; unless( $self->find($object) ) { $self->{LINK} = new Pogo::Hash($self->{SIZE}) unless $self->{LINK}; my $key = $self->{LINKCLASSISARRAY} ? $object->[$self->{KEYFIELD}] : $object->{$self->{KEYFIELD}}; $self->{LINK}->{$key} = $object; } } package PogoLink::Htree; use Carp; use strict; use vars qw(@ISA); @ISA = qw(PogoLink::Hash); sub _add { my($self, $object) = @_; unless( $self->find($object) ) { $self->{LINK} = new Pogo::Htree($self->{SIZE}) unless $self->{LINK}; my $key = $self->{LINKCLASSISARRAY} ? $object->[$self->{KEYFIELD}] : $object->{$self->{KEYFIELD}}; $self->{LINK}->{$key} = $object; } } package PogoLink::Btree; use Carp; use strict; use vars qw(@ISA); @ISA = qw(PogoLink::Hash); sub _add { my($self, $object) = @_; unless( $self->find($object) ) { $self->{LINK} = new Pogo::Btree unless $self->{LINK}; my $key = $self->{LINKCLASSISARRAY} ? $object->[$self->{KEYFIELD}] : $object->{$self->{KEYFIELD}}; $self->{LINK}->{$key} = $object; } } package PogoLink::Ntree; use Carp; use strict; use vars qw(@ISA); @ISA = qw(PogoLink::Hash); sub _add { my($self, $object) = @_; unless( $self->find($object) ) { $self->{LINK} = new Pogo::Ntree unless $self->{LINK}; my $key = $self->{LINKCLASSISARRAY} ? $object->[$self->{KEYFIELD}] : $object->{$self->{KEYFIELD}}; $self->{LINK}->{$key} = $object; } } 1; __END__ =head1 NAME PogoLink - Bidirectional relationship class for objects in a Pogo database =head1 SYNOPSIS use PogoLink; # Define relationships package Person; sub new { my($class, $name) = @_; my $self = new_tie Pogo::Hash 8, undef, $class; %$self = ( NAME => $name, FATHER => new PogoLink::Scalar($self, 'Man', 'CHILDREN'), MOTHER => new PogoLink::Scalar($self, 'Woman', 'CHILDREN'), FRIENDS => new PogoLink::Btree ($self, 'Person', 'FRIENDS', 'NAME'), ); $self; } package Man; @ISA = qw(Person); sub new { my($class, $name) = @_; my $self = $class->SUPER::new($name); $self->{CHILDREN} = new PogoLink::Array ($self, 'Person', 'FATHER'); $self->{WIFE} = new PogoLink::Scalar($self, 'Woman', 'HUS'); $self; } package Woman; @ISA = qw(Person); sub new { my($class, $name) = @_; my $self = $class->SUPER::new($name); $self->{CHILDREN} = new PogoLink::Array ($self, 'Person', 'MOTHER'); $self->{HUS} = new PogoLink::Scalar($self, 'Man', 'WIFE'); $self; } # Use relationships $Dad = new Man 'Dad'; $Mom = new Woman 'Mom'; $Jr = new Man 'Jr'; $Gal = new Woman 'Gal'; # Marriage $Dad->{WIFE}->add($Mom); # $Mom->{HUS} links to $Dad automatically # Birth $Dad->{CHILDREN}->add($Jr); # $Jr->{FATHER} links to $Dad automatically $Mom->{CHILDREN}->add($Jr); # $Jr->{MOTHER} links to $Mom automatically # Jr gets friend $Jr->{FRIENDS}->add($Gal); # $Gal->{FRIENDS} links to $Jr automatically # Oops! Gal gets Dad $Gal->{HUS}->add($Dad); # $Dad->{WIFE} links to $Gal automatically # $Mom->{HUS} unlinks to $Dad automatically =head1 DESCRIPTION PogoLink makes single-single or single-multi or multi-multi bidirectional relationships between objects in a Pogo database. The relationships are automatically maintained to link each other correctly. You can choose one of Pogo::Array, Pogo::Hash, Pogo::Htree, Pogo::Btree and Pogo::Ntree to make a multi end of link. =over 4 =head2 Classes =item PogoLink::Scalar This class makes a single end of link. =item PogoLink::Array This class makes a multi end of link as an array. It uses Pogo::Array to have links. =item PogoLink::Hash, PogoLink::Htree, PogoLink::Btree, PogoLink::Ntree These classes make a multi end of link as a hash. Each uses corresponding Pogo::* to have links. =head2 Methods =item new PogoLink::* $selfobject, $linkclass, $invfield, $keyfield, $size Constructor. Class method. $selfobject is a object in the database which possesses this link. It must be a object as a hash reference. $linkclass is a class name of linked object. If $linkclass defaults, any class object is allowed. $invfield is a field (i.e. hash key) name of the linked object which links inversely. $keyfield is only necessary for PogoLink::Hash, PogoLink::Htree, PogoLink::Btree, PogoLink::Ntree. It specifies a field name of the linked object thats value is used as the key of this link hash. $size may be specified for PogoLink::Array, PogoLink::Hash or PogoLink::Htree. $size will be used when internal linking Pogo::Array, Pogo::Hash or Pogo::Htree object will be constructed. NOTE: You cannot use PogoLink::* constructors as follows in a class constructor. sub new { my($class) = @_; my $self = {}; bless $self, $class; $self->{FOO} = new PogoLink::Scalar $self, 'Foo', 'BAR'; $self; } Because the self-object which is passed to PogoLink::* constructors must be tied to a Pogo::* object. In the above sample, $self is a Perl object on the memory yet. The right way is as follows. sub new { my($class) = @_; my $self = new_tie Pogo::Hash 8, undef, $class; $self->{FOO} = new PogoLink::Scalar $self, 'Foo', 'BAR'; $self; } You can make a blessed reference which is tied to specified Pogo::* object by using new_tie which takes a class name as arguments. =item get $idx_or_key Get the linked object. For PogoLink::Scalar, $idx_or_key is not necessary. For PogoLink::Array, $idx_or_key is an array index number. For other, $idx_or_key is a hash key string. =item getlist Get the linked object list. =item getkeylist Get the hash key list of linked objects. Only available for PogoLink::Hash, PogoLink::Htree, PogoLink::Btree, PogoLink::Ntree. =item find $object Test the link if it links to $object. =item clear Unlink to all objects in the link. =item del $object Unlink to $object. =item add $object Link to $object. The inverse field (it's name was specified as $invfield by new()) of $object must be a PogoLink::* object. If the inverse field is not defined yet and $object has INIT_fieldname method (e.g. the field name is 'FIELD', the method name is 'INIT_FIELD'), this method calls $object->INIT_fieldname() to initialize the inverse field before linking. =back =head1 AUTHOR Sey Nakajima =head1 SEE ALSO Pogo(3). sample/person.pl.