#!/usr/bin/perl -s

##########################################################################
#
# Module: ............... <user defined location>/eBay/API/XML
# File: ................. Session.pm
# Original Author: ...... Bob Bradley
# Last Modified By: ..... Jeff Nokes
# Last Modified: ........ 03/30/2007 @ 18:33
#
# This class is used to control api calls made in parallel, offer some
# transactional logic when executing the calls, as well as retries.
#
##########################################################################

=head1 eBay::API::XML::Session

Cluster and submit several eBay XML API calls at once.

=head1 DESCRIPTION

This module collects multiple requests to the eBay XML API and submits
them sequentially or in parallel.  Session.pm uses the CPAN module, 
LWP::Parallel, to manage the parallel submission of HTTP requests to the 
eBay XML API.

=head1 SYNOPSIS

  use eBay::API::XML::Call::GeteBayOfficialTime;
  use eBay::API::XML::Call::GetUser;
  use eBay::API::XML::DataType::Enum::DetailLevelCodeType;
  use eBay::API::XML::Call::GetSearchResults;
  use eBay::API::XML::DataType::PaginationType;
  use eBay::API::XML::Session;

  # Create a session (authorization info is pulled from ENV by the constructors)
  my $session = new eBay::API::XML::Session;

  # Get official time.
  my $pCall = eBay::API::XML::Call::GeteBayOfficialTime->new();
  $session->addRequest($pCall);

  # Get user details
  my $getUserCall = eBay::API::XML::Call::GetUser->new();
  $getUserCall->setDetailLevel( [eBay::API::XML::DataType::Enum::DetailLevelCodeType::ReturnAll] );
  $session->addRequest($getUserCall);

  # Get search results
  my $getListingsCall = new eBay::API::XML::Call::GetSearchResults;
  $getListingsCall->setQuery("new");
  my $pagination = new eBay::API::XML::DataType::PaginationType;
  $pagination->setEntriesPerPage(10);
  $getListingsCall->setPagination($pagination);
  $session->addRequest($getListingsCall);

  # session will submit the calls in parallel -- then wait til all come back
  $session->execute();

  # get results from various calls
  my $itemarray = $getListingsCall->getSearchResultItemArray()->getSearchResultItem();
  my $officialtime = $pCall->getEBayOfficialTime();
  my $pUser = $getUserCall->getUser();
  my $sStatusCode = $pUser->getStatus();
  my $sSiteCode  = $pUser->getSite();


=cut




# Package Declaration
# -------------------------------------------------------------------------
package eBay::API::XML::Session;

# Required Includes
# -------------------------------------------------------------------------
use strict;
use warnings;
use Exporter;
use LWP::Parallel;   # http://search.cpan.org/~marclang/ParallelUserAgent-2.57/
                     # Support for submitting bundled requests in parallel.
use Data::Dumper;
use eBay::API::XML::BaseXml;  # parent class
use HTTP::Request;

# Global Variables
our $VERSION = '0.01';    # The version of this module.
our @ISA = (
	    'Exporter', 
	    'eBay::API::XML::BaseXml',  # Parent class with logging framework
	   );

# :DEFAULT exported symbols
our @EXPORT = qw(
		);

# Subroutine Prototypes
# ----------------------------------------------------------------------------------
# Method Name                              Accessor Priviledges      Method Type

sub new($;$);
sub addRequest($$;$);                #         Public                Instance
sub clearSession($;);                #         Public                Instance
sub execute($;);                     #         Public                Instance
sub setSequentialExecution($$;);     #         Public                Instance
sub isSequentialExecution($;);       #         Public                Instance
sub _execute_sequential($;);         #         Private               Instance
sub _execute_parallel($;);           #         Private               Instance
sub _process_callback($$;);          #         Private               Instance

# Main Script
# ---------------------------------------------------------------------------
#

# Subroutine Definitions
# ---------------------------------------------------------------------------

=head1 Subroutines:



=pod

=head2 new()

Session constructor.  This constructor delegates most of the work to
the constructor for the abstract parent class, eBay::API::BaseApi.  See
perldoc eBay::API::BaseApi for more details.

Arguments:

=over 4

=item *

B<sequential>  If this is true (non-zero), then the bundled calls will
be executed sequentially.  See method setSequentialExecution() for more
details.

=back

Returns:

=over 4

=item *

B<success> A reference to a blessed Session object.

=item *

B<error> Undefined if an exception was encountered during construction
of the session object.  In that case, consult the log file for more
details.

=back


=cut



sub new($;$) {
  my $class = shift;
  my $arg_hash = shift;

  # validate that first arguments is blessed object
  eBay::API::BaseApi::_check_arg($class, Params::Validate::SCALAR);

  # more validations are done in the parent classes
  my $self = $class->SUPER::new($arg_hash);

  if (defined $self) {
    $self->clearSession();
    if    (defined($arg_hash)) {
      # validate that the arguements are a reference to a hash
      eBay::API::BaseApi::_check_arg($arg_hash, Params::Validate::HASHREF);
      if ($arg_hash->{sequential}) {
	$self->{sequential} = $arg_hash->{sequential};
      }
    }
  }
  return $self;
}



=head2 addRequest()

Instance method to add an eBay::API::XML::Call to the request bundle.

Arguments:

=over 4

=item *

Object reference of type eBay::API::XML::Session

=item *

Reference to an eBay::API::XML::Call to be issued to the eBay XML API.

=item *

Optional reference to a callback subroutine to be called when the
http request returns.  Note.  This subroutine will be called whether
the return is a success, a failure, or a timeout.

Argument going to the callback subroutine is the call object it is
associated with.

=back

Returns: None

=cut

sub addRequest($$;$) {
  my ($self, $apicall, $callback) = @_;

  # validate that first two arguments are blessed objects
  eBay::API::BaseApi::_check_arg($self, Params::Validate::OBJECT);
  eBay::API::BaseApi::_check_arg($apicall, Params::Validate::OBJECT);

  unshift (@{$self->{requestqueue}}, $apicall);
  if (defined $callback) {
    eBay::API::BaseApi::_check_arg($callback, Params::Validate::CODEREF);
    $self->{callbacks}{$apicall} = $callback;
  }
}



=pod

=head2 clearSession()

Reset an eBay::API::XML::Session object so it may be re-used.

This involves the following:

=over 4

=item *

Remove all bundled eBay::API::XML::Request objects.

=item *

Clear error information if present.

=back

Arguments:

=over 4

=item *

Object reference of type eBay::API::XML::Session

=back

Returns:

=over 4

=item *

B<success> Object reference to the eBay::API::XML::Session.

=item *

B<failure> undefined

=back

=cut


sub clearSession($;) {
  my $self = shift;

  # validate that first arguments is blessed object
  eBay::API::BaseApi::_check_arg($self, Params::Validate::OBJECT);

  # iniitialize Session member variables to sane values
  $self->_setError('');
  $self->{requestqueue} = ();
  $self->{sequential}  = 0;
  $self->{callbacks} = {};
  return $self;
}




=head2 execute()

Instance method used for executing the actual XML API request bundle.
This method really does most of the work.  It will attempt to perform
all necessary validations, as well as create and send the bundle of
XML requests.

This method will block until all issued requests have responses, or
until the timeout.  After the responses come back from the API, they
are populated back into the call objects registered with the session
so they can be accessed from the client application.

execute() also returns the eBay::API::XML::Response objects in an array.  
This array may have responses for all, some, or none of the issued 
requests, depending on the success or failure of each request.

If the client application wants to use the request objects in the returned
array, it should match up each response in the array with the
corresponding request.  This can best be done by using and tracking a
unique message id for each request.

If an incomplete set of responses are returned, an appropriate error
will be set and available to the getError() method.

Arguments:

=over 4

=item *

Object reference of type eBay::API::XML::Session

=back

Returns:

=over 4

=item *

B<success> Reference to array of api call objects submittted to the eBay
API.  The calls may, or may not have executed successfully; it is up to
the user to check error status of the session and possibly individual calls.

=item *

B<failure> undefined

=back

=cut



sub execute($;) {

  # Get all values passed in.
  my $self = shift;

  # validate that first argument is blessed object
  eBay::API::BaseApi::_check_arg($self, Params::Validate::OBJECT);

  if ($self->isSequentialExecution()) {
    return $self->_execute_sequential();
  } else {
    return $self->_execute_parallel();
  }
}



# _execute_parallel()

#  Use LWP::Parallel to do parallel processing of the bundled calls.

sub _execute_parallel($;) {
  my $self = shift;

  # validate that first argument is blessed object
  eBay::API::BaseApi::_check_arg($self, Params::Validate::OBJECT);


  # Set up for retries.
  # If the user has registered a retry object with retry requirements for the 
  # session, then use those requirements.  Otherwise, default to only 1 try (that
  # is to say, no retries.  The retry object has two main things: 1) the number
  # of times to retry, and 2) the types of errors that qualify for retry.
  # See perldoc eBay::API::XML::CallRetry for more details on the retry object.
  my @calls = @{$self->{requestqueue}};  # egg basket; take calls out and put back in to retry
  my $moreretries = 1;
  my $retryobj = $self->getCallRetry();
  if ( defined $retryobj ) {
     $moreretries =  $retryobj->getMaximumRetries();
  }

  # BEGIN RETRY LOOP

  # Loop through the current egg basket
  while ($moreretries && @calls) {

    $moreretries--;
    my %requests;
    my $parallel_agent = new LWP::Parallel::UserAgent;

    # Register the requests.  In the case of retries, we
    # only register those requests that were unsuccessful
    # in the previous try.
    while (my $apicall = pop(@calls)) {
      my $httprequest = $apicall->_getHttpRequestObject();
      $requests{$httprequest} = $apicall;
      $parallel_agent->register($httprequest);
    }

    # submit parallel and wait
    my $entries = $parallel_agent->wait($self->getTimeout());

    # Process the responses.
    while ( my ($key, $entry) = each %$entries) {
      my $httpresponse = $entry->{response};
      my $apicall = $requests{$key};
      # The base call does most of the work here.
      $apicall->processResponse($httpresponse);
      # test for errors and possibly retry
      if ($moreretries && defined $retryobj &&
	  $retryobj->shouldRetry(
				 'raErrors' => $apicall->getErrorsAndWarnings()
				)) {
	unshift(@calls, $apicall);
      }
    }

  }

  # END RETRY LOOP

  # Process the callbacks and any errors
  foreach (@{$self->{requestqueue}}) {
    $self->_process_callback($_);
    if ($_->hasErrors()) {
      my ($error) = @{$_->getErrors()};
      $self->_setError($error->getShortMessage);
    }
  }

  # Return the api call bundle

  return $self->{requestqueue};
}

#_execute_sequential

#Private instance method to execute the bundled calls in the
#sequence in which they were added to the bundle.

sub _execute_sequential($;) {
  my $self = shift;

  # validate that first arguments is blessed object
  eBay::API::BaseApi::_check_arg($self, Params::Validate::OBJECT);

  # Calls were unshifted on to the request queue array
  # in the order in which they were added to the session.
  # Now pop them off; and execute the calls in the same
  # sequence.
  my @calls = @{$self->{requestqueue}};
  while (my $call = pop(@calls)) {
    $call->execute();
    $self->_process_callback($call);
    if ($call->hasErrors()){
      my ($error) = @{$call->getErrors()};
      $self->_setError($error->getShortMessage);
      last;
    }
  }
  return $self->{requestqueue};
}

# _process_callback()

# Private method.  Check if a call was registered with a callback.
# If so, then call the callback.


sub _process_callback($$;) {
  my $self = shift;
  my $call = shift;

  # validate that first two arguments are blessed objects
  eBay::API::BaseApi::_check_arg($self, Params::Validate::OBJECT);
  eBay::API::BaseApi::_check_arg($call, Params::Validate::OBJECT);


  # If this call has a callback associated with it, then call
  # the callback.
  if (exists($self->{callbacks}{$call})) {
    my $callback = $self->{callbacks}{$call};
    &$callback($call);
  }
}


=head2 isSequentialExecution()

Returns current state of the session with regard to whether session
calls should be executed in sequence as an ordered transaction rather
than in parallel.


Arguments:

=over 4

=item *

Object reference of type eBay::API::XML::Session.

=back


Returns:

=over 4

=item *

B<zero - true> Session is set to issue calls in parallel.

=item *

B<non-zero - true> Session will issue calls sequentially.

=back

=cut


sub isSequentialExecution($;) {
  my $self = shift;

  # validate that first argument is blessed object
  eBay::API::BaseApi::_check_arg($self, Params::Validate::OBJECT);

  return $self->{sequential};
}


=head2 setSequentialExecution()

Instance method to prepare the session to execute the API requests
bundled in the session in the sequence in which they were added to the
session. The only difference between this and the normal execution
state is that the execute() method will execute each api call
asynchronously rather than in parallel.  If an error is encountered,
none of the calls after the error was encountered will be sent to the
eBay API.  This behavior, in effect, offers a kind application-level
transaction integrity, although there is no concept of 'rollback' in
the sense of backing out the effects of calls executed prior to the
error was encountered.

See the description of the execute() subroutine for more details.

Arguments:

=over 4

=item *

Object reference of type eBay::API::XML::Session.

=item *

Boolean.  True will set execution mode to sequential.  False
will set execution mode to parallel.

=back

Returns:

=over 4

=item *

B<success> Value set for execution mode.

=item *

B<failure> undefined

=back

=cut


sub setSequentialExecution($$;) {
  my ($self, $bool) = @_;

  # validate that first argument is blessed object
  eBay::API::BaseApi::_check_arg($self, Params::Validate::OBJECT);

  $self->{sequential} = $bool;
  return $bool;
}




# Return TRUE to perl
1;
