# TiedElement.pm -- Maintains the underlying data for an Element. # -------------- Deals with autoloading off disk; saving to disk. # September 17, 2000 -- Copyright Sunir Shah. All rights reserved. package TiedElement; use Exporter; use Serialize; @ISA = qw( Exporter ); @EXPORT = qw(); @EXPORT_OK = qw(); @EXPORT_TAGS = qw(); # ------------------------------------------------------------- GLOBALS $DatabaseRoot = $main::DatabaseRoot || "g:\\www\\cgi-bin\\crystal\\db"; my %cache; # ------------------------------------------------------ PUBLIC METHODS # TIEHASH() # --------- # Tie constructor. Takes as parameters # # class The Element's actual class. # primary_key The primary key of the Element. # hash A reference to the hash being tied. # # The hash will be blessed as the target class. sub TIEHASH { my ($self, $class, $primary_key, $hash) = @_; return bless { class => $class, key => $primary_key, hash => $hash }, $self; } # load() # ------ # Loads the element from disk or the cache. sub load { my $self = shift; # Return if the element is cached. This makes the Elements Singletons. return if $cache{$self->{class}}->{$self->{key}}; my $object = bless {}, $self->{class}; $cache{$self->{class}}->{$self->{key}} = $object; $self->{hash}->_initialize($self->{key}); my $RECORD; my $path = $DatabaseRoot . '/' . convertToFilename($self->{class}) . '/' . convertToFilename($self->{key}) . '.tpb'; if( -e $path ) { open RECORD, '<' . $path or die "Cannot open $path for reading"; lock( *RECORD ); my $new = serializeIn( join " ", ); unlock( *RECORD ); close RECORD; while( my ($key, $elem) = each %$new ) { $object->{$key} = $elem; } } } # save() # ------ # Saves the instance to the database. sub save { my $self = shift; return unless $cache{$self->{class}}->{$self->{key}}; my $component = $cache{$self->{class}}->{$self->{key}}; $cache{$self->{class}}->{$self->{key}} = undef; my $string = serializeOut( $component, '', 'TiedElement::_saveDatabaseReference' ); if( $self->{key} ) { # Create the directory if it doesn't exist my $path = $DatabaseRoot . '/' . convertToFilename($self->{class}); mkdir $path, 0777 unless -e $path; my $RECORD; $path .= '/' . convertToFilename($self->{key}) . '.tpb'; open RECORD, '>' . $path or die "Cannot open $path for writing"; lock( *RECORD ); print RECORD $string; unlock( *RECORD ); close RECORD; } } # exists() # -------- # Returns true if the object exists for the given class. sub exists { my $class = shift or die "No class to load"; my $key = shift or return 0; my $path = $DatabaseRoot . '/' . convertToFilename($class) . '/' . convertToFilename($key) . '.tpb'; return -e $path; } # ------------------------------------------------ TIED HASH OPERATIONS # # All hash operations will load the element off disk into the cache # if it hasn't been done before and then do the requested operation on # the data. sub FETCH { my ($self, $key) = @_; load $self; return $cache{$self->{class}}->{$self->{key}}->{$key}; } sub STORE { my ($self, $key, $value) = @_; load $self; $cache{$self->{class}}->{$self->{key}}->{$key} = $value; } sub DELETE { my ($self, $key) = @_; load $self; delete $cache{$self->{class}}->{$self->{key}}->{$key}; } sub CLEAR { my $self = shift; load $self; %{$cache{$self->{class}}->{$self->{key}}} = (); } sub EXISTS { my ($self, $key) = @_; load $self; return exists $cache{$self->{class}}->{$self->{key}}->{$key}; } sub FIRSTKEY { my $self = shift; load $self; my $foo = scalar keys %{$cache{$self->{class}}->{$self->{key}}}; # Calling keys in a scalar context resets each. return scalar each %{$cache{$self->{class}}->{$self->{key}}}; } sub NEXTKEY { my $self = shift; return scalar each %{$cache{$self->{class}}->{$self->{key}}}; } # -------------------------------------------- STREAMING HELPER METHODS # convertToFilename() # ------------------- # Takes an ASCII string and filters it into a filename. # There is a guaranteed one-to-one mapping between ASCII # strings and filenames. sub convertToFilename { my $string = shift; return undef if !defined($string); $string =~ s/([^a-zA-Z0-9\-\_])/sprintf("_%02x",ord($1))/ge; return $string; } # convertFromFilename() # --------------------- # Takes a filename and converts it to an ASCII string. # This undoes filenames created by convertToFilename(). sub convertFromFilename { my $string = shift; return undef if !defined($string); $string =~ s/_([0-9a-fA-F]{2})/pack('C',oct("0x$1"))/ge; return $string; } # lock() # ------ # Locks a file. You must call unlock afterwards. # WARNING: This will block until the file is available. sub lock { my $FILE = shift; flock FILE, 2; # 2 == exclusive lock } # unlock() # -------- # Unlocks a lock()ed file. sub unlock { my $FILE = shift; flock FILE, 8; # 8 == unlock } # _saveDatabaseReference() # ------------------------ # Private. Called by _buildComponent when it encounters an # object to store. If the component can be stored, it returns the # database reference to the component and then stores the component # in the database. Else, it just streams it out. sub _saveDatabaseReference() { my ($object, $indent) = @_; my $tied = tied %$object or die 'Error: Can only make a database reference to a TiedElement.'; my $key = $tied->{key} or die 'Cannot store ' . ref($object) . ' without a primary key.'; my $string = ''; $string = $indent . '^' . ::convertToHttp(ref($object)) . '^'; $string .= ::convertToHttp($key); $string .= '^/' . ::convertToHttp(ref($object)) . "^\n"; $tied->save(); return $string; } 1;