package SOAP::Packager;

use strict;
use vars qw($VERSION);
use SOAP::Defs;

$VERSION = '0.25';

sub new {
    my ($class, $soap_prefix, $depth, $print_fcn) = @_;
    my $self = {
        soap_prefix => $soap_prefix, # this allows us to turn on/off namespace support
        depth       => $depth,
        print_fcn   => $print_fcn,
    };
    bless $self, $class;
}

sub is_registered {
    my ($self, $object) = @_;

              $self->{objref_dictionary}{$object}[0]
    if exists $self->{objref_dictionary}{$object};
}

sub register {
    my ($self, $envelope, $object, $already_serialized) = @_;

    #
    # $already_serialized is an optional parameter that you can pass as nonzero
    # to indicate that the object has been serialized into the stream, but might
    # be referred to by other objects. This is used to deal with the special
    # cases where roots (body root, and headers) may be referenced, and *could*
    # be used to implement the special case of strings/bytearrays, if we can figure
    # out a reasonable way of automating string/bytearray matching. I don't expect
    # to implement this feature in my serializing (as opposed to deserializing) code
    # anytime soon though.
    #

    #
    # my dictionaries spring into life the first time we use them,
    # so be careful always to use them via hash references here!
    #
    if (exists $self->{objref_dictionary}{$object}) {
        return $self->{objref_dictionary}{$object}[0];
    }
    elsif (exists $self->{objref_dictionary_while_sealing}) {
        if (exists $self->{objref_dictionary_while_sealing}{$object}) {
            return $self->{objref_dictionary_while_sealing}{$object}[0];
        }
        else {
            my $id = $envelope->_alloc_id();
            $self->{objref_dictionary_while_sealing}{$object} = 
                $already_serialized ? [$id] : [$id, $object];
            return $id;
        }
    }
    else {
        my $id = $envelope->_alloc_id();
        $self->{objref_dictionary}{$object} =
            $already_serialized ? [$id] : [$id, $object];
        return $id;
    }
}

sub seal {
    my ($self, $envelope) = @_;

    #
    # NOTE: seal() explicitly supports sealing off multiple times
    #       to deal with the Envelope/Header/Body special case, where Header
    #       mustn't have any "external pointers", but may have "internal pointers"
    #       coming from Body.
    #       whenever an object is sealed, it is popped from the packager's
    #       dictionary (the hashed objref-->id mapping is left intact though)
    #       so that on a reseal, we won't end up reserializing any objects
    #

    # quit if there's nothing to do
    return unless exists $self->{objref_dictionary};

    #
    # the presence of this member variable indicates that we are sealing
    #
    my $objref_dictionary_while_sealing      =
    $self->{objref_dictionary_while_sealing} = {};

    my $objref_dictionary = $self->{objref_dictionary};
    while (my ($key, $id_and_object) = each %$objref_dictionary) {
        if (2 == @$id_and_object) {
            $self->seal_object($envelope, @$id_and_object);
            pop @{$id_and_object};
        }
    }

    while (%$objref_dictionary_while_sealing) {
        #
        # first merge newly added items into our main identity table
        #
        while (my ($key, $id_and_object) = each %$objref_dictionary_while_sealing) {
            $self->{objref_dictionary}{$id_and_object->[1]} = $id_and_object;
        }

        my $prev_dict = $objref_dictionary_while_sealing;
        $objref_dictionary_while_sealing         =
        $self->{objref_dictionary_while_sealing} = {};

        #
        # finally serialize the items added during the previous pass
        #
        while (my ($key, $id_and_object) = each %$prev_dict) {
            if (2 == @$id_and_object) {
                $self->seal_object($envelope, @$id_and_object);
                pop @{$id_and_object};
            }
        }
    }
    delete $self->{objref_dictionary_while_sealing};
}

sub seal_object {
    my ($self, $envelope, $id, $object) = @_;

    my $serializer = $envelope->_get_type_mapper()->get_serializer($object);

    my ($accessor_uri, $accessor_name) = $serializer->get_typeinfo();

    $accessor_name ||= 'item';

    my $sp = $self->{soap_prefix};

    my $attrs = qq[ ${sp}id="$id"];

    if ($serializer->is_compound()) {
	my $new_depth = $self->{depth} + 1;

	my $nsprefix = '';
	if (defined $accessor_uri) {
	    (my $nsdecl, $nsprefix) = $envelope->_push_ns_decl_and_prefix($accessor_uri, $new_depth);
	    $attrs .= $nsdecl if $nsdecl;
	}
	my $tag = $nsprefix . $accessor_name;

	$self->_print(qq[<$tag$attrs>]);

	my $packager = $serializer->is_package() ?
	    $envelope->_create_new_package($new_depth) : $self;
	
	my $stream = SOAP::OutputStream->new();
	$stream->{tag}          = $tag;
	$stream->{packager}     = $packager;
	$stream->{envelope}     = $envelope;
	$stream->{print_fcn}    = $self->{print_fcn};
	$stream->{soap_prefix}  = $self->{soap_prefix};
	$stream->{depth}        = $new_depth;

	$serializer->serialize($stream, $envelope);
	$stream->term();
    }
    else {
        my $content = $serializer->serialize_as_string();

        my $nsprefix = '';
        if (defined $accessor_uri) {
            (my $nsdecl, $nsprefix) = $envelope->_get_ns_decl_and_prefix($accessor_uri);
            $attrs .= $nsdecl if $nsdecl;
        }
        my $tag = $nsprefix . $accessor_name;

        $self->_print(qq[<$tag$attrs>$content</$tag>]);

        return;

    }
}

sub _print {
    my ($self, $s) = @_;
    
    $self->{print_fcn}->($s);
}



1;
__END__


=head1 NAME

SOAP::Packager - SOAP internal helper class

=head1 SYNOPSIS

    use SOAP::Packager;
    my $packager = SOAP::Packager->new('s:', 1, sub { print shift } );

    # some object used as a reference
    my $object = SOAP::Object->new();

    # on a given packager, register() always returns the same id for a given object
    my $id = $packager->register($env, $object);
    unless($id == $packager->register($env, $object)) { die "internal error" }

    # this serializes objectA
    $packager->seal($envelope);

    # note that the package is still valid
    unless($id == $packager->register($env, $object)) { die "internal error" }

    my $objectB = SOAP::Object->new();
    my $idB = $packager->register($env, $objectB);
    unless($idB == $packager->register($env, $objectB)) { die "internal error" }

    # this just serializes objectB - objectA was already serialized before
    $packager->seal($env);

    # this does nothing except waste some cycles enumerating a hash table
    $packager->seal($env=);

    # hash tables shut down at destruction of packager, releasing object references
    $packager = undef;

=head1 DESCRIPTION

This is an internal class used by the SOAP/Perl implementation. It is designed to
manage a table of object references and XML ids used for serializing object graphs
that may contain multiref data (and perhaps even cycles). If you are extending
SOAP/Perl, the above synopsis will probably be all you need if you want to reuse this
class. Whatever you pass for the $env reference should implement a function called
_alloc_id that returns a unique string each time it is called. This is normally
implemented by SOAP::Envelope, so you can see a sample implementation there.

NOTE NOTE NOTE: The SOAP "package" attribute was dropped when the SOAP spec
                went from version 1.0 to version 1.1. Use package-related
                functionality at your own risk - you may not interoperate
                with other servers if you rely on it. I'll eventually remove
                this feature if it doesn't reappear in the spec soon.

=head1 AUTHOR

Keith Brown

=head1 SEE ALSO

SOAP::Envelope
SOAP::OutputStream

=cut
