# Serialize.pm -- Serialize in and out arbitrary object graphs to # ------------ strings. # September 18, 2000 -- Copyright Sunir Shah. All rights reserved. # ---------------------------------------------------- GLOBAL FUNCTIONS package main; # convertToHttp() # --------------- # Takes an ASCII string and converts it to a URL friendly # string. sub convertToHttp { my $string = shift; return undef if !defined($string); $string =~ s/([^a-zA-Z0-9\$\-\_\@\.\&\+\!\*\(\)\,])/sprintf("%%%02x",ord($1))/ge; return $string; } # convertFromHttp() # ----------------- # Takes an URL friendly string and converts it to a normal # ASCII string. sub convertFromHttp { my $string = shift; return undef if !defined($string); $string =~ s/\+/ /g; $string =~ s/%([0-9a-fA-F]{2})/pack('C',oct("0x$1"))/ge; return $string; } # --------------------------------------------------- package Serialize package Serialize; use Exporter; @ISA = qw( Exporter ); @EXPORT = qw( serializeOut serializeIn ); @EXPORT_OK = qw(); @EXPORT_TAGS = qw(); # ---------------------------------------------------------- STREAM OUT # serializeOut( $component, $indent, $aggregateCallback ) # ------------------------------------------------------- # Streams out $component to a string. It # looks like XML, but it's # case-sensitive amongst other restrictions (like different tag # delimiters). # # $indent is the current indentation level. It should be made # of spaces (ASCII 32). # # $aggregateCallback is a function to call when an aggregate class # (a data member that is an instance of a class) is encountered. # It should take as parameters the object to stream followed by # the indent. It should return the string representation of the # object. # e.g. $string .= &$aggregateCallback( $object, $indent ); sub serializeOut( $$$ ) { my ($component, $indent, $aggregateCallback) = @_; $indent = '' unless $indent; $aggregateCallback = '_buildObject' unless $aggregateCallback; my $string; my $ref = ref($component); if( grep /$ref/, qw( SCALAR ARRAY HASH CODE GLOB FILEHANDLE NAME PACKAGE ) ) { return _buildComponent( $ref, '', $indent, $aggregateCallback ); } # Actually stream this object out, bypassing the $aggregateCallback. return _buildObject( $component, $indent, $aggregateCallback ); } # _buildStream( $object, $indent, $aggregateCallback ) # ---------------------------------------------------- # Private. Parameters as passed to serializeOut(). sub _buildObject( $$$ ) { my ($object, $indent, $aggregateCallback) = @_; $aggregateCallback = '_buildObject' unless $aggregateCallback; my $string = $indent . "{" . ::convertToHttp(ref($object)) . "}\n"; my $newIndent = $indent . ' 'x4; while( my ($key,$elem) = each %$object ) { $string .= _buildComponent( $elem, $key, $newIndent, $aggregateCallback ); } $string .= "$indent\{/" . ::convertToHttp(ref($object)) . "}\n"; return $string; } # _buildComponent( $component, $tag, $indent, $aggregateCallback ) # ---------------------------------------------------------------- # Private. This is not a method; this a normal function. # Parameters as passed to serializeOut(). $tag is the name for # this component. It can be an empty string. Tags starting with # underscores will not be streamed; they are considered private. sub _buildComponent( $$$$ ) { my ($component, $tag, $indent, $aggregateCallback) = @_; # All tags prefixed with an underscore are considered # private (actually, they are more likely derived attributes) # and won't be streamed. return '' if $tag =~ /^_/; my $string = ''; my $newIndent = $indent . ' 'x4; # Scalar if( !ref($component) ) { $string .= "$indent\<" . ::convertToHttp($tag) . '>'; $string .= ::convertToHttp($component) . '\n"; } elsif( ref($component) eq 'ARRAY' ) { $string .= "$indent\[" . ::convertToHttp($tag) . "]\n"; foreach (@$component) { $string .= _buildComponent( $_, '', $newIndent, $aggregateCallback ); } $string .= "$indent\[/" . ::convertToHttp($tag) . "]\n"; } elsif( ref($component) eq "HASH" ) { $string .= "$indent\#" . ::convertToHttp($tag) . "\#\n"; while( my ($key, $elem) = each %$component ) { $string .= _buildComponent( $elem, $key, $newIndent, $aggregateCallback ); } $string .= "$indent\#/" . ::convertToHttp($tag) . "\#\n"; } # Aggregate elsif( ref($component) ) { # Collections don't provide or require tag names as a # readability feature if not an orthoganilty feature. if( $tag ) { $string .= "$indent\<" . ::convertToHttp($tag) . ">\n"; $string .= &$aggregateCallback( $component, $newIndent ); $string .= "$indent\\n"; } else { $string .= &$aggregateCallback( $component, $indent ); } } else { die 'Unknown component type: ' . ref($component); } return $string; } # ----------------------------------------------------------- STREAM IN # serializeIn() # ---------- # Creates a new object graph based on a string emitted from # serializeOut(). This is a function, not a method. # # If you pass in garbage, this will likely die(). sub serializeIn { my $stream = shift; study $stream; my @tag_stack; my @object_stack = ([]); # Start with one collection to hold the resultant # The tag delimiters cannot be valid HTTP characters. # Be careful to add the delimiters to the scalar value # regexp. while( $stream =~ m@ \G \s* # Start where we left off ( (<\s*/\s*([^>\s]*)\s*>) | # (<\s*([^>\s]*)\s*>) | # ({\s*/\s*([^}\s]*)\s*}) | # {/OBJECT} ({\s*([^}\s]*)\s*}) | # {OBJECT} (\[\s*/\s*([^\]\s]*)\s*]) | # [/COLLECTION] (\[\s*([^\]\s]*)\s*]) | # [COLLECTION] (\^\s*/\s*([^\^\s]*)\s*\^) | # ^/DATABASE REFERENCE^ (\^\s*([^\^\s]*)\s*\^) | # ^DATABASE REFERENCE^ (\#\s*/\s*([^\#\s]*)\s*\#) | # #/HASH# (\#\s*([^\#\s]*)\s*\#) | # #HASH# ([^\x3C\]\}\^\#]+) # Scalar value ) \s* @gx ) { my ( $start_scalar, $end_scalar, $start_object, $end_object, $start_collection, $end_collection, $start_db_reference, $end_db_reference, $start_hash, $end_hash, $scalar ) = ( ::convertFromHttp($5), ::convertFromHttp($3), ::convertFromHttp($9), ::convertFromHttp($7), ::convertFromHttp($13), ::convertFromHttp($11), ::convertFromHttp($17), ::convertFromHttp($15), ::convertFromHttp($21), ::convertFromHttp($19), ::convertFromHttp($22) ); if( defined($start_scalar) ) { my $scalar; push @tag_stack, $start_scalar; push @object_stack, \$scalar; } elsif( defined($end_scalar) ) { { my $last_tag = pop @tag_stack; die "$end_scalar is mismatched with $last_tag" if $end_scalar ne $last_tag; } my $scalar = pop @object_stack; $$scalar = '' if not defined($$scalar); die "Orphaned scalar: $end_scalar" if 0 == scalar(@object_stack) or not ref($object_stack[$#object_stack]); if( ref($object_stack[$#object_stack]) eq "ARRAY" ) { push @{$object_stack[$#object_stack]}, ($$scalar); } else { $object_stack[$#object_stack]->{$end_scalar} = $$scalar; } } elsif( defined($start_object) ) { push @tag_stack, $start_object; my $object = bless {}, $start_object; push @object_stack, $object; } elsif( defined($end_object) ) { { my $last_tag = pop @tag_stack; die "$end_object is mismatched with $last_tag" if $end_object ne $last_tag; } my $object = pop @object_stack; die "Orphaned object: $end_object" if 0 == scalar(@object_stack) or not ref($object_stack[$#object_stack]); if( ref($object_stack[$#object_stack]) eq "ARRAY" ) { push @{$object_stack[$#object_stack]}, ($object); } elsif( ref($object_stack[$#object_stack]) eq "SCALAR" ) { # Stuff the object into the scalar reference. # Ultimately, the scalar will be dereferenced, # so stuff a reference to the object. $object_stack[$#object_stack] = \$object; } else { $object_stack[$#object_stack]->{$end_object} = $object; } } elsif( defined($start_collection) ) { push @tag_stack, $start_collection; push @object_stack, []; } elsif( defined($end_collection) ) { { my $last_tag = pop @tag_stack; die "$end_collection is mismatched with $last_tag" if $end_collection ne $last_tag; } my $collection = pop @object_stack; die "Orphaned collection: $end_collection" if 0 == scalar(@object_stack) or not ref($object_stack[$#object_stack]); if( ref($object_stack[$#object_stack]) eq "ARRAY" ) { push @{$object_stack[$#object_stack]}, ($collection); } else { $object_stack[$#object_stack]->{$end_collection} = $collection; } } elsif( defined($start_db_reference) ) { my $primary_key; push @tag_stack, $start_db_reference; push @object_stack, \$primary_key; } elsif( defined($end_db_reference) ) { { my $last_tag = pop @tag_stack; die "$end_db_reference is mismatched with $last_tag" if $end_db_reference ne $last_tag; } my $primary_key = pop @object_stack; die "No primary key specified for $end_db_reference" if !$$primary_key; my $object = $end_db_reference->new( $$primary_key ); die "Orphaned object: $end_db_reference" if 0 == scalar(@object_stack) or not ref($object_stack[$#object_stack]); if( ref($object_stack[$#object_stack]) eq "ARRAY" ) { push @{$object_stack[$#object_stack]}, $object; } elsif( ref($object_stack[$#object_stack]) eq "SCALAR" ) { # Stuff the object into the scalar reference. # Ultimately, the scalar will be dereferenced, # so stuff a reference to the object. $object_stack[$#object_stack] = \$object; } else { $object_stack[$#object_stack]->{$end_db_reference} = $object; } } elsif( defined($start_hash) ) { push @tag_stack, ($start_hash); push @object_stack, {}; } elsif( defined($end_hash) ) { { my $last_tag = pop @tag_stack; die "$end_hash is mismatched with $last_tag" if $end_hash ne $last_tag; } my $hash = pop @object_stack; die "Orphaned hash: $end_hash" if 0 == scalar(@object_stack) or not ref($object_stack[$#object_stack]); if( ref($object_stack[$#object_stack]) eq "ARRAY" ) { push @{$object_stack[$#object_stack]}, ($hash); } elsif( ref($object_stack[$#object_stack]) ) { $object_stack[$#object_stack]->{$end_hash} = $hash; } } elsif( defined($scalar) ) { die "Scalar found in non-scalar context ($scalar)" if 0 == scalar(@object_stack) or not ref($object_stack[$#object_stack]) or ref($object_stack[$#object_stack]) ne "SCALAR"; ${$object_stack[$#object_stack]} = $scalar; } else { die 'Unknown state in Serialize::serializeIn().'; } } die 'Unbalanced tags; someone popped the resultant!' if 0 == scalar(@object_stack); return @{pop @object_stack} if wantarray; return $object_stack[0][0]; } 1;