# Copyrights 2007-2008 by Mark Overmeer.
#  For other contributors see ChangeLog.
# See the manual pages for details on the licensing terms.
# Pod stripped from pm file by OODoc 1.04.
use warnings;
use strict;

package XML::Compile::SOAP::Daemon;
use vars '$VERSION';
$VERSION = '0.11';

our @ISA;   # filled-in at new().

use Log::Report 'xml-compile-soap-daemon', syntax => 'SHORT';

use XML::LibXML                  ();
use XML::Compile::Util           qw/type_of_node/;
use XML::Compile::SOAP::Util     qw/SOAP11ENV SOAP12ENV/;
use XML::Compile::SOAP11::Server ();
use XML::Compile::SOAP12::Server ();

use List::Util    qw/first/;
use Time::HiRes   qw/time/;

# we use HTTP status definitions for each protocol
use HTTP::Status  qw/RC_BAD_GATEWAY RC_NOT_IMPLEMENTED RC_SEE_OTHER/;

my $parser        = XML::LibXML->new;
my %faultWriter;
my @levelToReason = qw/ERROR WARNING NOTICE INFO TRACE/;


sub default_values()
{   my $self  = shift;
    my $def   = $self->SUPER::default_values;
    my %mydef =
     ( # changed defaults
       setsid => 1, background => 1, log_file => 'Log::Report'

       # make in-code defaults explicit, Net::Server 0.97
       # see http://rt.cpan.org//Ticket/Display.html?id=32226
     , log_level => 2, syslog_ident => 'net_server', syslog_logsock => 'unix'
     , syslog_facility => 'daemon', syslog_logopt => 'pid'
     );
   @$def{keys %mydef} = values %mydef;
   $def;
}


sub new(@)  # not called by HTTPDaemon
{   my ($class, %args) = @_;

    # Use a Net::Server as base object

    my $daemon = delete $args{based_on} || 'Net::Server::PreFork';
    unless(ref $daemon)
    {   eval "require $daemon";
        $@ and error __x"failed to compile Net::Server class {class}, {error}"
           , class => $daemon, error => $@;

        my %options;
        $daemon = $daemon->new;
    }

    $daemon->isa('Net::Server')
        or error __x"The daemon is not a Net::Server, but {class}"
             , class => ref $daemon;

    # Upgrade daemon, wow Perl!
    @ISA = ref $daemon;
    (bless $daemon, $class)->init(\%args);
}

sub init($)
{   my ($self, $args) = @_;
    my $support = delete $args->{support_soap} || 'ANY';
    $self->{supported} = ref $support ? $support->version : $support;

    foreach my $version ($self->soapVersions)
    {   $self->isSupportedVersion($version) or next;
        $faultWriter{$version}
          = "XML::Compile::${version}::Server"->faultWriter;
    }

    $self->{output_charset} = delete $args->{output_charset} || 'UTF-8';
    $self;
}

sub post_configure()
{   my $self = shift;
    my $prop = $self->{server};

    # Change the way messages are logged

    my $loglevel = $prop->{log_level};
    my $reasons  = ($levelToReason[$loglevel] || 'NOTICE') . '-';

    my $logger   = delete $prop->{log_file};
    if($logger eq 'Log::Report')
    {   # dispatching already initialized
    }
    elsif($logger eq 'Sys::Syslog')
    {   dispatcher SYSLOG => 'default'
          , accept    => $reasons
          , identity  => $prop->{syslog_ident}
          , logsocket => $prop->{syslog_logsock}
          , facility  => $prop->{syslog_facility}
          , flags     => $prop->{syslog_logopt}
    }
    else
    {   dispatcher FILE => 'default', to => $logger;
    }

    $self->SUPER::post_configure;
}

sub log($$@)
{   my ($self, $level, $msg) = (shift, shift, shift);
    $msg = sprintf $msg, @_ if @_;
    $msg =~ s/\n$//g;  # some log lines have a trailing newline

    my $reason = $levelToReason[$level] or return;
    report $reason => $msg;
}

# use Log::Report for hooks
sub write_to_log_hook { panic "write_to_log_hook cannot be used" }


sub outputCharset() {shift->{output_charset}}


sub isSupportedVersion($)
{   my $support = shift->{supported};
    $support eq 'ANY' || $support eq shift();
}


sub run(@)
{   my $self = shift;
    $self->SUPER::run
     ( no_client_stdout => 1    # it's a daemon, you know
     , @_
     , log_file         => undef # Net::Server should not mess with my preps
     );
}


# defined by Net::Server
sub process_request(@) { panic "must be extended" }

sub process($$$)
{   my ($self, $request, $xmlin) = @_;
    $xmlin    = $xmlin->documentElement
        if $xmlin->isa('XML::LibXML::Document');

    my $local = $xmlin->localName;
    return $self->faultNotSoapMessage(type_of_node $xmlin)
        if $local ne 'Envelope';

    my $envns = $xmlin->namespaceURI || '';
    my $version
      = $envns eq SOAP11ENV ? 'SOAP11'
      : $envns eq SOAP12ENV ? 'SOAP12'
      : return $self->faultUnsupportedSoapVersion($envns);

    my $info  = XML::Compile::SOAP->messageStructure($xmlin);
    $info->{soap_version} = $version;

    my $handlers = $self->{$version} || {};

    keys %$handlers;  # reset each!
    while(my ($name, $handler) = each %$handlers)
    {
        my $xmlout = $handler->($name, $xmlin, $info);
        defined $xmlout or next;

        trace "call $version $name";
        return $self->acceptResponse($request, $xmlout);
    }

    my $bodyel = $info->{body}[0] || '(none)';
    my @other  = sort grep {$_ ne $version && keys %{$self->{$_}}}
        $self->soapVersions;

    return $self->faultTryOtherProtocol($version, $bodyel, \@other)
        if @other;

    my @available = sort keys %$handlers;
    $self->faultMessageNotRecognized($version, $bodyel, \@available);
}


sub operationsFromWSDL($@)
{   my ($self, $wsdl, %args) = @_;
    my $callbacks = $args{callbacks} || {};
    my %names;

    my $default = $args{default_callback}
               || sub {$self->faultNotImplemented(@_)};

    foreach my $version ($self->soapVersions)
    {   $self->isSupportedVersion($version) or next;

        my $soap = "XML::Compile::${version}::Server"
           ->new(schemas => $wsdl->schemas);

        my @ops  = $wsdl->operations(produce => 'OBJECTS', soap => $soap);
        unless(@ops)
        {   info __x"no operations for {soap}; skipped", soap => $version;
            next;
        }

        foreach my $op (@ops)
        {   my $name = $op->name;
            $names{$name}++;
            my $callback = $callbacks->{$name} || $default;
            my $has_callback = defined $callbacks->{$name};

            ref $callback eq 'CODE'
                or error __x"callback {name} must provide a CODE ref"
                     , name => $name;

            my $code = $op->compileHandler
              ( soap     => $soap
              , callback => $callback
              );

            if($has_callback)
            {   trace __x"add handler for {soap} named {name}"
                  , soap => $version, name => $name;
            }
            else
            {   trace __x"add stub handler for {soap} named {name}"
                  , soap => $version, name => $name;
            }

            $self->addHandler($name, $version, $code);
        }

        info __x"added {nr} operations for {soap} from WSDL"
          , nr => scalar @ops, soap => $version;
    }

    # the same handler can be used for different soap versions, so we
    # should not complain too early.
    delete $callbacks->{$_} for keys %names;
    error __x"no port with name {names}", names => keys %$callbacks
        if keys %$callbacks;
}


sub addHandler($$$)
{   my ($self, $name, $soap, $code) = @_;

    my $version = ref $soap ? $soap->version : $soap;
    $self->{$version}{$name} = $code;
}


sub handlers($)
{   my ($self, $soap) = @_;
    my $version = ref $soap ? $soap->version : $soap;
    my $table   = $self->{$version} || {};
    keys %$table;
}


sub soapVersions() { qw/SOAP11 SOAP12/ }


sub inputToXML($$$)
{   my ($self, $client, $action, $strref) = @_;

    my $input = try { $parser->parse_string($$strref) };
    if($@ || !$input)
    {   info __x"request from {client}, {action} parse error: {msg}"
           , client => $client, action => $action, msg => $@;
        return undef;
    }

    $input;
}



sub printIndex(;$)
{   my $self = shift;
    my $fh   = shift || \*STDOUT;

    foreach my $version ($self->soapVersions)
    {   my @handlers = $self->handlers;
        @handlers or next;

        local $" = "\n   ";
        $fh->print("$version:\n   @handlers\n");
    }
}


sub faultNotSoapMessage($)
{   my ($self, $type) = @_;

    my $text =
        __x"The message was XML, but not SOAP; not an Envelope but `{type}'"
      , type => $type;

    $self->soapFault
      ( 'SOAP11'
      , XML::Compile::SOAP11::Server->faultMessageNotRecognized($text)
      , RC_BAD_GATEWAY
      , 'message not SOAP'
      );
}


sub faultUnsupportedSoapVersion($)
{   my ($self, $envns) = @_;

    my $text =
        __x"The soap version `{envns}' is not supported"
      , envns => $envns;

    $self->soapFault
      ( 'SOAP11'
      , XML::Compile::SOAP11::Server->faultUnsupportedSoapVersion($text)
      , RC_BAD_GATEWAY
      , 'SOAP version not supported'
      );
}


sub acceptResponse($) { $_[2] }


sub soapFault($$$$)
{   my ($self, $version, $data, $rc, $abstract) = @_;
    my $writer = $faultWriter{$version}
        or panic "soapFault no writer for $version";
    $writer->($data);
}


sub faultMessageNotRecognized($$$)
{   my ($self, $version, $name, $handlers) = @_;

    my $text =
       __x "{version} body element {name} not recognized, available are {def}"
        , version => $version, name => $name, def => $handlers;

    $self->soapFault
      ( $version,
      , "XML::Compile::${version}::Server"->faultMessageNotRecognized($text)
      , RC_NOT_IMPLEMENTED
      , 'message not recognized'
      );
}


sub faultTryOtherProtocol($$$)
{   my ($self, $version, $name, $other) = @_;

    my $text =
      __x"body element {name} not available in {version}, try {other}"
        , name => $name, version => $version, other => $other;

    $self->soapFault
      ( $version,
      , "XML::Compile::${version}::Server"->faultTryOtherProtocol($text)
      , RC_SEE_OTHER
      , 'SOAP protocol not in use'
      );
}


sub faultNotImplemented($$$)
{   my ($self, $name, $xml, $info) = @_;
    my $version = $info->{soap_version};

    my $text =
      __x"procedure {name} for {version} is not yet implemented"
        , name => $name, version => $version;

    $self->soapFault
      ( $version,
      , "XML::Compile::${version}::Server"->faultNotImplemented($text)
      , RC_NOT_IMPLEMENTED
      , 'procedure stub called'
      );
}


1;
